diff --git a/apps/ruststack-server/src/events_bridge.rs b/apps/ruststack-server/src/events_bridge.rs
index 79f984d..43af4df 100644
--- a/apps/ruststack-server/src/events_bridge.rs
+++ b/apps/ruststack-server/src/events_bridge.rs
@@ -7,7 +7,6 @@
use std::sync::Arc;
use async_trait::async_trait;
-
use ruststack_events_core::delivery::{DeliveryError, TargetDelivery};
use ruststack_sqs_core::provider::RustStackSqs;
use ruststack_sqs_model::input::SendMessageInput;
diff --git a/apps/ruststack-server/src/gateway.rs b/apps/ruststack-server/src/gateway.rs
index 43abf14..6f5bf71 100644
--- a/apps/ruststack-server/src/gateway.rs
+++ b/apps/ruststack-server/src/gateway.rs
@@ -8,13 +8,9 @@
//! intercepted at the gateway level and return a combined status for all
//! registered services.
-use std::convert::Infallible;
-use std::future::Future;
-use std::pin::Pin;
-use std::sync::Arc;
+use std::{convert::Infallible, future::Future, pin::Pin, sync::Arc};
-use hyper::body::Incoming;
-use hyper::service::Service;
+use hyper::{body::Incoming, service::Service};
use crate::service::{GatewayBody, ServiceRouter, gateway_body_from_string};
diff --git a/apps/ruststack-server/src/handler.rs b/apps/ruststack-server/src/handler.rs
index b8dd18a..23350c1 100644
--- a/apps/ruststack-server/src/handler.rs
+++ b/apps/ruststack-server/src/handler.rs
@@ -5,22 +5,20 @@
//! dispatched to the corresponding `handle_*` method on [`RustStackS3`], with request
//! deserialization via [`FromS3Request`] and response serialization via [`IntoS3Response`].
-use std::collections::HashMap;
-use std::future::Future;
-use std::pin::Pin;
+use std::{collections::HashMap, future::Future, pin::Pin};
use bytes::Bytes;
use ruststack_s3_core::RustStackS3;
-use ruststack_s3_http::body::S3ResponseBody;
-use ruststack_s3_http::dispatch::S3Handler;
-use ruststack_s3_http::multipart;
-use ruststack_s3_http::request::FromS3Request;
-use ruststack_s3_http::response::IntoS3Response;
-use ruststack_s3_http::router::RoutingContext;
-use ruststack_s3_model::S3Operation;
-use ruststack_s3_model::error::{S3Error, S3ErrorCode};
-use ruststack_s3_model::input::PutObjectInput;
-use ruststack_s3_model::request::StreamingBlob;
+use ruststack_s3_http::{
+ body::S3ResponseBody, dispatch::S3Handler, multipart, request::FromS3Request,
+ response::IntoS3Response, router::RoutingContext,
+};
+use ruststack_s3_model::{
+ S3Operation,
+ error::{S3Error, S3ErrorCode},
+ input::PutObjectInput,
+ request::StreamingBlob,
+};
/// Wrapper that implements [`S3Handler`] by delegating to [`RustStackS3`] handler methods.
#[derive(Debug, Clone)]
@@ -623,13 +621,11 @@ async fn dispatch_post_object(
let etag = output.e_tag.unwrap_or_default();
let location = format!("/{bucket_name}/{key}");
let xml = format!(
- "\n\
- \n\
- {location}\n\
- {bucket_name}\n\
- {key}\n\
- {etag}\n\
- "
+ "\n\n{location}\\
+ \
+ n{bucket_name}\n{key}\n{etag}\n\
+ PostResponse>"
);
http::Response::builder()
.status(success_status)
diff --git a/apps/ruststack-server/src/main.rs b/apps/ruststack-server/src/main.rs
index d41def0..71a5452 100644
--- a/apps/ruststack-server/src/main.rs
+++ b/apps/ruststack-server/src/main.rs
@@ -30,16 +30,29 @@ mod service;
#[cfg(feature = "sns")]
mod sns_bridge;
-use std::net::SocketAddr;
-use std::sync::Arc;
+use std::{net::SocketAddr, sync::Arc};
use anyhow::{Context, Result};
-use hyper_util::rt::{TokioExecutor, TokioIo};
-use hyper_util::server::conn::auto::Builder as HttpConnBuilder;
-use tokio::net::TcpListener;
-use tracing::{info, warn};
-use tracing_subscriber::EnvFilter;
-
+use hyper_util::{
+ rt::{TokioExecutor, TokioIo},
+ server::conn::auto::Builder as HttpConnBuilder,
+};
+#[cfg(feature = "apigatewayv2")]
+use ruststack_apigatewayv2_core::config::ApiGatewayV2Config;
+#[cfg(feature = "apigatewayv2")]
+use ruststack_apigatewayv2_core::handler::RustStackApiGatewayV2Handler;
+#[cfg(feature = "apigatewayv2")]
+use ruststack_apigatewayv2_core::provider::RustStackApiGatewayV2;
+#[cfg(feature = "apigatewayv2")]
+use ruststack_apigatewayv2_http::service::{ApiGatewayV2HttpConfig, ApiGatewayV2HttpService};
+#[cfg(feature = "cloudwatch")]
+use ruststack_cloudwatch_core::config::CloudWatchConfig;
+#[cfg(feature = "cloudwatch")]
+use ruststack_cloudwatch_core::handler::RustStackCloudWatchHandler;
+#[cfg(feature = "cloudwatch")]
+use ruststack_cloudwatch_core::provider::RustStackCloudWatch;
+#[cfg(feature = "cloudwatch")]
+use ruststack_cloudwatch_http::service::{CloudWatchHttpConfig, CloudWatchHttpService};
#[cfg(feature = "dynamodb")]
use ruststack_dynamodb_core::config::DynamoDBConfig;
#[cfg(feature = "dynamodb")]
@@ -48,47 +61,22 @@ use ruststack_dynamodb_core::handler::RustStackDynamoDBHandler;
use ruststack_dynamodb_core::provider::RustStackDynamoDB;
#[cfg(feature = "dynamodb")]
use ruststack_dynamodb_http::service::{DynamoDBHttpConfig, DynamoDBHttpService};
-
-#[cfg(feature = "sqs")]
-use ruststack_sqs_core::config::SqsConfig;
-#[cfg(feature = "sqs")]
-use ruststack_sqs_core::handler::RustStackSqsHandler;
-#[cfg(feature = "sqs")]
-use ruststack_sqs_core::provider::RustStackSqs;
-#[cfg(feature = "sqs")]
-use ruststack_sqs_http::service::{SqsHttpConfig, SqsHttpService};
-
-#[cfg(feature = "ssm")]
-use ruststack_ssm_core::config::SsmConfig;
-#[cfg(feature = "ssm")]
-use ruststack_ssm_core::handler::RustStackSsmHandler;
-#[cfg(feature = "ssm")]
-use ruststack_ssm_core::provider::RustStackSsm;
-#[cfg(feature = "ssm")]
-use ruststack_ssm_http::service::{SsmHttpConfig, SsmHttpService};
-
-#[cfg(feature = "sns")]
-use crate::sns_bridge::RustStackSqsPublisher;
-#[cfg(feature = "sns")]
-use ruststack_sns_core::config::SnsConfig;
-#[cfg(feature = "sns")]
-use ruststack_sns_core::handler::RustStackSnsHandler;
-#[cfg(feature = "sns")]
-use ruststack_sns_core::provider::RustStackSns;
-#[cfg(feature = "sns")]
-use ruststack_sns_http::service::{SnsHttpConfig, SnsHttpService};
-
-#[cfg(feature = "lambda")]
-use ruststack_lambda_core::config::LambdaConfig;
-#[cfg(feature = "lambda")]
-use ruststack_lambda_core::handler::RustStackLambdaHandler;
-#[cfg(feature = "lambda")]
-use ruststack_lambda_core::provider::RustStackLambda;
-#[cfg(feature = "lambda")]
-use ruststack_lambda_http::service::{LambdaHttpConfig, LambdaHttpService};
-
-#[cfg(feature = "events")]
-use crate::events_bridge::LocalTargetDelivery;
+#[cfg(feature = "dynamodbstreams")]
+use ruststack_dynamodbstreams_core::config::DynamoDBStreamsConfig;
+#[cfg(feature = "dynamodbstreams")]
+use ruststack_dynamodbstreams_core::emitter::{
+ DynamoDBStreamEmitter, DynamoDBStreamLifecycleManager,
+};
+#[cfg(feature = "dynamodbstreams")]
+use ruststack_dynamodbstreams_core::handler::RustStackDynamoDBStreamsHandler;
+#[cfg(feature = "dynamodbstreams")]
+use ruststack_dynamodbstreams_core::provider::RustStackDynamoDBStreams;
+#[cfg(feature = "dynamodbstreams")]
+use ruststack_dynamodbstreams_core::storage::StreamStore;
+#[cfg(feature = "dynamodbstreams")]
+use ruststack_dynamodbstreams_http::service::{
+ DynamoDBStreamsHttpConfig, DynamoDBStreamsHttpService,
+};
#[cfg(feature = "events")]
use ruststack_events_core::config::EventsConfig;
#[cfg(feature = "events")]
@@ -97,25 +85,16 @@ use ruststack_events_core::handler::RustStackEventsHandler;
use ruststack_events_core::provider::RustStackEvents;
#[cfg(feature = "events")]
use ruststack_events_http::service::{EventsHttpConfig, EventsHttpService};
-
-#[cfg(feature = "logs")]
-use ruststack_logs_core::config::LogsConfig;
-#[cfg(feature = "logs")]
-use ruststack_logs_core::handler::RustStackLogsHandler;
-#[cfg(feature = "logs")]
-use ruststack_logs_core::provider::RustStackLogs;
-#[cfg(feature = "logs")]
-use ruststack_logs_http::service::{LogsHttpConfig, LogsHttpService};
-
-#[cfg(feature = "kms")]
-use ruststack_kms_core::config::KmsConfig;
-#[cfg(feature = "kms")]
-use ruststack_kms_core::handler::RustStackKmsHandler;
-#[cfg(feature = "kms")]
-use ruststack_kms_core::provider::RustStackKms;
-#[cfg(feature = "kms")]
-use ruststack_kms_http::service::{KmsHttpConfig, KmsHttpService};
-
+#[cfg(feature = "iam")]
+use ruststack_iam_core::config::IamConfig;
+#[cfg(feature = "iam")]
+use ruststack_iam_core::handler::RustStackIamHandler;
+#[cfg(feature = "iam")]
+use ruststack_iam_core::provider::RustStackIam;
+#[cfg(feature = "iam")]
+use ruststack_iam_core::store::IamStore;
+#[cfg(feature = "iam")]
+use ruststack_iam_http::service::{IamHttpConfig, IamHttpService};
#[cfg(feature = "kinesis")]
use ruststack_kinesis_core::config::KinesisConfig;
#[cfg(feature = "kinesis")]
@@ -124,7 +103,34 @@ use ruststack_kinesis_core::handler::RustStackKinesisHandler;
use ruststack_kinesis_core::provider::RustStackKinesis;
#[cfg(feature = "kinesis")]
use ruststack_kinesis_http::service::{KinesisHttpConfig, KinesisHttpService};
-
+#[cfg(feature = "kms")]
+use ruststack_kms_core::config::KmsConfig;
+#[cfg(feature = "kms")]
+use ruststack_kms_core::handler::RustStackKmsHandler;
+#[cfg(feature = "kms")]
+use ruststack_kms_core::provider::RustStackKms;
+#[cfg(feature = "kms")]
+use ruststack_kms_http::service::{KmsHttpConfig, KmsHttpService};
+#[cfg(feature = "lambda")]
+use ruststack_lambda_core::config::LambdaConfig;
+#[cfg(feature = "lambda")]
+use ruststack_lambda_core::handler::RustStackLambdaHandler;
+#[cfg(feature = "lambda")]
+use ruststack_lambda_core::provider::RustStackLambda;
+#[cfg(feature = "lambda")]
+use ruststack_lambda_http::service::{LambdaHttpConfig, LambdaHttpService};
+#[cfg(feature = "logs")]
+use ruststack_logs_core::config::LogsConfig;
+#[cfg(feature = "logs")]
+use ruststack_logs_core::handler::RustStackLogsHandler;
+#[cfg(feature = "logs")]
+use ruststack_logs_core::provider::RustStackLogs;
+#[cfg(feature = "logs")]
+use ruststack_logs_http::service::{LogsHttpConfig, LogsHttpService};
+#[cfg(feature = "s3")]
+use ruststack_s3_core::{RustStackS3, S3Config};
+#[cfg(feature = "s3")]
+use ruststack_s3_http::service::{S3HttpConfig, S3HttpService};
#[cfg(feature = "secretsmanager")]
use ruststack_secretsmanager_core::config::SecretsManagerConfig;
#[cfg(feature = "secretsmanager")]
@@ -133,7 +139,6 @@ use ruststack_secretsmanager_core::handler::RustStackSecretsManagerHandler;
use ruststack_secretsmanager_core::provider::RustStackSecretsManager;
#[cfg(feature = "secretsmanager")]
use ruststack_secretsmanager_http::service::{SecretsManagerHttpConfig, SecretsManagerHttpService};
-
#[cfg(feature = "ses")]
use ruststack_ses_core::config::SesConfig;
#[cfg(feature = "ses")]
@@ -144,53 +149,30 @@ use ruststack_ses_core::provider::RustStackSes;
use ruststack_ses_http::service::{SesHttpConfig, SesHttpService};
#[cfg(feature = "ses")]
use ruststack_ses_http::v2::SesV2HttpService;
-
-#[cfg(feature = "apigatewayv2")]
-use ruststack_apigatewayv2_core::config::ApiGatewayV2Config;
-#[cfg(feature = "apigatewayv2")]
-use ruststack_apigatewayv2_core::handler::RustStackApiGatewayV2Handler;
-#[cfg(feature = "apigatewayv2")]
-use ruststack_apigatewayv2_core::provider::RustStackApiGatewayV2;
-#[cfg(feature = "apigatewayv2")]
-use ruststack_apigatewayv2_http::service::{ApiGatewayV2HttpConfig, ApiGatewayV2HttpService};
-
-#[cfg(feature = "dynamodbstreams")]
-use ruststack_dynamodbstreams_core::config::DynamoDBStreamsConfig;
-#[cfg(feature = "dynamodbstreams")]
-use ruststack_dynamodbstreams_core::emitter::{
- DynamoDBStreamEmitter, DynamoDBStreamLifecycleManager,
-};
-#[cfg(feature = "dynamodbstreams")]
-use ruststack_dynamodbstreams_core::handler::RustStackDynamoDBStreamsHandler;
-#[cfg(feature = "dynamodbstreams")]
-use ruststack_dynamodbstreams_core::provider::RustStackDynamoDBStreams;
-#[cfg(feature = "dynamodbstreams")]
-use ruststack_dynamodbstreams_core::storage::StreamStore;
-#[cfg(feature = "dynamodbstreams")]
-use ruststack_dynamodbstreams_http::service::{
- DynamoDBStreamsHttpConfig, DynamoDBStreamsHttpService,
-};
-
-#[cfg(feature = "cloudwatch")]
-use ruststack_cloudwatch_core::config::CloudWatchConfig;
-#[cfg(feature = "cloudwatch")]
-use ruststack_cloudwatch_core::handler::RustStackCloudWatchHandler;
-#[cfg(feature = "cloudwatch")]
-use ruststack_cloudwatch_core::provider::RustStackCloudWatch;
-#[cfg(feature = "cloudwatch")]
-use ruststack_cloudwatch_http::service::{CloudWatchHttpConfig, CloudWatchHttpService};
-
-#[cfg(feature = "iam")]
-use ruststack_iam_core::config::IamConfig;
-#[cfg(feature = "iam")]
-use ruststack_iam_core::handler::RustStackIamHandler;
-#[cfg(feature = "iam")]
-use ruststack_iam_core::provider::RustStackIam;
-#[cfg(feature = "iam")]
-use ruststack_iam_core::store::IamStore;
-#[cfg(feature = "iam")]
-use ruststack_iam_http::service::{IamHttpConfig, IamHttpService};
-
+#[cfg(feature = "sns")]
+use ruststack_sns_core::config::SnsConfig;
+#[cfg(feature = "sns")]
+use ruststack_sns_core::handler::RustStackSnsHandler;
+#[cfg(feature = "sns")]
+use ruststack_sns_core::provider::RustStackSns;
+#[cfg(feature = "sns")]
+use ruststack_sns_http::service::{SnsHttpConfig, SnsHttpService};
+#[cfg(feature = "sqs")]
+use ruststack_sqs_core::config::SqsConfig;
+#[cfg(feature = "sqs")]
+use ruststack_sqs_core::handler::RustStackSqsHandler;
+#[cfg(feature = "sqs")]
+use ruststack_sqs_core::provider::RustStackSqs;
+#[cfg(feature = "sqs")]
+use ruststack_sqs_http::service::{SqsHttpConfig, SqsHttpService};
+#[cfg(feature = "ssm")]
+use ruststack_ssm_core::config::SsmConfig;
+#[cfg(feature = "ssm")]
+use ruststack_ssm_core::handler::RustStackSsmHandler;
+#[cfg(feature = "ssm")]
+use ruststack_ssm_core::provider::RustStackSsm;
+#[cfg(feature = "ssm")]
+use ruststack_ssm_http::service::{SsmHttpConfig, SsmHttpService};
#[cfg(feature = "sts")]
use ruststack_sts_core::config::StsConfig;
#[cfg(feature = "sts")]
@@ -199,14 +181,15 @@ use ruststack_sts_core::handler::RustStackStsHandler;
use ruststack_sts_core::provider::RustStackSts;
#[cfg(feature = "sts")]
use ruststack_sts_http::service::{StsHttpConfig, StsHttpService};
+use tokio::net::TcpListener;
+use tracing::{info, warn};
+use tracing_subscriber::EnvFilter;
-#[cfg(feature = "s3")]
-use ruststack_s3_core::{RustStackS3, S3Config};
-#[cfg(feature = "s3")]
-use ruststack_s3_http::service::{S3HttpConfig, S3HttpService};
-
-use crate::gateway::GatewayService;
-use crate::service::ServiceRouter;
+#[cfg(feature = "events")]
+use crate::events_bridge::LocalTargetDelivery;
+#[cfg(feature = "sns")]
+use crate::sns_bridge::RustStackSqsPublisher;
+use crate::{gateway::GatewayService, service::ServiceRouter};
/// Server version reported in health check responses.
const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -630,8 +613,10 @@ fn parse_services_value(raw: &str) -> Vec {
/// Exits with code 0 if the response is 200 OK and contains at least one
/// running service, 1 otherwise.
async fn run_health_check(addr: &str) -> Result<()> {
- use tokio::io::{AsyncReadExt, AsyncWriteExt};
- use tokio::net::TcpStream;
+ use tokio::{
+ io::{AsyncReadExt, AsyncWriteExt},
+ net::TcpStream,
+ };
let mut stream = TcpStream::connect(addr)
.await
@@ -1029,8 +1014,8 @@ async fn main() -> Result<()> {
if services.is_empty() {
anyhow::bail!(
- "no services enabled. Check the SERVICES environment variable \
- and compiled feature flags."
+ "no services enabled. Check the SERVICES environment variable and compiled feature \
+ flags."
);
}
diff --git a/apps/ruststack-server/src/service.rs b/apps/ruststack-server/src/service.rs
index 9926a7e..f719755 100644
--- a/apps/ruststack-server/src/service.rs
+++ b/apps/ruststack-server/src/service.rs
@@ -6,14 +6,10 @@
//!
//! [`GatewayBody`] is a type-erased HTTP response body shared by all services.
-use std::convert::Infallible;
-use std::future::Future;
-use std::io;
-use std::pin::Pin;
+use std::{convert::Infallible, future::Future, io, pin::Pin};
use bytes::Bytes;
-use http_body_util::combinators::BoxBody;
-use http_body_util::{BodyExt, Full};
+use http_body_util::{BodyExt, Full, combinators::BoxBody};
use hyper::body::Incoming;
/// Type-erased response body used by the gateway.
@@ -53,15 +49,11 @@ pub trait ServiceRouter: Send + Sync {
#[cfg(feature = "s3")]
mod s3_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_s3_http::dispatch::S3Handler;
- use ruststack_s3_http::service::S3HttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_s3_http::{dispatch::S3Handler, service::S3HttpService};
use super::{GatewayBody, ServiceRouter};
@@ -113,15 +105,11 @@ pub use s3_router::S3ServiceRouter;
#[cfg(feature = "dynamodb")]
mod dynamodb_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_dynamodb_http::dispatch::DynamoDBHandler;
- use ruststack_dynamodb_http::service::DynamoDBHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_dynamodb_http::{dispatch::DynamoDBHandler, service::DynamoDBHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -174,15 +162,13 @@ pub use dynamodb_router::DynamoDBServiceRouter;
#[cfg(feature = "dynamodbstreams")]
mod dynamodbstreams_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_dynamodbstreams_http::dispatch::DynamoDBStreamsHandler;
- use ruststack_dynamodbstreams_http::service::DynamoDBStreamsHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_dynamodbstreams_http::{
+ dispatch::DynamoDBStreamsHandler, service::DynamoDBStreamsHttpService,
+ };
use super::{GatewayBody, ServiceRouter};
@@ -235,15 +221,11 @@ pub use dynamodbstreams_router::DynamoDBStreamsServiceRouter;
#[cfg(feature = "sqs")]
mod sqs_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_sqs_http::dispatch::SqsHandler;
- use ruststack_sqs_http::service::SqsHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_sqs_http::{dispatch::SqsHandler, service::SqsHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -296,15 +278,11 @@ pub use sqs_router::SqsServiceRouter;
#[cfg(feature = "ssm")]
mod ssm_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_ssm_http::dispatch::SsmHandler;
- use ruststack_ssm_http::service::SsmHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_ssm_http::{dispatch::SsmHandler, service::SsmHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -357,15 +335,11 @@ pub use ssm_router::SsmServiceRouter;
#[cfg(feature = "sns")]
mod sns_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_sns_http::dispatch::SnsHandler;
- use ruststack_sns_http::service::SnsHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_sns_http::{dispatch::SnsHandler, service::SnsHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -448,15 +422,11 @@ pub use sns_router::SnsServiceRouter;
#[cfg(feature = "lambda")]
mod lambda_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_lambda_http::dispatch::LambdaHandler;
- use ruststack_lambda_http::service::LambdaHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_lambda_http::{dispatch::LambdaHandler, service::LambdaHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -491,6 +461,27 @@ mod lambda_router {
}
}
}
+ // Layer paths (e.g., /2018-10-31/layers/{name}/versions).
+ if path.contains("/layers") {
+ if let Some(rest) = path.strip_prefix('/') {
+ let parts: Vec<&str> = rest.splitn(2, '/').collect();
+ if parts.len() == 2 && parts[0].len() == 10 && parts[1].starts_with("layers") {
+ return true;
+ }
+ }
+ }
+ // Event source mapping paths (e.g., /2015-03-31/event-source-mappings/).
+ if path.contains("/event-source-mappings") {
+ if let Some(rest) = path.strip_prefix('/') {
+ let parts: Vec<&str> = rest.splitn(2, '/').collect();
+ if parts.len() == 2
+ && parts[0].len() == 10
+ && parts[1].starts_with("event-source-mappings")
+ {
+ return true;
+ }
+ }
+ }
// Function URL invocation paths.
path.starts_with("/lambda-url/")
}
@@ -543,15 +534,11 @@ pub use lambda_router::LambdaServiceRouter;
#[cfg(feature = "events")]
mod events_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_events_http::dispatch::EventsHandler;
- use ruststack_events_http::service::EventsHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_events_http::{dispatch::EventsHandler, service::EventsHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -604,15 +591,11 @@ pub use events_router::EventsServiceRouter;
#[cfg(feature = "logs")]
mod logs_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_logs_http::dispatch::LogsHandler;
- use ruststack_logs_http::service::LogsHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_logs_http::{dispatch::LogsHandler, service::LogsHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -665,15 +648,11 @@ pub use logs_router::LogsServiceRouter;
#[cfg(feature = "kms")]
mod kms_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_kms_http::dispatch::KmsHandler;
- use ruststack_kms_http::service::KmsHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_kms_http::{dispatch::KmsHandler, service::KmsHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -726,15 +705,11 @@ pub use kms_router::KmsServiceRouter;
#[cfg(feature = "kinesis")]
mod kinesis_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_kinesis_http::dispatch::KinesisHandler;
- use ruststack_kinesis_http::service::KinesisHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_kinesis_http::{dispatch::KinesisHandler, service::KinesisHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -787,15 +762,13 @@ pub use kinesis_router::KinesisServiceRouter;
#[cfg(feature = "secretsmanager")]
mod secretsmanager_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_secretsmanager_http::dispatch::SecretsManagerHandler;
- use ruststack_secretsmanager_http::service::SecretsManagerHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_secretsmanager_http::{
+ dispatch::SecretsManagerHandler, service::SecretsManagerHttpService,
+ };
use super::{GatewayBody, ServiceRouter};
@@ -848,16 +821,11 @@ pub use secretsmanager_router::SecretsManagerServiceRouter;
#[cfg(feature = "ses")]
mod ses_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_ses_http::dispatch::SesHandler;
- use ruststack_ses_http::service::SesHttpService;
- use ruststack_ses_http::v2::SesV2HttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_ses_http::{dispatch::SesHandler, service::SesHttpService, v2::SesV2HttpService};
use super::{GatewayBody, ServiceRouter};
@@ -978,7 +946,8 @@ mod ses_router {
headers.insert(
"authorization",
http::HeaderValue::from_static(
- "AWS4-HMAC-SHA256 Credential=test/20260319/us-east-1/ses/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=abc123",
+ "AWS4-HMAC-SHA256 Credential=test/20260319/us-east-1/ses/aws4_request, \
+ SignedHeaders=content-type;host;x-amz-date, Signature=abc123",
),
);
assert_eq!(extract_sigv4_service(&headers), Some("ses"));
@@ -990,7 +959,8 @@ mod ses_router {
headers.insert(
"authorization",
http::HeaderValue::from_static(
- "AWS4-HMAC-SHA256 Credential=AKID/20260319/us-east-1/email/aws4_request, SignedHeaders=host, Signature=abc123",
+ "AWS4-HMAC-SHA256 Credential=AKID/20260319/us-east-1/email/aws4_request, \
+ SignedHeaders=host, Signature=abc123",
),
);
assert_eq!(extract_sigv4_service(&headers), Some("email"));
@@ -1002,7 +972,8 @@ mod ses_router {
headers.insert(
"authorization",
http::HeaderValue::from_static(
- "AWS4-HMAC-SHA256 Credential=AKID/20260319/us-east-1/sns/aws4_request, SignedHeaders=host, Signature=abc123",
+ "AWS4-HMAC-SHA256 Credential=AKID/20260319/us-east-1/sns/aws4_request, \
+ SignedHeaders=host, Signature=abc123",
),
);
assert_eq!(extract_sigv4_service(&headers), Some("sns"));
@@ -1035,19 +1006,18 @@ pub use ses_router::SesServiceRouter;
#[cfg(feature = "apigatewayv2")]
mod apigatewayv2_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
- use std::sync::Arc;
+ use std::{convert::Infallible, future::Future, pin::Pin, sync::Arc};
use bytes::Bytes;
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_apigatewayv2_core::execution::{handle_execution, parse_execution_path};
- use ruststack_apigatewayv2_core::provider::RustStackApiGatewayV2;
- use ruststack_apigatewayv2_http::dispatch::ApiGatewayV2Handler;
- use ruststack_apigatewayv2_http::service::ApiGatewayV2HttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_apigatewayv2_core::{
+ execution::{handle_execution, parse_execution_path},
+ provider::RustStackApiGatewayV2,
+ };
+ use ruststack_apigatewayv2_http::{
+ dispatch::ApiGatewayV2Handler, service::ApiGatewayV2HttpService,
+ };
use super::{GatewayBody, ServiceRouter, gateway_body_from_string};
@@ -1223,15 +1193,11 @@ pub use apigatewayv2_router::{ApiGatewayV2ExecutionRouter, ApiGatewayV2Managemen
#[cfg(feature = "cloudwatch")]
mod cloudwatch_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_cloudwatch_http::dispatch::CloudWatchHandler;
- use ruststack_cloudwatch_http::service::CloudWatchHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_cloudwatch_http::{dispatch::CloudWatchHandler, service::CloudWatchHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -1276,8 +1242,8 @@ mod cloudwatch_router {
/// CloudWatch Metrics matches in three ways:
/// 1. awsQuery: form-urlencoded POST signed with `monitoring` SigV4 service.
/// 2. rpcv2Cbor path: POST to `/service/GraniteServiceVersion20100801/...`.
- /// 3. rpcv2Cbor header: POST with `smithy-protocol: rpc-v2-cbor` signed
- /// with `monitoring` SigV4 service (AWS SDK v1.108+).
+ /// 3. rpcv2Cbor header: POST with `smithy-protocol: rpc-v2-cbor` signed with `monitoring`
+ /// SigV4 service (AWS SDK v1.108+).
fn matches(&self, req: &http::Request) -> bool {
if *req.method() != http::Method::POST {
return false;
@@ -1347,15 +1313,11 @@ pub use cloudwatch_router::CloudWatchServiceRouter;
#[cfg(feature = "iam")]
mod iam_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_iam_http::dispatch::IamHandler;
- use ruststack_iam_http::service::IamHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_iam_http::{dispatch::IamHandler, service::IamHttpService};
use super::{GatewayBody, ServiceRouter};
@@ -1437,15 +1399,11 @@ pub use iam_router::IamServiceRouter;
#[cfg(feature = "sts")]
mod sts_router {
- use std::convert::Infallible;
- use std::future::Future;
- use std::pin::Pin;
+ use std::{convert::Infallible, future::Future, pin::Pin};
use http_body_util::BodyExt;
- use hyper::body::Incoming;
- use hyper::service::Service;
- use ruststack_sts_http::dispatch::StsHandler;
- use ruststack_sts_http::service::StsHttpService;
+ use hyper::{body::Incoming, service::Service};
+ use ruststack_sts_http::{dispatch::StsHandler, service::StsHttpService};
use super::{GatewayBody, ServiceRouter};
diff --git a/apps/ruststack-server/src/sns_bridge.rs b/apps/ruststack-server/src/sns_bridge.rs
index 8e7db93..c34bb9e 100644
--- a/apps/ruststack-server/src/sns_bridge.rs
+++ b/apps/ruststack-server/src/sns_bridge.rs
@@ -7,9 +7,10 @@
use std::sync::Arc;
use async_trait::async_trait;
-
-use ruststack_sns_core::config::SnsConfig;
-use ruststack_sns_core::publisher::{DeliveryError, SqsPublisher};
+use ruststack_sns_core::{
+ config::SnsConfig,
+ publisher::{DeliveryError, SqsPublisher},
+};
use ruststack_sqs_core::provider::RustStackSqs;
use ruststack_sqs_model::input::SendMessageInput;
diff --git a/crates/ruststack-apigatewayv2-core/src/execution/http_proxy.rs b/crates/ruststack-apigatewayv2-core/src/execution/http_proxy.rs
index 03278bb..0b7c563 100644
--- a/crates/ruststack-apigatewayv2-core/src/execution/http_proxy.rs
+++ b/crates/ruststack-apigatewayv2-core/src/execution/http_proxy.rs
@@ -4,9 +4,9 @@
use bytes::Bytes;
-use crate::error::ApiGatewayV2ServiceError;
-use crate::provider::RustStackApiGatewayV2;
-use crate::storage::IntegrationRecord;
+use crate::{
+ error::ApiGatewayV2ServiceError, provider::RustStackApiGatewayV2, storage::IntegrationRecord,
+};
/// Handle an HTTP proxy integration.
///
diff --git a/crates/ruststack-apigatewayv2-core/src/execution/lambda_proxy.rs b/crates/ruststack-apigatewayv2-core/src/execution/lambda_proxy.rs
index 34c9365..97f41f8 100644
--- a/crates/ruststack-apigatewayv2-core/src/execution/lambda_proxy.rs
+++ b/crates/ruststack-apigatewayv2-core/src/execution/lambda_proxy.rs
@@ -8,11 +8,10 @@ use std::collections::HashMap;
use bytes::Bytes;
use serde::Deserialize;
-use crate::error::ApiGatewayV2ServiceError;
-use crate::provider::RustStackApiGatewayV2;
-use crate::storage::IntegrationRecord;
-
use super::event::build_lambda_event;
+use crate::{
+ error::ApiGatewayV2ServiceError, provider::RustStackApiGatewayV2, storage::IntegrationRecord,
+};
/// Lambda function response (payload format version 2.0).
#[derive(Debug, Deserialize)]
diff --git a/crates/ruststack-apigatewayv2-core/src/execution/mock_integration.rs b/crates/ruststack-apigatewayv2-core/src/execution/mock_integration.rs
index 0d152bd..30a1b7a 100644
--- a/crates/ruststack-apigatewayv2-core/src/execution/mock_integration.rs
+++ b/crates/ruststack-apigatewayv2-core/src/execution/mock_integration.rs
@@ -4,8 +4,7 @@
use bytes::Bytes;
-use crate::error::ApiGatewayV2ServiceError;
-use crate::storage::IntegrationRecord;
+use crate::{error::ApiGatewayV2ServiceError, storage::IntegrationRecord};
/// Handle a mock integration.
///
diff --git a/crates/ruststack-apigatewayv2-core/src/execution/mod.rs b/crates/ruststack-apigatewayv2-core/src/execution/mod.rs
index eedb499..1b2e1b3 100644
--- a/crates/ruststack-apigatewayv2-core/src/execution/mod.rs
+++ b/crates/ruststack-apigatewayv2-core/src/execution/mod.rs
@@ -13,8 +13,7 @@ pub mod router;
use bytes::Bytes;
use ruststack_apigatewayv2_model::types::IntegrationType;
-use crate::error::ApiGatewayV2ServiceError;
-use crate::provider::RustStackApiGatewayV2;
+use crate::{error::ApiGatewayV2ServiceError, provider::RustStackApiGatewayV2};
/// Target for an API execution request.
#[derive(Debug)]
diff --git a/crates/ruststack-apigatewayv2-core/src/handler.rs b/crates/ruststack-apigatewayv2-core/src/handler.rs
index 216784b..bc83400 100644
--- a/crates/ruststack-apigatewayv2-core/src/handler.rs
+++ b/crates/ruststack-apigatewayv2-core/src/handler.rs
@@ -6,20 +6,18 @@
//! Uses manual `Pin>` return types because the `ApiGatewayV2Handler`
//! trait requires object safety for `Arc`.
-use std::future::Future;
-use std::pin::Pin;
-use std::sync::Arc;
+use std::{future::Future, pin::Pin, sync::Arc};
use bytes::Bytes;
-
-use ruststack_apigatewayv2_http::body::ApiGatewayV2ResponseBody;
-use ruststack_apigatewayv2_http::dispatch::ApiGatewayV2Handler;
-use ruststack_apigatewayv2_http::response::{empty_response, json_response};
-use ruststack_apigatewayv2_http::router::PathParams;
-use ruststack_apigatewayv2_model::error::ApiGatewayV2Error;
+use ruststack_apigatewayv2_http::{
+ body::ApiGatewayV2ResponseBody,
+ dispatch::ApiGatewayV2Handler,
+ response::{empty_response, json_response},
+ router::PathParams,
+};
#[allow(clippy::wildcard_imports)]
use ruststack_apigatewayv2_model::input::*;
-use ruststack_apigatewayv2_model::operations::ApiGatewayV2Operation;
+use ruststack_apigatewayv2_model::{error::ApiGatewayV2Error, operations::ApiGatewayV2Operation};
use crate::provider::RustStackApiGatewayV2;
diff --git a/crates/ruststack-apigatewayv2-core/src/provider.rs b/crates/ruststack-apigatewayv2-core/src/provider.rs
index 1f34454..4dc53be 100644
--- a/crates/ruststack-apigatewayv2-core/src/provider.rs
+++ b/crates/ruststack-apigatewayv2-core/src/provider.rs
@@ -3,12 +3,9 @@
//! Implements all 56 API Gateway v2 operations, maintaining internal storage
//! and converting between model input/output types and internal records.
-use std::collections::HashMap;
-use std::sync::Arc;
+use std::{collections::HashMap, sync::Arc};
use chrono::Utc;
-use tracing::info;
-
#[allow(clippy::wildcard_imports)]
use ruststack_apigatewayv2_model::input::*;
#[allow(clippy::wildcard_imports)]
@@ -18,13 +15,16 @@ use ruststack_apigatewayv2_model::types::{
DeploymentStatus, DomainName, Integration, Model, MutualTlsAuthentication, Route,
RouteResponse, Stage, TlsConfig, VpcLink, VpcLinkStatus, VpcLinkVersion,
};
+use tracing::info;
-use crate::config::ApiGatewayV2Config;
-use crate::error::ApiGatewayV2ServiceError;
-use crate::storage::{
- ApiMappingRecord, ApiRecord, ApiStore, AuthorizerRecord, DeploymentRecord, DomainNameRecord,
- IntegrationRecord, ModelRecord, RouteRecord, RouteResponseRecord, StageRecord, VpcLinkRecord,
- generate_id,
+use crate::{
+ config::ApiGatewayV2Config,
+ error::ApiGatewayV2ServiceError,
+ storage::{
+ ApiMappingRecord, ApiRecord, ApiStore, AuthorizerRecord, DeploymentRecord,
+ DomainNameRecord, IntegrationRecord, ModelRecord, RouteRecord, RouteResponseRecord,
+ StageRecord, VpcLinkRecord, generate_id,
+ },
};
/// Main API Gateway v2 provider. Owns resource storage and configuration.
diff --git a/crates/ruststack-apigatewayv2-core/src/storage.rs b/crates/ruststack-apigatewayv2-core/src/storage.rs
index 06bae7a..b35cef5 100644
--- a/crates/ruststack-apigatewayv2-core/src/storage.rs
+++ b/crates/ruststack-apigatewayv2-core/src/storage.rs
@@ -9,7 +9,6 @@ use std::collections::HashMap;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use rand::Rng;
-
use ruststack_apigatewayv2_model::types::{
AccessLogSettings, AuthorizationType, AuthorizerType, ConnectionType, ContentHandlingStrategy,
Cors, DeploymentStatus, DomainNameConfiguration, IntegrationType, IpAddressType,
diff --git a/crates/ruststack-apigatewayv2-http/src/body.rs b/crates/ruststack-apigatewayv2-http/src/body.rs
index 82bacfc..313ebac 100644
--- a/crates/ruststack-apigatewayv2-http/src/body.rs
+++ b/crates/ruststack-apigatewayv2-http/src/body.rs
@@ -1,7 +1,9 @@
//! API Gateway v2 HTTP response body type.
-use std::pin::Pin;
-use std::task::{Context, Poll};
+use std::{
+ pin::Pin,
+ task::{Context, Poll},
+};
use bytes::Bytes;
use http_body_util::Full;
diff --git a/crates/ruststack-apigatewayv2-http/src/dispatch.rs b/crates/ruststack-apigatewayv2-http/src/dispatch.rs
index 23534ad..edc9e04 100644
--- a/crates/ruststack-apigatewayv2-http/src/dispatch.rs
+++ b/crates/ruststack-apigatewayv2-http/src/dispatch.rs
@@ -3,16 +3,12 @@
//! Uses manual `Pin>` return types because `ApiGatewayV2Handler`
//! requires object safety for dynamic dispatch (`Arc`).
-use std::future::Future;
-use std::pin::Pin;
+use std::{future::Future, pin::Pin};
use bytes::Bytes;
+use ruststack_apigatewayv2_model::{error::ApiGatewayV2Error, operations::ApiGatewayV2Operation};
-use ruststack_apigatewayv2_model::error::ApiGatewayV2Error;
-use ruststack_apigatewayv2_model::operations::ApiGatewayV2Operation;
-
-use crate::body::ApiGatewayV2ResponseBody;
-use crate::router::PathParams;
+use crate::{body::ApiGatewayV2ResponseBody, router::PathParams};
/// The boundary between HTTP and business logic for API Gateway v2.
///
diff --git a/crates/ruststack-apigatewayv2-http/src/response.rs b/crates/ruststack-apigatewayv2-http/src/response.rs
index 8bdb6e9..0b9e529 100644
--- a/crates/ruststack-apigatewayv2-http/src/response.rs
+++ b/crates/ruststack-apigatewayv2-http/src/response.rs
@@ -4,9 +4,8 @@
//! `{"message": "..."}`
use bytes::Bytes;
-use serde::Serialize;
-
use ruststack_apigatewayv2_model::error::ApiGatewayV2Error;
+use serde::Serialize;
/// Content type for API Gateway v2 JSON responses.
pub const CONTENT_TYPE: &str = "application/json";
@@ -74,9 +73,10 @@ pub fn empty_response(status: u16) -> Result, ApiGatewayV2
#[cfg(test)]
mod tests {
- use super::*;
use ruststack_apigatewayv2_model::error::ApiGatewayV2ErrorCode;
+ use super::*;
+
#[test]
fn test_should_format_error_with_lowercase_message() {
let err = ApiGatewayV2Error::with_message(
diff --git a/crates/ruststack-apigatewayv2-http/src/router.rs b/crates/ruststack-apigatewayv2-http/src/router.rs
index eb4b53d..2309edf 100644
--- a/crates/ruststack-apigatewayv2-http/src/router.rs
+++ b/crates/ruststack-apigatewayv2-http/src/router.rs
@@ -5,8 +5,10 @@
use std::collections::HashMap;
-use ruststack_apigatewayv2_model::error::ApiGatewayV2Error;
-use ruststack_apigatewayv2_model::operations::{APIGATEWAYV2_ROUTES, ApiGatewayV2Operation};
+use ruststack_apigatewayv2_model::{
+ error::ApiGatewayV2Error,
+ operations::{APIGATEWAYV2_ROUTES, ApiGatewayV2Operation},
+};
/// Extracted path parameters from a matched route.
#[derive(Debug, Clone, Default)]
diff --git a/crates/ruststack-apigatewayv2-http/src/service.rs b/crates/ruststack-apigatewayv2-http/src/service.rs
index cfd06b2..6a8f17d 100644
--- a/crates/ruststack-apigatewayv2-http/src/service.rs
+++ b/crates/ruststack-apigatewayv2-http/src/service.rs
@@ -1,20 +1,18 @@
//! API Gateway v2 HTTP service implementing the hyper `Service` trait.
-use std::convert::Infallible;
-use std::future::Future;
-use std::pin::Pin;
-use std::sync::Arc;
+use std::{convert::Infallible, future::Future, pin::Pin, sync::Arc};
+use bytes::Bytes;
use http_body_util::BodyExt;
use hyper::body::Incoming;
-
-use bytes::Bytes;
use ruststack_apigatewayv2_model::error::ApiGatewayV2Error;
-use crate::body::ApiGatewayV2ResponseBody;
-use crate::dispatch::{ApiGatewayV2Handler, dispatch_operation};
-use crate::response::{CONTENT_TYPE, error_to_response};
-use crate::router::resolve_operation;
+use crate::{
+ body::ApiGatewayV2ResponseBody,
+ dispatch::{ApiGatewayV2Handler, dispatch_operation},
+ response::{CONTENT_TYPE, error_to_response},
+ router::resolve_operation,
+};
/// Configuration for the API Gateway v2 HTTP service.
#[derive(Clone)]
diff --git a/crates/ruststack-auth/src/canonical.rs b/crates/ruststack-auth/src/canonical.rs
index b6e700a..f350546 100644
--- a/crates/ruststack-auth/src/canonical.rs
+++ b/crates/ruststack-auth/src/canonical.rs
@@ -68,9 +68,11 @@ pub fn build_canonical_request(
let canonical_headers = build_canonical_headers(headers, signed_headers);
let signed_headers_str = build_signed_headers_string(signed_headers);
- format!(
+ #[rustfmt::skip]
+ let result = format!(
"{method}\n{canonical_uri}\n{canonical_query}\n{canonical_headers}\n\n{signed_headers_str}\n{payload_hash}"
- )
+ );
+ result
}
/// Build the canonical URI by URI-encoding each path segment individually.
@@ -300,6 +302,7 @@ mod tests {
&headers.iter().map(|(k, v)| (*k, *v)).collect::>(),
&signed,
);
+ #[rustfmt::skip]
let expected = "host:examplebucket.s3.amazonaws.com\n\
range:bytes=0-9\n\
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\
@@ -351,6 +354,7 @@ mod tests {
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
);
+ #[rustfmt::skip]
let expected = "GET\n\
/test.txt\n\
\n\
@@ -373,11 +377,9 @@ mod tests {
#[test]
fn test_should_handle_presigned_url_query_string() {
- let query = "X-Amz-Algorithm=AWS4-HMAC-SHA256\
- &X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request\
- &X-Amz-Date=20130524T000000Z\
- &X-Amz-Expires=86400\
- &X-Amz-SignedHeaders=host";
+ let query = "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%\
+ 2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&\
+ X-Amz-Expires=86400&X-Amz-SignedHeaders=host";
let result = build_canonical_query_string(query);
// Should be sorted, raw values preserved
assert!(result.contains("X-Amz-Algorithm=AWS4-HMAC-SHA256"));
diff --git a/crates/ruststack-auth/src/credentials.rs b/crates/ruststack-auth/src/credentials.rs
index 23d37db..811f383 100644
--- a/crates/ruststack-auth/src/credentials.rs
+++ b/crates/ruststack-auth/src/credentials.rs
@@ -44,7 +44,8 @@ pub struct StaticCredentialProvider {
}
impl StaticCredentialProvider {
- /// Create a new `StaticCredentialProvider` from an iterable of (access_key_id, secret_key) pairs.
+ /// Create a new `StaticCredentialProvider` from an iterable of (access_key_id, secret_key)
+ /// pairs.
pub fn new(credentials: impl IntoIterator- ) -> Self {
Self {
credentials: credentials.into_iter().collect(),
diff --git a/crates/ruststack-auth/src/presigned.rs b/crates/ruststack-auth/src/presigned.rs
index f8cc2fe..924ab15 100644
--- a/crates/ruststack-auth/src/presigned.rs
+++ b/crates/ruststack-auth/src/presigned.rs
@@ -19,13 +19,15 @@ use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use tracing::debug;
-use crate::canonical::{
- build_canonical_headers, build_canonical_query_string, build_canonical_uri,
- build_signed_headers_string,
+use crate::{
+ canonical::{
+ build_canonical_headers, build_canonical_query_string, build_canonical_uri,
+ build_signed_headers_string,
+ },
+ credentials::CredentialProvider,
+ error::AuthError,
+ sigv4::{AuthResult, build_string_to_sign, compute_signature, derive_signing_key},
};
-use crate::credentials::CredentialProvider;
-use crate::error::AuthError;
-use crate::sigv4::{AuthResult, build_string_to_sign, compute_signature, derive_signing_key};
/// The payload hash value used for all presigned URL requests.
const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
@@ -166,6 +168,7 @@ pub fn verify_presigned(
let signed_headers_str = build_signed_headers_string(&signed_header_refs);
// For presigned URLs, the payload hash is always UNSIGNED-PAYLOAD.
+ #[rustfmt::skip]
let canonical_request = format!(
"{method}\n{canonical_uri}\n{canonical_query}\n{canonical_headers}\n\n{signed_headers_str}\n{UNSIGNED_PAYLOAD}"
);
@@ -292,12 +295,8 @@ mod tests {
#[test]
fn test_should_parse_presigned_params() {
- let query = "X-Amz-Algorithm=AWS4-HMAC-SHA256\
- &X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request\
- &X-Amz-Date=20130524T000000Z\
- &X-Amz-Expires=86400\
- &X-Amz-SignedHeaders=host\
- &X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404";
+ #[rustfmt::skip]
+ let query = "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404";
let parsed = parse_presigned_params(query).unwrap();
assert_eq!(parsed.algorithm, "AWS4-HMAC-SHA256");
@@ -316,11 +315,9 @@ mod tests {
#[test]
fn test_should_reject_missing_algorithm_param() {
- let query = "X-Amz-Credential=AKID%2F20130524%2Fus-east-1%2Fs3%2Faws4_request\
- &X-Amz-Date=20130524T000000Z\
- &X-Amz-Expires=86400\
- &X-Amz-SignedHeaders=host\
- &X-Amz-Signature=abc";
+ let query = "X-Amz-Credential=AKID%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&\
+ X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&\
+ X-Amz-Signature=abc";
let result = parse_presigned_params(query);
assert!(matches!(result, Err(AuthError::MissingQueryParam(_))));
@@ -343,12 +340,9 @@ mod tests {
#[test]
fn test_should_build_query_string_without_signature() {
- let query = "X-Amz-Algorithm=AWS4-HMAC-SHA256\
- &X-Amz-Credential=AKID%2F20130524%2Fus-east-1%2Fs3%2Faws4_request\
- &X-Amz-Date=20130524T000000Z\
- &X-Amz-Expires=86400\
- &X-Amz-SignedHeaders=host\
- &X-Amz-Signature=abc123";
+ let query = "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKID%2F20130524%\
+ 2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&\
+ X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=abc123";
let result = build_canonical_query_string_without_signature(query);
assert!(!result.contains("X-Amz-Signature"));
@@ -367,17 +361,11 @@ mod tests {
let signing_key = derive_signing_key(TEST_SECRET_KEY, "20130524", "us-east-1", "s3");
// Build canonical request for the presigned URL test vector.
- let canonical_request = "GET\n\
- /test.txt\n\
- X-Amz-Algorithm=AWS4-HMAC-SHA256\
- &X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request\
- &X-Amz-Date=20130524T000000Z\
- &X-Amz-Expires=86400\
- &X-Amz-SignedHeaders=host\n\
- host:examplebucket.s3.amazonaws.com\n\
- \n\
- host\n\
- UNSIGNED-PAYLOAD";
+ let canonical_request = "GET\n/test.txt\nX-Amz-Algorithm=AWS4-HMAC-SHA256&\
+ X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%\
+ 2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&\
+ X-Amz-Expires=86400&X-Amz-SignedHeaders=host\nhost:examplebucket.\
+ s3.amazonaws.com\n\nhost\nUNSIGNED-PAYLOAD";
let canonical_hash = hex::encode(Sha256::digest(canonical_request.as_bytes()));
assert_eq!(
@@ -411,16 +399,14 @@ mod tests {
// Build the canonical request components.
let canonical_uri = "/test.txt";
let query_without_sig = format!(
- "X-Amz-Algorithm=AWS4-HMAC-SHA256\
- &X-Amz-Credential={}\
- &X-Amz-Date={timestamp}\
- &X-Amz-Expires=86400\
- &X-Amz-SignedHeaders=host",
+ "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={}&X-Amz-Date={timestamp}&\
+ X-Amz-Expires=86400&X-Amz-SignedHeaders=host",
percent_encoding::utf8_percent_encode(&credential, percent_encoding::NON_ALPHANUMERIC)
);
let canonical_query = build_canonical_query_string(&query_without_sig);
+ #[rustfmt::skip]
let canonical_request = format!(
"GET\n{canonical_uri}\n{canonical_query}\nhost:examplebucket.s3.amazonaws.com\n\nhost\nUNSIGNED-PAYLOAD"
);
diff --git a/crates/ruststack-auth/src/sigv2.rs b/crates/ruststack-auth/src/sigv2.rs
index 7cd3874..5c3c2e7 100644
--- a/crates/ruststack-auth/src/sigv2.rs
+++ b/crates/ruststack-auth/src/sigv2.rs
@@ -20,16 +20,13 @@
use std::collections::BTreeMap;
-use base64::Engine;
-use base64::engine::general_purpose::STANDARD as BASE64;
+use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use hmac::{Hmac, Mac};
use sha1::Sha1;
use subtle::ConstantTimeEq;
use tracing::debug;
-use crate::credentials::CredentialProvider;
-use crate::error::AuthError;
-use crate::sigv4::AuthResult;
+use crate::{credentials::CredentialProvider, error::AuthError, sigv4::AuthResult};
type HmacSha1 = Hmac;
diff --git a/crates/ruststack-auth/src/sigv4.rs b/crates/ruststack-auth/src/sigv4.rs
index e699d6b..b94d0fd 100644
--- a/crates/ruststack-auth/src/sigv4.rs
+++ b/crates/ruststack-auth/src/sigv4.rs
@@ -2,13 +2,13 @@
//!
//! This module implements the core SigV4 signature verification flow:
//!
-//! 1. Parse the `Authorization` header to extract the algorithm, credential scope,
-//! signed headers, and provided signature.
+//! 1. Parse the `Authorization` header to extract the algorithm, credential scope, signed headers,
+//! and provided signature.
//! 2. Reconstruct the canonical request from the HTTP request parts.
//! 3. Build the string to sign from the timestamp, credential scope, and canonical request hash.
//! 4. Derive the signing key using HMAC-SHA256 from the secret key and credential scope components.
-//! 5. Compute the expected signature and compare it to the provided signature using
-//! constant-time comparison.
+//! 5. Compute the expected signature and compare it to the provided signature using constant-time
+//! comparison.
//!
//! The main entry point is [`verify_sigv4`].
@@ -17,9 +17,9 @@ use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use tracing::debug;
-use crate::canonical::build_canonical_request;
-use crate::credentials::CredentialProvider;
-use crate::error::AuthError;
+use crate::{
+ canonical::build_canonical_request, credentials::CredentialProvider, error::AuthError,
+};
/// The only algorithm supported by this implementation.
const SUPPORTED_ALGORITHM: &str = "AWS4-HMAC-SHA256";
@@ -72,7 +72,8 @@ pub struct ParsedAuth {
/// Returns [`AuthError::InvalidAuthHeader`] if the header format is invalid,
/// or [`AuthError::UnsupportedAlgorithm`] if the algorithm is not `AWS4-HMAC-SHA256`.
pub fn parse_authorization_header(header: &str) -> Result {
- // Split algorithm from the rest: "AWS4-HMAC-SHA256 Credential=...,SignedHeaders=...,Signature=..."
+ // Split algorithm from the rest: "AWS4-HMAC-SHA256
+ // Credential=...,SignedHeaders=...,Signature=..."
let (algorithm, rest) = header.split_once(' ').ok_or(AuthError::InvalidAuthHeader)?;
if algorithm != SUPPORTED_ALGORITHM {
@@ -361,8 +362,7 @@ pub fn hash_payload(payload: &[u8]) -> String {
#[cfg(test)]
mod tests {
use super::*;
- use crate::canonical::build_signed_headers_string;
- use crate::credentials::StaticCredentialProvider;
+ use crate::{canonical::build_signed_headers_string, credentials::StaticCredentialProvider};
const TEST_ACCESS_KEY: &str = "AKIAIOSFODNN7EXAMPLE";
const TEST_SECRET_KEY: &str = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
@@ -388,9 +388,9 @@ mod tests {
#[test]
fn test_should_parse_authorization_header() {
let header = "AWS4-HMAC-SHA256 \
- Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,\
- SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,\
- Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41";
+ Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,\
+ SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,\
+ Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41";
let parsed = parse_authorization_header(header).unwrap();
assert_eq!(parsed.algorithm, "AWS4-HMAC-SHA256");
@@ -410,16 +410,17 @@ mod tests {
#[test]
fn test_should_reject_unsupported_algorithm() {
- let header = "AWS4-HMAC-SHA512 Credential=AKID/20130524/us-east-1/s3/aws4_request,\
- SignedHeaders=host,Signature=abc";
+ let header = "AWS4-HMAC-SHA512 \
+ Credential=AKID/20130524/us-east-1/s3/aws4_request,SignedHeaders=host,\
+ Signature=abc";
let result = parse_authorization_header(header);
assert!(matches!(result, Err(AuthError::UnsupportedAlgorithm(_))));
}
#[test]
fn test_should_reject_invalid_credential_format() {
- let header = "AWS4-HMAC-SHA256 Credential=AKID/20130524/us-east-1,\
- SignedHeaders=host,Signature=abc";
+ let header =
+ "AWS4-HMAC-SHA256 Credential=AKID/20130524/us-east-1,SignedHeaders=host,Signature=abc";
let result = parse_authorization_header(header);
assert!(matches!(result, Err(AuthError::InvalidCredential)));
}
@@ -432,6 +433,7 @@ mod tests {
"20130524/us-east-1/s3/aws4_request",
canonical_hash,
);
+ #[rustfmt::skip]
let expected = "AWS4-HMAC-SHA256\n\
20130524T000000Z\n\
20130524/us-east-1/s3/aws4_request\n\
@@ -444,6 +446,7 @@ mod tests {
// Full end-to-end test using the AWS GET Object example.
let signing_key = derive_signing_key(TEST_SECRET_KEY, TEST_DATE, TEST_REGION, TEST_SERVICE);
+ #[rustfmt::skip]
let string_to_sign = "AWS4-HMAC-SHA256\n\
20130524T000000Z\n\
20130524/us-east-1/s3/aws4_request\n\
@@ -472,8 +475,9 @@ mod tests {
// Compute the expected signature to build the auth header.
let auth_value = format!(
- "AWS4-HMAC-SHA256 Credential={TEST_ACCESS_KEY}/20130524/us-east-1/s3/aws4_request,\
- SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,\
+ "AWS4-HMAC-SHA256 \
+ Credential={TEST_ACCESS_KEY}/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;\
+ range;x-amz-content-sha256;x-amz-date,\
Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41"
);
builder = builder.header(http::header::AUTHORIZATION, &auth_value);
@@ -497,8 +501,9 @@ mod tests {
let empty_hash = hash_payload(b"");
let auth_value = format!(
- "AWS4-HMAC-SHA256 Credential={TEST_ACCESS_KEY}/20130524/us-east-1/s3/aws4_request,\
- SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,\
+ "AWS4-HMAC-SHA256 \
+ Credential={TEST_ACCESS_KEY}/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;\
+ range;x-amz-content-sha256;x-amz-date,\
Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41"
);
@@ -540,11 +545,10 @@ mod tests {
let provider = StaticCredentialProvider::new(vec![]);
let empty_hash = hash_payload(b"");
- let auth_value =
- "AWS4-HMAC-SHA256 Credential=UNKNOWN_KEY/20130524/us-east-1/s3/aws4_request,\
- SignedHeaders=host;x-amz-date,\
- Signature=abc123"
- .to_owned();
+ let auth_value = "AWS4-HMAC-SHA256 \
+ Credential=UNKNOWN_KEY/20130524/us-east-1/s3/aws4_request,\
+ SignedHeaders=host;x-amz-date,Signature=abc123"
+ .to_owned();
let (parts, _body) = http::Request::builder()
.method("GET")
diff --git a/crates/ruststack-cloudwatch-core/src/alarm_store.rs b/crates/ruststack-cloudwatch-core/src/alarm_store.rs
index 1e78566..b62e5f5 100644
--- a/crates/ruststack-cloudwatch-core/src/alarm_store.rs
+++ b/crates/ruststack-cloudwatch-core/src/alarm_store.rs
@@ -225,10 +225,10 @@ impl Default for AlarmStore {
#[cfg(test)]
mod tests {
- use super::*;
-
use ruststack_cloudwatch_model::types::StateValue;
+ use super::*;
+
fn make_alarm(name: &str) -> MetricAlarm {
MetricAlarm {
alarm_name: Some(name.to_owned()),
diff --git a/crates/ruststack-cloudwatch-core/src/handler.rs b/crates/ruststack-cloudwatch-core/src/handler.rs
index 2c1e14d..09ac8ec 100644
--- a/crates/ruststack-cloudwatch-core/src/handler.rs
+++ b/crates/ruststack-cloudwatch-core/src/handler.rs
@@ -7,42 +7,43 @@
//! Covers all 31 CloudWatch operations: metrics, alarms, dashboards,
//! insight rules, anomaly detectors, metric streams, and tagging.
-use std::future::Future;
-use std::pin::Pin;
-use std::sync::Arc;
+use std::{future::Future, pin::Pin, sync::Arc};
use bytes::Bytes;
-use serde::Serialize;
-
-use ruststack_cloudwatch_http::body::CloudWatchResponseBody;
-use ruststack_cloudwatch_http::dispatch::{CloudWatchHandler, Protocol};
-use ruststack_cloudwatch_http::request::{
- get_optional_bool, get_optional_f64, get_optional_i32, get_optional_param, get_required_param,
- parse_dimension_filters, parse_dimensions, parse_form_params, parse_string_list,
- parse_struct_list, parse_tag_list,
+use ruststack_cloudwatch_http::{
+ body::CloudWatchResponseBody,
+ dispatch::{CloudWatchHandler, Protocol},
+ request::{
+ get_optional_bool, get_optional_f64, get_optional_i32, get_optional_param,
+ get_required_param, parse_dimension_filters, parse_dimensions, parse_form_params,
+ parse_string_list, parse_struct_list, parse_tag_list,
+ },
+ response::{XmlWriter, cbor_response, xml_response},
};
-use ruststack_cloudwatch_http::response::{XmlWriter, cbor_response, xml_response};
-use ruststack_cloudwatch_model::error::{CloudWatchError, CloudWatchErrorCode};
-use ruststack_cloudwatch_model::input::{
- DeleteAlarmsInput, DeleteAnomalyDetectorInput, DeleteDashboardsInput, DeleteInsightRulesInput,
- DeleteMetricStreamInput, DescribeAlarmHistoryInput, DescribeAlarmsForMetricInput,
- DescribeAlarmsInput, DescribeAnomalyDetectorsInput, DescribeInsightRulesInput,
- DisableAlarmActionsInput, EnableAlarmActionsInput, GetDashboardInput, GetMetricDataInput,
- GetMetricStatisticsInput, GetMetricStreamInput, ListDashboardsInput, ListMetricStreamsInput,
- ListMetricsInput, ListTagsForResourceInput, PutAnomalyDetectorInput, PutCompositeAlarmInput,
- PutDashboardInput, PutInsightRuleInput, PutManagedInsightRulesInput, PutMetricAlarmInput,
- PutMetricDataInput, PutMetricStreamInput, SetAlarmStateInput, TagResourceInput,
- UntagResourceInput,
-};
-use ruststack_cloudwatch_model::operations::CloudWatchOperation;
-use ruststack_cloudwatch_model::types::{
- AlarmType, AnomalyDetectorType, ComparisonOperator, CompositeAlarm, Dimension, DimensionFilter,
- HistoryItemType, LabelOptions, ManagedRule, MetricAlarm, MetricCharacteristics,
- MetricDataQuery, MetricDatum, MetricMathAnomalyDetector, MetricStat, MetricStreamFilter,
- MetricStreamOutputFormat, MetricStreamStatisticsConfiguration, MetricStreamStatisticsMetric,
- RecentlyActive, ScanBy, SingleMetricAnomalyDetector, StandardUnit, StateValue, Statistic,
- StatisticSet, Tag,
+use ruststack_cloudwatch_model::{
+ error::{CloudWatchError, CloudWatchErrorCode},
+ input::{
+ DeleteAlarmsInput, DeleteAnomalyDetectorInput, DeleteDashboardsInput,
+ DeleteInsightRulesInput, DeleteMetricStreamInput, DescribeAlarmHistoryInput,
+ DescribeAlarmsForMetricInput, DescribeAlarmsInput, DescribeAnomalyDetectorsInput,
+ DescribeInsightRulesInput, DisableAlarmActionsInput, EnableAlarmActionsInput,
+ GetDashboardInput, GetMetricDataInput, GetMetricStatisticsInput, GetMetricStreamInput,
+ ListDashboardsInput, ListMetricStreamsInput, ListMetricsInput, ListTagsForResourceInput,
+ PutAnomalyDetectorInput, PutCompositeAlarmInput, PutDashboardInput, PutInsightRuleInput,
+ PutManagedInsightRulesInput, PutMetricAlarmInput, PutMetricDataInput, PutMetricStreamInput,
+ SetAlarmStateInput, TagResourceInput, UntagResourceInput,
+ },
+ operations::CloudWatchOperation,
+ types::{
+ AlarmType, AnomalyDetectorType, ComparisonOperator, CompositeAlarm, Dimension,
+ DimensionFilter, HistoryItemType, LabelOptions, ManagedRule, MetricAlarm,
+ MetricCharacteristics, MetricDataQuery, MetricDatum, MetricMathAnomalyDetector, MetricStat,
+ MetricStreamFilter, MetricStreamOutputFormat, MetricStreamStatisticsConfiguration,
+ MetricStreamStatisticsMetric, RecentlyActive, ScanBy, SingleMetricAnomalyDetector,
+ StandardUnit, StateValue, Statistic, StatisticSet, Tag,
+ },
};
+use serde::Serialize;
use crate::provider::RustStackCloudWatch;
diff --git a/crates/ruststack-cloudwatch-core/src/provider.rs b/crates/ruststack-cloudwatch-core/src/provider.rs
index 6e6f5b6..ed4baf3 100644
--- a/crates/ruststack-cloudwatch-core/src/provider.rs
+++ b/crates/ruststack-cloudwatch-core/src/provider.rs
@@ -4,51 +4,54 @@
//! alarm management, dashboard CRUD, insight rules, anomaly detectors,
//! metric streams, and tagging.
-use std::collections::HashMap;
-use std::sync::Arc;
+use std::{collections::HashMap, sync::Arc};
use chrono::Utc;
-use tracing::instrument;
-
-use ruststack_cloudwatch_model::error::{CloudWatchError, CloudWatchErrorCode};
-use ruststack_cloudwatch_model::input::{
- DeleteAlarmsInput, DeleteAnomalyDetectorInput, DeleteDashboardsInput, DeleteInsightRulesInput,
- DeleteMetricStreamInput, DescribeAlarmHistoryInput, DescribeAlarmsForMetricInput,
- DescribeAlarmsInput, DescribeAnomalyDetectorsInput, DescribeInsightRulesInput,
- DisableAlarmActionsInput, EnableAlarmActionsInput, GetDashboardInput, GetMetricDataInput,
- GetMetricStatisticsInput, GetMetricStreamInput, ListDashboardsInput, ListMetricStreamsInput,
- ListMetricsInput, ListTagsForResourceInput, PutAnomalyDetectorInput, PutCompositeAlarmInput,
- PutDashboardInput, PutInsightRuleInput, PutManagedInsightRulesInput, PutMetricAlarmInput,
- PutMetricDataInput, PutMetricStreamInput, SetAlarmStateInput, TagResourceInput,
- UntagResourceInput,
-};
-use ruststack_cloudwatch_model::output::{
- DeleteAnomalyDetectorOutput, DeleteDashboardsOutput, DeleteInsightRulesOutput,
- DeleteMetricStreamOutput, DescribeAlarmHistoryOutput, DescribeAlarmsForMetricOutput,
- DescribeAlarmsOutput, DescribeAnomalyDetectorsOutput, DescribeInsightRulesOutput,
- GetDashboardOutput, GetMetricDataOutput, GetMetricStatisticsOutput, GetMetricStreamOutput,
- ListDashboardsOutput, ListMetricStreamsOutput, ListMetricsOutput, ListTagsForResourceOutput,
- PutAnomalyDetectorOutput, PutDashboardOutput, PutInsightRuleOutput,
- PutManagedInsightRulesOutput, PutMetricStreamOutput, TagResourceOutput, UntagResourceOutput,
-};
-use ruststack_cloudwatch_model::types::{
- AlarmHistoryItem, AlarmType, AnomalyDetector, CompositeAlarm, DashboardEntry, Datapoint,
- HistoryItemType, InsightRule, Metric, MetricAlarm, MetricDataResult, MetricStreamEntry,
- MetricStreamOutputFormat, StateValue, Statistic, StatusCode,
+use ruststack_cloudwatch_model::{
+ error::{CloudWatchError, CloudWatchErrorCode},
+ input::{
+ DeleteAlarmsInput, DeleteAnomalyDetectorInput, DeleteDashboardsInput,
+ DeleteInsightRulesInput, DeleteMetricStreamInput, DescribeAlarmHistoryInput,
+ DescribeAlarmsForMetricInput, DescribeAlarmsInput, DescribeAnomalyDetectorsInput,
+ DescribeInsightRulesInput, DisableAlarmActionsInput, EnableAlarmActionsInput,
+ GetDashboardInput, GetMetricDataInput, GetMetricStatisticsInput, GetMetricStreamInput,
+ ListDashboardsInput, ListMetricStreamsInput, ListMetricsInput, ListTagsForResourceInput,
+ PutAnomalyDetectorInput, PutCompositeAlarmInput, PutDashboardInput, PutInsightRuleInput,
+ PutManagedInsightRulesInput, PutMetricAlarmInput, PutMetricDataInput, PutMetricStreamInput,
+ SetAlarmStateInput, TagResourceInput, UntagResourceInput,
+ },
+ output::{
+ DeleteAnomalyDetectorOutput, DeleteDashboardsOutput, DeleteInsightRulesOutput,
+ DeleteMetricStreamOutput, DescribeAlarmHistoryOutput, DescribeAlarmsForMetricOutput,
+ DescribeAlarmsOutput, DescribeAnomalyDetectorsOutput, DescribeInsightRulesOutput,
+ GetDashboardOutput, GetMetricDataOutput, GetMetricStatisticsOutput, GetMetricStreamOutput,
+ ListDashboardsOutput, ListMetricStreamsOutput, ListMetricsOutput,
+ ListTagsForResourceOutput, PutAnomalyDetectorOutput, PutDashboardOutput,
+ PutInsightRuleOutput, PutManagedInsightRulesOutput, PutMetricStreamOutput,
+ TagResourceOutput, UntagResourceOutput,
+ },
+ types::{
+ AlarmHistoryItem, AlarmType, AnomalyDetector, CompositeAlarm, DashboardEntry, Datapoint,
+ HistoryItemType, InsightRule, Metric, MetricAlarm, MetricDataResult, MetricStreamEntry,
+ MetricStreamOutputFormat, StateValue, Statistic, StatusCode,
+ },
};
+use tracing::instrument;
-use crate::aggregation::aggregate_statistics;
-use crate::alarm_store::AlarmStore;
-use crate::anomaly_store::AnomalyStore;
-use crate::config::CloudWatchConfig;
-use crate::dashboard_store::{DashboardRecord, DashboardStore};
-use crate::dimensions::{dimensions_match, normalize_dimensions};
-use crate::insight_store::InsightStore;
-use crate::metric_store::{DataPoint, MetricKey, MetricStore, StatisticSet};
-use crate::metric_stream_store::{MetricStreamRecord, MetricStreamStore};
-use crate::validation::{
- validate_alarm_name, validate_dashboard_body, validate_dashboard_name, validate_dimensions,
- validate_metric_name, validate_namespace,
+use crate::{
+ aggregation::aggregate_statistics,
+ alarm_store::AlarmStore,
+ anomaly_store::AnomalyStore,
+ config::CloudWatchConfig,
+ dashboard_store::{DashboardRecord, DashboardStore},
+ dimensions::{dimensions_match, normalize_dimensions},
+ insight_store::InsightStore,
+ metric_store::{DataPoint, MetricKey, MetricStore, StatisticSet},
+ metric_stream_store::{MetricStreamRecord, MetricStreamStore},
+ validation::{
+ validate_alarm_name, validate_dashboard_body, validate_dashboard_name, validate_dimensions,
+ validate_metric_name, validate_namespace,
+ },
};
/// CloudWatch service provider implementing all 31 operations.
@@ -1254,12 +1257,15 @@ impl RustStackCloudWatch {
#[cfg(test)]
mod tests {
- use super::*;
- use ruststack_cloudwatch_model::input::{
- DeleteAlarmsInput, DescribeAlarmsInput, PutMetricAlarmInput, PutMetricDataInput,
- SetAlarmStateInput,
+ use ruststack_cloudwatch_model::{
+ input::{
+ DeleteAlarmsInput, DescribeAlarmsInput, PutMetricAlarmInput, PutMetricDataInput,
+ SetAlarmStateInput,
+ },
+ types::{ComparisonOperator, MetricDatum, StateValue},
};
- use ruststack_cloudwatch_model::types::{ComparisonOperator, MetricDatum, StateValue};
+
+ use super::*;
fn make_provider() -> RustStackCloudWatch {
RustStackCloudWatch::new(Arc::new(CloudWatchConfig::default()))
diff --git a/crates/ruststack-cloudwatch-core/src/validation.rs b/crates/ruststack-cloudwatch-core/src/validation.rs
index 18a5a3f..bca8b3b 100644
--- a/crates/ruststack-cloudwatch-core/src/validation.rs
+++ b/crates/ruststack-cloudwatch-core/src/validation.rs
@@ -1,7 +1,9 @@
//! Input validation for CloudWatch operations.
-use ruststack_cloudwatch_model::error::{CloudWatchError, CloudWatchErrorCode};
-use ruststack_cloudwatch_model::types::Dimension;
+use ruststack_cloudwatch_model::{
+ error::{CloudWatchError, CloudWatchErrorCode},
+ types::Dimension,
+};
/// Validate a namespace string.
pub fn validate_namespace(namespace: &str) -> Result<(), CloudWatchError> {
diff --git a/crates/ruststack-cloudwatch-http/src/body.rs b/crates/ruststack-cloudwatch-http/src/body.rs
index ca145f5..79130d5 100644
--- a/crates/ruststack-cloudwatch-http/src/body.rs
+++ b/crates/ruststack-cloudwatch-http/src/body.rs
@@ -1,7 +1,9 @@
//! CloudWatch HTTP response body type.
-use std::pin::Pin;
-use std::task::{Context, Poll};
+use std::{
+ pin::Pin,
+ task::{Context, Poll},
+};
use bytes::Bytes;
use http_body_util::Full;
diff --git a/crates/ruststack-cloudwatch-http/src/dispatch.rs b/crates/ruststack-cloudwatch-http/src/dispatch.rs
index 6145e86..f9e46be 100644
--- a/crates/ruststack-cloudwatch-http/src/dispatch.rs
+++ b/crates/ruststack-cloudwatch-http/src/dispatch.rs
@@ -1,12 +1,9 @@
//! CloudWatch handler trait and operation dispatch.
-use std::future::Future;
-use std::pin::Pin;
+use std::{future::Future, pin::Pin};
use bytes::Bytes;
-
-use ruststack_cloudwatch_model::error::CloudWatchError;
-use ruststack_cloudwatch_model::operations::CloudWatchOperation;
+use ruststack_cloudwatch_model::{error::CloudWatchError, operations::CloudWatchOperation};
use crate::body::CloudWatchResponseBody;
diff --git a/crates/ruststack-cloudwatch-http/src/lib.rs b/crates/ruststack-cloudwatch-http/src/lib.rs
index 5342e3c..91a38dd 100644
--- a/crates/ruststack-cloudwatch-http/src/lib.rs
+++ b/crates/ruststack-cloudwatch-http/src/lib.rs
@@ -1,12 +1,12 @@
//! CloudWatch Metrics HTTP service layer for RustStack.
//!
//! Supports two wire protocols:
-//! - **awsQuery**: form-urlencoded requests, XML responses (legacy SDKs).
-//! Requests are `POST /` with `Content-Type: application/x-www-form-urlencoded`
-//! and the operation is dispatched via the `Action=` form parameter.
-//! - **rpcv2Cbor**: CBOR requests/responses (AWS SDK v1.108+).
-//! Requests are `POST /service/GraniteServiceVersion20100801/operation/{Op}`
-//! with `Content-Type: application/cbor` and `smithy-protocol: rpc-v2-cbor`.
+//! - **awsQuery**: form-urlencoded requests, XML responses (legacy SDKs). Requests are `POST /`
+//! with `Content-Type: application/x-www-form-urlencoded` and the operation is dispatched via the
+//! `Action=` form parameter.
+//! - **rpcv2Cbor**: CBOR requests/responses (AWS SDK v1.108+). Requests are `POST
+//! /service/GraniteServiceVersion20100801/operation/{Op}` with `Content-Type: application/cbor`
+//! and `smithy-protocol: rpc-v2-cbor`.
pub mod body;
pub mod dispatch;
diff --git a/crates/ruststack-cloudwatch-http/src/response.rs b/crates/ruststack-cloudwatch-http/src/response.rs
index 5c503d5..c46d7aa 100644
--- a/crates/ruststack-cloudwatch-http/src/response.rs
+++ b/crates/ruststack-cloudwatch-http/src/response.rs
@@ -50,14 +50,9 @@ pub fn error_to_xml(error: &CloudWatchError, request_id: &str) -> String {
"Sender"
};
format!(
- "\
- \
- {fault}\
-
{}\
- {}\
- \
- {}\
- ",
+ "{fault}{}{}\
+ Error>{}",
xml_escape(&error.code.to_string()),
xml_escape(&error.message),
xml_escape(request_id),
diff --git a/crates/ruststack-cloudwatch-http/src/router.rs b/crates/ruststack-cloudwatch-http/src/router.rs
index a6a49df..09b06cb 100644
--- a/crates/ruststack-cloudwatch-http/src/router.rs
+++ b/crates/ruststack-cloudwatch-http/src/router.rs
@@ -4,8 +4,7 @@
//! `Content-Type: application/x-www-form-urlencoded`. The operation is
//! specified by the `Action=` form parameter.
-use ruststack_cloudwatch_model::error::CloudWatchError;
-use ruststack_cloudwatch_model::operations::CloudWatchOperation;
+use ruststack_cloudwatch_model::{error::CloudWatchError, operations::CloudWatchOperation};
/// Resolve a CloudWatch operation from parsed form parameters.
///
diff --git a/crates/ruststack-cloudwatch-http/src/service.rs b/crates/ruststack-cloudwatch-http/src/service.rs
index 0814292..a117b8a 100644
--- a/crates/ruststack-cloudwatch-http/src/service.rs
+++ b/crates/ruststack-cloudwatch-http/src/service.rs
@@ -6,23 +6,20 @@
//!
//! The protocol is detected automatically from request headers and URL path.
-use std::convert::Infallible;
-use std::future::Future;
-use std::pin::Pin;
-use std::sync::Arc;
+use std::{convert::Infallible, future::Future, pin::Pin, sync::Arc};
use bytes::Bytes;
use http_body_util::BodyExt;
use hyper::body::Incoming;
+use ruststack_cloudwatch_model::{error::CloudWatchError, operations::CloudWatchOperation};
-use ruststack_cloudwatch_model::error::CloudWatchError;
-use ruststack_cloudwatch_model::operations::CloudWatchOperation;
-
-use crate::body::CloudWatchResponseBody;
-use crate::dispatch::{CloudWatchHandler, Protocol, dispatch_operation};
-use crate::request::parse_form_params;
-use crate::response::{CONTENT_TYPE, cbor_error_response, error_to_response};
-use crate::router::resolve_operation;
+use crate::{
+ body::CloudWatchResponseBody,
+ dispatch::{CloudWatchHandler, Protocol, dispatch_operation},
+ request::parse_form_params,
+ response::{CONTENT_TYPE, cbor_error_response, error_to_response},
+ router::resolve_operation,
+};
/// Configuration for the CloudWatch HTTP service.
#[derive(Clone)]
diff --git a/crates/ruststack-dynamodb-core/src/expression/ast.rs b/crates/ruststack-dynamodb-core/src/expression/ast.rs
index cfdd12b..a8a8fdf 100644
--- a/crates/ruststack-dynamodb-core/src/expression/ast.rs
+++ b/crates/ruststack-dynamodb-core/src/expression/ast.rs
@@ -8,8 +8,7 @@
//! name and value references from parsed ASTs, used for validating that all
//! provided names/values are actually used in expressions.
-use std::collections::HashSet;
-use std::fmt;
+use std::{collections::HashSet, fmt};
/// Expression AST node for condition, filter, and key-condition expressions.
#[derive(Debug, Clone)]
diff --git a/crates/ruststack-dynamodb-core/src/expression/evaluator.rs b/crates/ruststack-dynamodb-core/src/expression/evaluator.rs
index a41572e..a1bea5a 100644
--- a/crates/ruststack-dynamodb-core/src/expression/evaluator.rs
+++ b/crates/ruststack-dynamodb-core/src/expression/evaluator.rs
@@ -8,11 +8,13 @@ use std::collections::HashMap;
use ruststack_dynamodb_model::AttributeValue;
-use super::ast::{
- AddAction, AttributePath, CompareOp, DeleteAction, Expr, FunctionName, LogicalOp, Operand,
- PathElement, SetAction, SetValue, UpdateExpr,
+use super::{
+ ast::{
+ AddAction, AttributePath, CompareOp, DeleteAction, Expr, FunctionName, LogicalOp, Operand,
+ PathElement, SetAction, SetValue, UpdateExpr,
+ },
+ parser::ExpressionError,
};
-use super::parser::ExpressionError;
// ---------------------------------------------------------------------------
// Evaluation context
@@ -105,16 +107,16 @@ impl EvalContext<'_> {
if is_query_constant(low) && is_query_constant(high) {
if std::mem::discriminant(lo) != std::mem::discriminant(hi) {
return Err(ExpressionError::TypeMismatch {
- message: "BETWEEN bounds must have the same type when both \
- are expression attribute values"
+ message: "BETWEEN bounds must have the same type when both are expression \
+ attribute values"
.to_owned(),
});
}
// Check ordering: if low > high, it is a ValidationException.
if compare_values(lo, hi, CompareOp::Gt)? {
return Err(ExpressionError::TypeMismatch {
- message: "BETWEEN bounds are in wrong order; \
- low bound must be less than or equal to high bound"
+ message: "BETWEEN bounds are in wrong order; low bound must be less than or \
+ equal to high bound"
.to_owned(),
});
}
@@ -196,8 +198,8 @@ impl EvalContext<'_> {
if !is_valid_type_descriptor(&expected_type) {
return Err(ExpressionError::TypeMismatch {
message: format!(
- "Invalid type: {expected_type}. \
- Valid types: S, SS, N, NS, B, BS, BOOL, NULL, L, M"
+ "Invalid type: {expected_type}. Valid types: S, SS, N, NS, B, BS, \
+ BOOL, NULL, L, M"
),
});
}
@@ -224,8 +226,8 @@ impl EvalContext<'_> {
return Err(ExpressionError::InvalidOperand {
operation: "begins_with".to_owned(),
message: format!(
- "Incorrect operand type for operator or function; \
- operator or function: begins_with, operand type: {td}",
+ "Incorrect operand type for operator or function; operator or \
+ function: begins_with, operand type: {td}",
td = v.type_descriptor()
),
});
@@ -504,8 +506,8 @@ impl EvalContext<'_> {
return Err(ExpressionError::InvalidOperand {
operation: "ADD".to_owned(),
message: format!(
- "Incorrect operand type for operator or function; \
- operator: ADD, operand type: {operand_type}"
+ "Incorrect operand type for operator or function; operator: ADD, operand \
+ type: {operand_type}"
),
});
}
@@ -539,8 +541,8 @@ impl EvalContext<'_> {
return Err(ExpressionError::InvalidOperand {
operation: "DELETE".to_owned(),
message: format!(
- "Incorrect operand type for operator or function; \
- operator: DELETE, operand type: {operand_type}"
+ "Incorrect operand type for operator or function; operator: DELETE, operand \
+ type: {operand_type}"
),
});
}
@@ -595,8 +597,8 @@ impl EvalContext<'_> {
return Err(ExpressionError::InvalidOperand {
operation: "DELETE".to_owned(),
message: format!(
- "Type mismatch for DELETE; operator type: {del_type}, \
- existing type: {existing_type}"
+ "Type mismatch for DELETE; operator type: {del_type}, existing type: \
+ {existing_type}"
),
});
}
@@ -681,8 +683,8 @@ fn validate_ordering_operand_type(
return Err(ExpressionError::InvalidOperand {
operation: "operator".to_owned(),
message: format!(
- "Incorrect operand type for operator or function; \
- operator: {operator_name}, operand type: {type_desc}",
+ "Incorrect operand type for operator or function; operator: {operator_name}, \
+ operand type: {type_desc}",
type_desc = resolved_value.type_descriptor()
),
});
@@ -973,7 +975,9 @@ fn precise_arithmetic(a: &str, b: &str, is_add: bool) -> Result 38 + 2 {
// Precision would be lost in the result.
return Err(ExpressionError::Validation {
- message: "Number overflow. Attempting to store a number with magnitude larger than supported range".to_owned(),
+ message: "Number overflow. Attempting to store a number with magnitude larger than \
+ supported range"
+ .to_owned(),
});
}
@@ -1012,7 +1016,9 @@ fn precise_arithmetic(a: &str, b: &str, is_add: bool) -> Result 38 {
return Err(ExpressionError::Validation {
- message: "Number overflow. Attempting to store a number with magnitude larger than supported range".to_owned(),
+ message: "Number overflow. Attempting to store a number with magnitude larger than \
+ supported range"
+ .to_owned(),
});
}
@@ -1020,12 +1026,16 @@ fn precise_arithmetic(a: &str, b: &str, is_add: bool) -> Result 125 {
return Err(ExpressionError::Validation {
- message: "Number overflow. Attempting to store a number with magnitude larger than supported range".to_owned(),
+ message: "Number overflow. Attempting to store a number with magnitude larger than \
+ supported range"
+ .to_owned(),
});
}
if magnitude < -130 {
return Err(ExpressionError::Validation {
- message: "Number underflow. Attempting to store a number with magnitude smaller than supported range".to_owned(),
+ message: "Number underflow. Attempting to store a number with magnitude smaller than \
+ supported range"
+ .to_owned(),
});
}
@@ -1366,8 +1376,7 @@ fn validate_nested_path_for_set(
// because DynamoDB requires existing intermediate containers.
return Err(ExpressionError::InvalidOperand {
operation: "SET".to_owned(),
- message: "The document path provided in the update expression \
- is invalid for update"
+ message: "The document path provided in the update expression is invalid for update"
.to_owned(),
});
};
@@ -1399,8 +1408,8 @@ fn validate_path_type(
}
_ => Err(ExpressionError::InvalidOperand {
operation: "SET".to_owned(),
- message: "The document path provided in the update expression \
- is invalid for update"
+ message: "The document path provided in the update expression is invalid for \
+ update"
.to_owned(),
}),
}
@@ -1416,8 +1425,8 @@ fn validate_path_type(
}
_ => Err(ExpressionError::InvalidOperand {
operation: "SET".to_owned(),
- message: "The document path provided in the update expression \
- is invalid for update"
+ message: "The document path provided in the update expression is invalid for \
+ update"
.to_owned(),
}),
},
@@ -1514,8 +1523,7 @@ fn apply_remove(
// Path root doesn't exist - this is a validation error for nested paths.
return Err(ExpressionError::InvalidOperand {
operation: "REMOVE".to_owned(),
- message: "The document path provided in the update expression \
- is invalid for update"
+ message: "The document path provided in the update expression is invalid for update"
.to_owned(),
});
}
@@ -1662,8 +1670,8 @@ fn compute_add_result(
Err(ExpressionError::InvalidOperand {
operation: "ADD".to_owned(),
message: format!(
- "Type mismatch for ADD; operator type: {add_type}, \
- existing type: {existing_type}"
+ "Type mismatch for ADD; operator type: {add_type}, existing type: \
+ {existing_type}"
),
})
}
diff --git a/crates/ruststack-dynamodb-core/src/expression/parser.rs b/crates/ruststack-dynamodb-core/src/expression/parser.rs
index cc98932..edd49cc 100644
--- a/crates/ruststack-dynamodb-core/src/expression/parser.rs
+++ b/crates/ruststack-dynamodb-core/src/expression/parser.rs
@@ -4,9 +4,7 @@
//! projection expressions. Keywords and function names are matched
//! case-insensitively per DynamoDB specification.
-use std::fmt;
-use std::iter::Peekable;
-use std::str::Chars;
+use std::{fmt, iter::Peekable, str::Chars};
use super::ast::{
AddAction, AttributePath, CompareOp, DeleteAction, Expr, FunctionName, LogicalOp, Operand,
@@ -565,8 +563,8 @@ impl Parser {
return Err(ExpressionError::InvalidOperand {
operation: name.to_owned(),
message: format!(
- "The function is not allowed to be used this way in an expression; \
- function: {name}"
+ "The function is not allowed to be used this way in an expression; function: \
+ {name}"
),
});
}
@@ -662,16 +660,15 @@ impl Parser {
expected: "valid function name".to_owned(),
found: format!(
"'{func_name}' is not a valid function; valid functions are: \
- attribute_exists, attribute_not_exists, attribute_type, begins_with, \
- contains, size"
+ attribute_exists, attribute_not_exists, attribute_type, begins_with, \
+ contains, size"
),
})
}
_ => Err(ExpressionError::UnexpectedToken {
expected: "comparison operator, BETWEEN, or IN after operand".to_owned(),
found: format!(
- "Syntax error; a standalone value or path is not a valid condition; \
- found: {}",
+ "Syntax error; a standalone value or path is not a valid condition; found: {}",
self.peek()
),
}),
@@ -726,8 +723,8 @@ impl Parser {
return Err(ExpressionError::InvalidOperand {
operation: "size".to_owned(),
message: format!(
- "Incorrect number of operands for operator or function; \
- operator or function: size, number of operands: {count}"
+ "Incorrect number of operands for operator or function; operator or \
+ function: size, number of operands: {count}"
),
});
}
@@ -830,9 +827,9 @@ impl Parser {
if seen_set {
return Err(ExpressionError::InvalidOperand {
operation: "UpdateExpression".to_owned(),
- message:
- "The \"SET\" section can only be used once in an update expression"
- .to_owned(),
+ message: "The \"SET\" section can only be used once in an update \
+ expression"
+ .to_owned(),
});
}
seen_set = true;
@@ -843,7 +840,9 @@ impl Parser {
if seen_remove {
return Err(ExpressionError::InvalidOperand {
operation: "UpdateExpression".to_owned(),
- message: "The \"REMOVE\" section can only be used once in an update expression".to_owned(),
+ message: "The \"REMOVE\" section can only be used once in an update \
+ expression"
+ .to_owned(),
});
}
seen_remove = true;
@@ -854,9 +853,9 @@ impl Parser {
if seen_add {
return Err(ExpressionError::InvalidOperand {
operation: "UpdateExpression".to_owned(),
- message:
- "The \"ADD\" section can only be used once in an update expression"
- .to_owned(),
+ message: "The \"ADD\" section can only be used once in an update \
+ expression"
+ .to_owned(),
});
}
seen_add = true;
@@ -867,7 +866,9 @@ impl Parser {
if seen_delete {
return Err(ExpressionError::InvalidOperand {
operation: "UpdateExpression".to_owned(),
- message: "The \"DELETE\" section can only be used once in an update expression".to_owned(),
+ message: "The \"DELETE\" section can only be used once in an update \
+ expression"
+ .to_owned(),
});
}
seen_delete = true;
@@ -1150,8 +1151,8 @@ pub fn parse_projection(input: &str) -> Result, ExpressionErr
if path.elements.len() > 32 {
return Err(ExpressionError::Validation {
message: format!(
- "Invalid ProjectionExpression: The document path has too many nesting \
- levels; nesting levels: {}",
+ "Invalid ProjectionExpression: The document path has too many nesting levels; \
+ nesting levels: {}",
path.elements.len()
),
});
@@ -1166,9 +1167,9 @@ pub fn parse_projection(input: &str) -> Result, ExpressionErr
if seen.contains(&repr) {
return Err(ExpressionError::Validation {
message: format!(
- "Invalid ProjectionExpression: Two document paths overlap with \
- each other; must remove or rewrite one of these paths; path one: \
- [{repr}], path two: [{repr}]"
+ "Invalid ProjectionExpression: Two document paths overlap with each \
+ other; must remove or rewrite one of these paths; path one: [{repr}], \
+ path two: [{repr}]"
),
});
}
@@ -1205,9 +1206,8 @@ fn path_to_resolved(path: &AttributePath) -> Vec {
/// Validate that no two projection paths overlap or conflict.
///
/// - **Overlap**: One path is a prefix of another, or two paths are identical.
-/// - **Conflict**: At a shared prefix point, one path accesses via dot (map key)
-/// and the other via index (list), meaning the same node would need to be both
-/// a map and a list simultaneously.
+/// - **Conflict**: At a shared prefix point, one path accesses via dot (map key) and the other via
+/// index (list), meaning the same node would need to be both a map and a list simultaneously.
fn validate_projection_paths(paths: &[AttributePath]) -> Result<(), ExpressionError> {
let resolved: Vec> = paths.iter().map(path_to_resolved).collect();
@@ -1238,9 +1238,9 @@ fn validate_projection_paths(paths: &[AttributePath]) -> Result<(), ExpressionEr
_ => {
return Err(ExpressionError::Validation {
message: format!(
- "Invalid ProjectionExpression: Two document paths conflict \
- with each other; must remove or rewrite one of these paths; \
- path one: [{}], path two: [{}]",
+ "Invalid ProjectionExpression: Two document paths conflict with \
+ each other; must remove or rewrite one of these paths; path one: \
+ [{}], path two: [{}]",
paths[i], paths[j]
),
});
@@ -1253,9 +1253,9 @@ fn validate_projection_paths(paths: &[AttributePath]) -> Result<(), ExpressionEr
if prefix_matches && (a.len() != b.len()) {
return Err(ExpressionError::Validation {
message: format!(
- "Invalid ProjectionExpression: Two document paths overlap with \
- each other; must remove or rewrite one of these paths; \
- path one: [{}], path two: [{}]",
+ "Invalid ProjectionExpression: Two document paths overlap with each \
+ other; must remove or rewrite one of these paths; path one: [{}], path \
+ two: [{}]",
paths[i], paths[j]
),
});
diff --git a/crates/ruststack-dynamodb-core/src/handler.rs b/crates/ruststack-dynamodb-core/src/handler.rs
index ff4eb2d..926170f 100644
--- a/crates/ruststack-dynamodb-core/src/handler.rs
+++ b/crates/ruststack-dynamodb-core/src/handler.rs
@@ -1,16 +1,12 @@
//! DynamoDB handler implementation bridging HTTP to business logic.
-use std::future::Future;
-use std::pin::Pin;
-use std::sync::Arc;
+use std::{future::Future, pin::Pin, sync::Arc};
use bytes::Bytes;
-
-use ruststack_dynamodb_http::body::DynamoDBResponseBody;
-use ruststack_dynamodb_http::dispatch::DynamoDBHandler;
-use ruststack_dynamodb_http::response::json_response;
-use ruststack_dynamodb_model::error::DynamoDBError;
-use ruststack_dynamodb_model::operations::DynamoDBOperation;
+use ruststack_dynamodb_http::{
+ body::DynamoDBResponseBody, dispatch::DynamoDBHandler, response::json_response,
+};
+use ruststack_dynamodb_model::{error::DynamoDBError, operations::DynamoDBOperation};
use crate::provider::RustStackDynamoDB;
@@ -44,6 +40,7 @@ impl DynamoDBHandler for RustStackDynamoDBHandler {
}
/// Dispatch a DynamoDB operation to the appropriate handler method.
+#[allow(clippy::too_many_lines)]
fn dispatch(
provider: &RustStackDynamoDB,
op: DynamoDBOperation,
@@ -118,6 +115,51 @@ fn dispatch(
let output = provider.handle_batch_write_item(input)?;
serialize(&output, &request_id)
}
+ DynamoDBOperation::TagResource => {
+ let input = deserialize(body)?;
+ let output = provider.handle_tag_resource(input)?;
+ serialize(&output, &request_id)
+ }
+ DynamoDBOperation::UntagResource => {
+ let input = deserialize(body)?;
+ let output = provider.handle_untag_resource(input)?;
+ serialize(&output, &request_id)
+ }
+ DynamoDBOperation::ListTagsOfResource => {
+ let input = deserialize(body)?;
+ let output = provider.handle_list_tags_of_resource(input)?;
+ serialize(&output, &request_id)
+ }
+ DynamoDBOperation::DescribeTimeToLive => {
+ let input = deserialize(body)?;
+ let output = provider.handle_describe_time_to_live(input)?;
+ serialize(&output, &request_id)
+ }
+ DynamoDBOperation::UpdateTimeToLive => {
+ let input = deserialize(body)?;
+ let output = provider.handle_update_time_to_live(input)?;
+ serialize(&output, &request_id)
+ }
+ DynamoDBOperation::TransactGetItems => {
+ let input = deserialize(body)?;
+ let output = provider.handle_transact_get_items(input)?;
+ serialize(&output, &request_id)
+ }
+ DynamoDBOperation::TransactWriteItems => {
+ let input = deserialize(body)?;
+ let output = provider.handle_transact_write_items(input)?;
+ serialize(&output, &request_id)
+ }
+ DynamoDBOperation::DescribeLimits => {
+ let input = deserialize(body)?;
+ let output = provider.handle_describe_limits(input)?;
+ serialize(&output, &request_id)
+ }
+ DynamoDBOperation::DescribeEndpoints => {
+ let input = deserialize(body)?;
+ let output = provider.handle_describe_endpoints(input)?;
+ serialize(&output, &request_id)
+ }
}
}
diff --git a/crates/ruststack-dynamodb-core/src/provider.rs b/crates/ruststack-dynamodb-core/src/provider.rs
index 2d63b99..f3d3ba9 100644
--- a/crates/ruststack-dynamodb-core/src/provider.rs
+++ b/crates/ruststack-dynamodb-core/src/provider.rs
@@ -1,38 +1,51 @@
//! DynamoDB provider implementing all MVP operations.
-use std::collections::{HashMap, HashSet};
-use std::sync::Arc;
-
-use ruststack_dynamodb_model::AttributeValue;
-use ruststack_dynamodb_model::error::DynamoDBError;
-use ruststack_dynamodb_model::input::{
- BatchGetItemInput, BatchWriteItemInput, CreateTableInput, DeleteItemInput, DeleteTableInput,
- DescribeTableInput, GetItemInput, ListTablesInput, PutItemInput, QueryInput, ScanInput,
- UpdateItemInput, UpdateTableInput,
-};
-use ruststack_dynamodb_model::output::{
- BatchGetItemOutput, BatchWriteItemOutput, CreateTableOutput, DeleteItemOutput,
- DeleteTableOutput, DescribeTableOutput, GetItemOutput, ListTablesOutput, PutItemOutput,
- QueryOutput, ScanOutput, UpdateItemOutput, UpdateTableOutput,
-};
-use ruststack_dynamodb_model::types::{
- AttributeAction, AttributeDefinition, AttributeValueUpdate, BillingMode, ComparisonOperator,
- Condition, ConditionalOperator, ExpectedAttributeValue, KeyType, ReturnValue,
- ScalarAttributeType, Select, TableStatus,
+use std::{
+ collections::{HashMap, HashSet},
+ sync::Arc,
};
-use crate::config::DynamoDBConfig;
-use crate::error::{expression_error_to_dynamodb, storage_error_to_dynamodb};
-use crate::expression::{
- AttributePath, EvalContext, PathElement, UpdateExpr, collect_names_from_expr,
- collect_names_from_projection, collect_names_from_update, collect_paths_from_expr,
- collect_values_from_expr, collect_values_from_update, parse_condition, parse_projection,
- parse_update,
+use ruststack_dynamodb_model::{
+ AttributeValue,
+ error::DynamoDBError,
+ input::{
+ BatchGetItemInput, BatchWriteItemInput, CreateTableInput, DeleteItemInput,
+ DeleteTableInput, DescribeEndpointsInput, DescribeLimitsInput, DescribeTableInput,
+ DescribeTimeToLiveInput, GetItemInput, ListTablesInput, ListTagsOfResourceInput,
+ PutItemInput, QueryInput, ScanInput, TagResourceInput, TransactGetItemsInput,
+ TransactWriteItemsInput, UntagResourceInput, UpdateItemInput, UpdateTableInput,
+ UpdateTimeToLiveInput,
+ },
+ output::{
+ BatchGetItemOutput, BatchWriteItemOutput, CreateTableOutput, DeleteItemOutput,
+ DeleteTableOutput, DescribeEndpointsOutput, DescribeLimitsOutput, DescribeTableOutput,
+ DescribeTimeToLiveOutput, Endpoint, GetItemOutput, ListTablesOutput,
+ ListTagsOfResourceOutput, PutItemOutput, QueryOutput, ScanOutput, TagResourceOutput,
+ TransactGetItemsOutput, TransactWriteItemsOutput, UntagResourceOutput, UpdateItemOutput,
+ UpdateTableOutput, UpdateTimeToLiveOutput,
+ },
+ types::{
+ AttributeAction, AttributeDefinition, AttributeValueUpdate, BillingMode,
+ CancellationReason, ComparisonOperator, Condition, ConditionalOperator,
+ ExpectedAttributeValue, ItemResponse, KeyType, ReturnValue, ScalarAttributeType, Select,
+ TableStatus, TimeToLiveDescription,
+ },
};
-use crate::state::{DynamoDBServiceState, DynamoDBTable};
-use crate::storage::{
- KeyAttribute, KeySchema, PrimaryKey, SortKeyCondition, SortableAttributeValue, TableStorage,
- calculate_item_size, extract_primary_key, partition_key_segment,
+
+use crate::{
+ config::DynamoDBConfig,
+ error::{expression_error_to_dynamodb, storage_error_to_dynamodb},
+ expression::{
+ AttributePath, EvalContext, PathElement, UpdateExpr, collect_names_from_expr,
+ collect_names_from_projection, collect_names_from_update, collect_paths_from_expr,
+ collect_values_from_expr, collect_values_from_update, parse_condition, parse_projection,
+ parse_update,
+ },
+ state::{DynamoDBServiceState, DynamoDBTable},
+ storage::{
+ KeyAttribute, KeySchema, PrimaryKey, SortKeyCondition, SortableAttributeValue,
+ TableStorage, calculate_item_size, extract_primary_key, partition_key_segment,
+ },
};
/// Maximum item size in bytes (400 KB).
@@ -98,7 +111,8 @@ fn validate_number_string(s: &str) -> Result<(), DynamoDBError> {
if ch == '.' {
if has_dot {
return Err(DynamoDBError::validation(
- "The parameter cannot be converted to a numeric value: numeric value is not valid",
+ "The parameter cannot be converted to a numeric value: numeric value is not \
+ valid",
));
}
has_dot = true;
@@ -136,7 +150,8 @@ fn validate_number_string(s: &str) -> Result<(), DynamoDBError> {
}
// Compute the actual magnitude of the number.
- // The number is: significant_digits * 10^(explicit_exp - frac_digits + trailing_zeros_in_significant)
+ // The number is: significant_digits * 10^(explicit_exp - frac_digits +
+ // trailing_zeros_in_significant)
let dot_pos = mantissa.find('.');
#[allow(clippy::cast_possible_wrap)]
let frac_digits = if let Some(pos) = dot_pos {
@@ -153,7 +168,8 @@ fn validate_number_string(s: &str) -> Result<(), DynamoDBError> {
if magnitude > 125 {
return Err(DynamoDBError::validation(
- "Number overflow. Attempting to store a number with magnitude larger than supported range",
+ "Number overflow. Attempting to store a number with magnitude larger than supported \
+ range",
));
}
// The smallest allowed magnitude is -130.
@@ -161,7 +177,8 @@ fn validate_number_string(s: &str) -> Result<(), DynamoDBError> {
// A number like 1e-131 has magnitude = -131, which is NOT allowed.
if magnitude < -130 {
return Err(DynamoDBError::validation(
- "Number underflow. Attempting to store a number with magnitude smaller than supported range",
+ "Number underflow. Attempting to store a number with magnitude smaller than supported \
+ range",
));
}
@@ -206,8 +223,8 @@ fn validate_numbers_in_value(val: &AttributeValue) -> Result<(), DynamoDBError>
fn validate_table_name(name: &str) -> Result<(), DynamoDBError> {
if name.len() < 3 || name.len() > 255 {
return Err(DynamoDBError::validation(format!(
- "TableName must be at least 3 characters long and at most 255 characters long, \
- but was {} characters",
+ "TableName must be at least 3 characters long and at most 255 characters long, but \
+ was {} characters",
name.len()
)));
}
@@ -233,17 +250,15 @@ fn validate_key_not_empty(
match val {
AttributeValue::S(s) if s.is_empty() => {
return Err(DynamoDBError::validation(format!(
- "One or more parameter values are not valid. \
- The AttributeValue for a key attribute cannot contain an \
- empty string value. Key: {}",
+ "One or more parameter values are not valid. The AttributeValue for a key \
+ attribute cannot contain an empty string value. Key: {}",
ka.name
)));
}
AttributeValue::B(b) if b.is_empty() => {
return Err(DynamoDBError::validation(format!(
- "One or more parameter values are not valid. \
- The AttributeValue for a key attribute cannot contain an \
- empty string value. Key: {}",
+ "One or more parameter values are not valid. The AttributeValue for a key \
+ attribute cannot contain an empty string value. Key: {}",
ka.name
)));
}
@@ -262,9 +277,8 @@ fn validate_key_only_has_key_attrs(
for attr_name in key.keys() {
if !is_key_attribute(attr_name, key_schema) {
return Err(DynamoDBError::validation(format!(
- "One or more parameter values are not valid. \
- Number of user supplied keys don't match number of table schema keys. \
- Keys provided: [{}], schema keys: [{}]",
+ "One or more parameter values are not valid. Number of user supplied keys don't \
+ match number of table schema keys. Keys provided: [{}], schema keys: [{}]",
format_key_names(key),
format_schema_key_names(key_schema),
)));
@@ -291,8 +305,8 @@ fn validate_key_types(
};
if !type_matches {
return Err(DynamoDBError::validation(format!(
- "The provided key element does not match the schema. \
- Expected type {expected} for key column {name}, got type {actual}",
+ "The provided key element does not match the schema. Expected type {expected} \
+ for key column {name}, got type {actual}",
expected = ka.attr_type,
name = ka.name,
actual = val.type_descriptor(),
@@ -309,8 +323,8 @@ fn validate_no_duplicate_attributes_to_get(attrs: &[String]) -> Result<(), Dynam
for attr in attrs {
if !seen.insert(attr.as_str()) {
return Err(DynamoDBError::validation(format!(
- "One or more parameter values are not valid. \
- Duplicate value in AttributesToGet: {attr}"
+ "One or more parameter values are not valid. Duplicate value in AttributesToGet: \
+ {attr}"
)));
}
}
@@ -356,7 +370,8 @@ fn validate_select(
Select::SpecificAttributes => {
if !has_projection && !has_attributes_to_get {
return Err(DynamoDBError::validation(
- "SPECIFIC_ATTRIBUTES requires either ProjectionExpression or AttributesToGet",
+ "SPECIFIC_ATTRIBUTES requires either ProjectionExpression or \
+ AttributesToGet",
));
}
}
@@ -395,8 +410,8 @@ fn validate_parallel_scan(
// TotalSegments must be in [1, MAX_TOTAL_SEGMENTS].
if total > MAX_TOTAL_SEGMENTS {
return Err(DynamoDBError::validation(format!(
- "1 validation error detected: Value '{total}' at 'totalSegments' failed \
- to satisfy constraint: Member must have value less than or equal to \
+ "1 validation error detected: Value '{total}' at 'totalSegments' failed to \
+ satisfy constraint: Member must have value less than or equal to \
{MAX_TOTAL_SEGMENTS}. The Segment parameter is required but was not present \
in the request when parameter TotalSegments is present"
)));
@@ -404,8 +419,8 @@ fn validate_parallel_scan(
// Segment must be in [0, TotalSegments).
if seg >= total {
return Err(DynamoDBError::validation(format!(
- "The Segment parameter is zero-indexed and must be less than \
- parameter TotalSegments. Segment: {seg}, TotalSegments: {total}"
+ "The Segment parameter is zero-indexed and must be less than parameter \
+ TotalSegments. Segment: {seg}, TotalSegments: {total}"
)));
}
// ExclusiveStartKey must map to the same segment.
@@ -415,8 +430,8 @@ fn validate_parallel_scan(
#[allow(clippy::cast_sign_loss)]
if key_segment != seg as u32 {
return Err(DynamoDBError::validation(
- "The provided Exclusive start key does not map to the provided \
- Segment and TotalSegments values."
+ "The provided Exclusive start key does not map to the provided Segment \
+ and TotalSegments values."
.to_owned(),
));
}
@@ -429,12 +444,12 @@ fn validate_parallel_scan(
// missing one, but boto3 rejects this client-side. We still
// handle it for raw API callers.
(Some(_), None) => Err(DynamoDBError::validation(
- "The TotalSegments parameter is required but was not present in the request \
- when parameter Segment is present",
+ "The TotalSegments parameter is required but was not present in the request when \
+ parameter Segment is present",
)),
(None, Some(_)) => Err(DynamoDBError::validation(
- "The Segment parameter is required but was not present in the request \
- when parameter TotalSegments is present",
+ "The Segment parameter is required but was not present in the request when parameter \
+ TotalSegments is present",
)),
}
}
@@ -510,9 +525,8 @@ impl RustStackDynamoDB {
// Validate attribute definitions are present.
if input.attribute_definitions.is_empty() {
return Err(DynamoDBError::validation(
- "One or more parameter values were invalid: \
- Some AttributeDefinitions are not valid. \
- AttributeDefinitions must be provided for all key attributes",
+ "One or more parameter values were invalid: Some AttributeDefinitions are not \
+ valid. AttributeDefinitions must be provided for all key attributes",
));
}
@@ -563,6 +577,7 @@ impl RustStackDynamoDB {
stream_specification: input.stream_specification,
sse_specification: input.sse_specification,
tags: parking_lot::RwLock::new(input.tags),
+ ttl: parking_lot::RwLock::new(None),
arn,
table_id: uuid::Uuid::new_v4().to_string(),
created_at: chrono::Utc::now(),
@@ -728,7 +743,8 @@ impl RustStackDynamoDB {
if !input.expected.is_empty() && input.condition_expression.is_some() {
return Err(DynamoDBError::validation(
"Can not use both expression and non-expression parameters in the same request: \
- Non-expression parameters: {Expected} Expression parameters: {ConditionExpression}",
+ Non-expression parameters: {Expected} Expression parameters: \
+ {ConditionExpression}",
));
}
@@ -951,7 +967,8 @@ impl RustStackDynamoDB {
if !input.expected.is_empty() && input.condition_expression.is_some() {
return Err(DynamoDBError::validation(
"Can not use both expression and non-expression parameters in the same request: \
- Non-expression parameters: {Expected} Expression parameters: {ConditionExpression}",
+ Non-expression parameters: {Expected} Expression parameters: \
+ {ConditionExpression}",
));
}
@@ -1077,13 +1094,15 @@ impl RustStackDynamoDB {
if !input.attribute_updates.is_empty() && input.update_expression.is_some() {
return Err(DynamoDBError::validation(
"Can not use both expression and non-expression parameters in the same request: \
- Non-expression parameters: {AttributeUpdates} Expression parameters: {UpdateExpression}",
+ Non-expression parameters: {AttributeUpdates} Expression parameters: \
+ {UpdateExpression}",
));
}
if !input.attribute_updates.is_empty() && input.condition_expression.is_some() {
return Err(DynamoDBError::validation(
"Can not use both expression and non-expression parameters in the same request: \
- Non-expression parameters: {AttributeUpdates} Expression parameters: {ConditionExpression}",
+ Non-expression parameters: {AttributeUpdates} Expression parameters: \
+ {ConditionExpression}",
));
}
if !input.expected.is_empty() && input.update_expression.is_some() {
@@ -1095,7 +1114,8 @@ impl RustStackDynamoDB {
if !input.expected.is_empty() && input.condition_expression.is_some() {
return Err(DynamoDBError::validation(
"Can not use both expression and non-expression parameters in the same request: \
- Non-expression parameters: {Expected} Expression parameters: {ConditionExpression}",
+ Non-expression parameters: {Expected} Expression parameters: \
+ {ConditionExpression}",
));
}
@@ -1150,8 +1170,7 @@ impl RustStackDynamoDB {
}
Some(existing_val) => {
return Err(DynamoDBError::validation(format!(
- "Type mismatch for ADD; operator type: L, \
- existing type: {}",
+ "Type mismatch for ADD; operator type: L, existing type: {}",
existing_val.type_descriptor(),
)));
}
@@ -1891,8 +1910,8 @@ impl RustStackDynamoDB {
let total_writes: usize = input.request_items.values().map(Vec::len).sum();
if total_writes > 25 {
return Err(DynamoDBError::validation(format!(
- "Too many items in the BatchWriteItem request; \
- the request length {total_writes} exceeds the limit of 25"
+ "Too many items in the BatchWriteItem request; the request length {total_writes} \
+ exceeds the limit of 25"
)));
}
@@ -1995,6 +2014,681 @@ impl RustStackDynamoDB {
}
}
+// ---------------------------------------------------------------------------
+// Tagging operations
+// ---------------------------------------------------------------------------
+
+/// Maximum number of tags per resource.
+const MAX_TAGS_PER_RESOURCE: usize = 50;
+
+/// Maximum tag key length (characters).
+const MAX_TAG_KEY_LENGTH: usize = 128;
+
+/// Maximum tag value length (characters).
+const MAX_TAG_VALUE_LENGTH: usize = 256;
+
+impl RustStackDynamoDB {
+ /// Resolve a DynamoDB resource ARN to a table name.
+ fn resolve_table_from_arn(arn: &str) -> Result<&str, DynamoDBError> {
+ arn.rsplit_once('/')
+ .map(|(_, name)| name)
+ .filter(|name| !name.is_empty())
+ .ok_or_else(|| DynamoDBError::validation("Invalid resource ARN"))
+ }
+
+ /// Handle `TagResource`.
+ #[allow(clippy::needless_pass_by_value)]
+ pub fn handle_tag_resource(
+ &self,
+ input: TagResourceInput,
+ ) -> Result {
+ let table_name = Self::resolve_table_from_arn(&input.resource_arn)?;
+ let table = self.state.require_table(table_name)?;
+
+ // Validate each tag.
+ for tag in &input.tags {
+ if tag.key.is_empty() || tag.key.len() > MAX_TAG_KEY_LENGTH {
+ return Err(DynamoDBError::validation(format!(
+ "Tag key must be between 1 and {MAX_TAG_KEY_LENGTH} characters long"
+ )));
+ }
+ if tag.value.len() > MAX_TAG_VALUE_LENGTH {
+ return Err(DynamoDBError::validation(format!(
+ "Tag value must be no more than {MAX_TAG_VALUE_LENGTH} characters long"
+ )));
+ }
+ if tag.key.starts_with("aws:") {
+ return Err(DynamoDBError::validation(
+ "Tag keys starting with 'aws:' are reserved for system use",
+ ));
+ }
+ }
+
+ let mut tags = table.tags.write();
+
+ // Clone, merge, validate count, then commit — avoids TOCTOU where
+ // over-limit tags persist if validation fails after mutation.
+ let mut merged = tags.clone();
+ for new_tag in &input.tags {
+ if let Some(existing) = merged.iter_mut().find(|t| t.key == new_tag.key) {
+ existing.value.clone_from(&new_tag.value);
+ } else {
+ merged.push(new_tag.clone());
+ }
+ }
+
+ if merged.len() > MAX_TAGS_PER_RESOURCE {
+ return Err(DynamoDBError::validation(format!(
+ "The number of tags exceeds the limit of {MAX_TAGS_PER_RESOURCE}"
+ )));
+ }
+
+ *tags = merged;
+
+ Ok(TagResourceOutput {})
+ }
+
+ /// Handle `UntagResource`.
+ #[allow(clippy::needless_pass_by_value)]
+ pub fn handle_untag_resource(
+ &self,
+ input: UntagResourceInput,
+ ) -> Result {
+ let table_name = Self::resolve_table_from_arn(&input.resource_arn)?;
+ let table = self.state.require_table(table_name)?;
+
+ let keys_to_remove: HashSet<&str> = input.tag_keys.iter().map(String::as_str).collect();
+ let mut tags = table.tags.write();
+ tags.retain(|t| !keys_to_remove.contains(t.key.as_str()));
+
+ Ok(UntagResourceOutput {})
+ }
+
+ /// Handle `ListTagsOfResource`.
+ #[allow(clippy::needless_pass_by_value)]
+ pub fn handle_list_tags_of_resource(
+ &self,
+ input: ListTagsOfResourceInput,
+ ) -> Result {
+ let table_name = Self::resolve_table_from_arn(&input.resource_arn)?;
+ let table = self.state.require_table(table_name)?;
+
+ let tags = table.tags.read().clone();
+ Ok(ListTagsOfResourceOutput {
+ tags: Some(tags),
+ next_token: None,
+ })
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Time to Live operations
+// ---------------------------------------------------------------------------
+
+impl RustStackDynamoDB {
+ /// Handle `UpdateTimeToLive`.
+ #[allow(clippy::needless_pass_by_value)]
+ pub fn handle_update_time_to_live(
+ &self,
+ input: UpdateTimeToLiveInput,
+ ) -> Result {
+ validate_table_name(&input.table_name)?;
+ let table = self.state.require_table(&input.table_name)?;
+
+ if input.time_to_live_specification.attribute_name.is_empty() {
+ return Err(DynamoDBError::validation(
+ "TimeToLiveSpecification AttributeName must not be empty",
+ ));
+ }
+
+ let spec = input.time_to_live_specification;
+ *table.ttl.write() = Some(spec.clone());
+
+ Ok(UpdateTimeToLiveOutput {
+ time_to_live_specification: Some(spec),
+ })
+ }
+
+ /// Handle `DescribeTimeToLive`.
+ #[allow(clippy::needless_pass_by_value)]
+ pub fn handle_describe_time_to_live(
+ &self,
+ input: DescribeTimeToLiveInput,
+ ) -> Result {
+ validate_table_name(&input.table_name)?;
+ let table = self.state.require_table(&input.table_name)?;
+
+ let ttl_guard = table.ttl.read();
+ let description = match ttl_guard.as_ref() {
+ Some(spec) => TimeToLiveDescription {
+ attribute_name: Some(spec.attribute_name.clone()),
+ time_to_live_status: Some(if spec.enabled {
+ "ENABLED".to_owned()
+ } else {
+ "DISABLED".to_owned()
+ }),
+ },
+ None => TimeToLiveDescription {
+ attribute_name: None,
+ time_to_live_status: Some("DISABLED".to_owned()),
+ },
+ };
+
+ Ok(DescribeTimeToLiveOutput {
+ time_to_live_description: Some(description),
+ })
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Describe operations
+// ---------------------------------------------------------------------------
+
+impl RustStackDynamoDB {
+ /// Handle `DescribeLimits`.
+ ///
+ /// Returns hardcoded account and table capacity limits matching the default
+ /// AWS DynamoDB provisioned-mode limits.
+ pub fn handle_describe_limits(
+ &self,
+ _input: DescribeLimitsInput,
+ ) -> Result {
+ Ok(DescribeLimitsOutput {
+ account_max_read_capacity_units: Some(80_000),
+ account_max_write_capacity_units: Some(80_000),
+ table_max_read_capacity_units: Some(40_000),
+ table_max_write_capacity_units: Some(40_000),
+ })
+ }
+
+ /// Handle `DescribeEndpoints`.
+ ///
+ /// Returns a single endpoint for the configured region with a 1440-minute
+ /// (24 hour) cache period, matching the real DynamoDB behaviour.
+ pub fn handle_describe_endpoints(
+ &self,
+ _input: DescribeEndpointsInput,
+ ) -> Result {
+ let address = format!("dynamodb.{}.amazonaws.com", self.config.default_region);
+ Ok(DescribeEndpointsOutput {
+ endpoints: vec![Endpoint {
+ address,
+ cache_period_in_minutes: 1440,
+ }],
+ })
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Transaction operations
+// ---------------------------------------------------------------------------
+
+/// Maximum number of items in a transaction.
+const MAX_TRANSACT_ITEMS: usize = 100;
+
+impl RustStackDynamoDB {
+ /// Handle `TransactGetItems`.
+ #[allow(clippy::needless_pass_by_value)]
+ pub fn handle_transact_get_items(
+ &self,
+ input: TransactGetItemsInput,
+ ) -> Result {
+ if input.transact_items.is_empty() {
+ return Err(DynamoDBError::validation(
+ "1 validation error detected: Value null at 'transactItems' failed to satisfy \
+ constraint: Member must not be null",
+ ));
+ }
+ if input.transact_items.len() > MAX_TRANSACT_ITEMS {
+ return Err(DynamoDBError::validation(format!(
+ "1 validation error detected: Value '[TransactGetItem]' at 'transactItems' failed \
+ to satisfy constraint: Member must have length less than or equal to \
+ {MAX_TRANSACT_ITEMS}"
+ )));
+ }
+
+ let mut responses = Vec::with_capacity(input.transact_items.len());
+
+ for transact_item in &input.transact_items {
+ let get = &transact_item.get;
+ let table = self.state.require_table(&get.table_name)?;
+ let pk = extract_primary_key(&table.key_schema, &get.key)
+ .map_err(storage_error_to_dynamodb)?;
+
+ let item = table.storage.get_item(&pk);
+
+ // Apply projection if specified.
+ let result_item = match (item, &get.projection_expression) {
+ (Some(found_item), Some(projection)) => {
+ let names = get.expression_attribute_names.as_ref();
+ let empty_names = HashMap::new();
+ let names_ref = names.unwrap_or(&empty_names);
+ let paths =
+ parse_projection(projection).map_err(projection_error_to_dynamodb)?;
+ let empty_values = HashMap::new();
+ let ctx = EvalContext {
+ item: &found_item,
+ names: names_ref,
+ values: &empty_values,
+ };
+ let projected = ctx.apply_projection(&paths);
+ if projected.is_empty() {
+ None
+ } else {
+ Some(projected)
+ }
+ }
+ (Some(found_item), None) => Some(found_item),
+ (None, _) => None,
+ };
+
+ responses.push(ItemResponse { item: result_item });
+ }
+
+ Ok(TransactGetItemsOutput {
+ consumed_capacity: Vec::new(),
+ responses: Some(responses),
+ })
+ }
+
+ /// Handle `TransactWriteItems`.
+ #[allow(clippy::too_many_lines, clippy::needless_pass_by_value)]
+ pub fn handle_transact_write_items(
+ &self,
+ input: TransactWriteItemsInput,
+ ) -> Result {
+ if input.transact_items.is_empty() {
+ return Err(DynamoDBError::validation(
+ "1 validation error detected: Value null at 'transactItems' failed to satisfy \
+ constraint: Member must not be null",
+ ));
+ }
+ if input.transact_items.len() > MAX_TRANSACT_ITEMS {
+ return Err(DynamoDBError::validation(format!(
+ "1 validation error detected: Value '[TransactWriteItem]' at 'transactItems' \
+ failed to satisfy constraint: Member must have length less than or equal to \
+ {MAX_TRANSACT_ITEMS}"
+ )));
+ }
+
+ // Phase 1: Validate each item has exactly one action and collect
+ // (table_name, primary_key) pairs for duplicate detection.
+ let mut seen_keys: HashSet<(String, PrimaryKey)> = HashSet::new();
+
+ for (idx, item) in input.transact_items.iter().enumerate() {
+ let action_count = [
+ item.condition_check.is_some(),
+ item.put.is_some(),
+ item.delete.is_some(),
+ item.update.is_some(),
+ ]
+ .iter()
+ .filter(|&&b| b)
+ .count();
+
+ if action_count != 1 {
+ return Err(DynamoDBError::validation(format!(
+ "TransactItems[{idx}] must specify exactly one of ConditionCheck, Put, \
+ Delete, or Update"
+ )));
+ }
+
+ // Extract (table_name, key) for duplicate detection.
+ let (table_name, key_map) = if let Some(ref cc) = item.condition_check {
+ (cc.table_name.as_str(), &cc.key)
+ } else if let Some(ref put) = item.put {
+ let table = self.state.require_table(&put.table_name)?;
+ let pk = extract_primary_key(&table.key_schema, &put.item)
+ .map_err(storage_error_to_dynamodb)?;
+ if !seen_keys.insert((put.table_name.clone(), pk)) {
+ return Err(DynamoDBError::validation(
+ "Transaction request cannot include multiple operations on one item",
+ ));
+ }
+ continue;
+ } else if let Some(ref del) = item.delete {
+ (del.table_name.as_str(), &del.key)
+ } else if let Some(ref upd) = item.update {
+ (upd.table_name.as_str(), &upd.key)
+ } else {
+ continue;
+ };
+
+ let table = self.state.require_table(table_name)?;
+ let pk = extract_primary_key(&table.key_schema, key_map)
+ .map_err(storage_error_to_dynamodb)?;
+ if !seen_keys.insert((table_name.to_owned(), pk)) {
+ return Err(DynamoDBError::validation(
+ "Transaction request cannot include multiple operations on one item",
+ ));
+ }
+ }
+
+ // Phase 2: Evaluate all condition expressions.
+ let mut cancellation_reasons: Vec = vec![
+ CancellationReason {
+ code: Some("None".to_owned()),
+ message: Some("None".to_owned()),
+ item: None,
+ };
+ input.transact_items.len()
+ ];
+ let mut any_cancelled = false;
+
+ for (idx, item) in input.transact_items.iter().enumerate() {
+ let condition_result = self.evaluate_transact_write_condition(item);
+ match condition_result {
+ Ok(()) => {}
+ Err(reason) => {
+ cancellation_reasons[idx] = reason;
+ any_cancelled = true;
+ }
+ }
+ }
+
+ if any_cancelled {
+ return Err(DynamoDBError::transaction_cancelled(cancellation_reasons));
+ }
+
+ // Phase 3: Apply all writes.
+ for item in &input.transact_items {
+ if let Some(ref put) = item.put {
+ let table = self.state.require_table(&put.table_name)?;
+ let old = table
+ .storage
+ .put_item(put.item.clone())
+ .map_err(storage_error_to_dynamodb)?;
+
+ if table
+ .stream_specification
+ .as_ref()
+ .is_some_and(|s| s.stream_enabled)
+ {
+ let event_name = if old.is_some() {
+ crate::stream::ChangeEventName::Modify
+ } else {
+ crate::stream::ChangeEventName::Insert
+ };
+ let keys = extract_key_attributes(&put.item, &table.key_schema_elements);
+ let size = calculate_item_size(&put.item);
+ self.emitter.emit(crate::stream::ChangeEvent {
+ table_name: table.name.clone(),
+ event_name,
+ keys,
+ old_image: old,
+ new_image: Some(put.item.clone()),
+ size_bytes: size,
+ });
+ }
+ } else if let Some(ref del) = item.delete {
+ let table = self.state.require_table(&del.table_name)?;
+ let pk = extract_primary_key(&table.key_schema, &del.key)
+ .map_err(storage_error_to_dynamodb)?;
+ let old = table.storage.delete_item(&pk);
+
+ if table
+ .stream_specification
+ .as_ref()
+ .is_some_and(|s| s.stream_enabled)
+ {
+ if let Some(ref old_item) = old {
+ let keys = extract_key_attributes(old_item, &table.key_schema_elements);
+ let size = calculate_item_size(old_item);
+ self.emitter.emit(crate::stream::ChangeEvent {
+ table_name: table.name.clone(),
+ event_name: crate::stream::ChangeEventName::Remove,
+ keys,
+ old_image: Some(old_item.clone()),
+ new_image: None,
+ size_bytes: size,
+ });
+ }
+ }
+ } else if let Some(ref upd) = item.update {
+ let table = self.state.require_table(&upd.table_name)?;
+ let pk = extract_primary_key(&table.key_schema, &upd.key)
+ .map_err(storage_error_to_dynamodb)?;
+ let existing = table.storage.get_item(&pk);
+ let current = existing.clone().unwrap_or_else(|| upd.key.clone());
+
+ let names = upd.expression_attribute_names.as_ref();
+ let values = upd.expression_attribute_values.as_ref();
+ let empty_names = HashMap::new();
+ let empty_values = HashMap::new();
+ let names_ref = names.unwrap_or(&empty_names);
+ let values_ref = values.unwrap_or(&empty_values);
+
+ let parsed =
+ parse_update(&upd.update_expression).map_err(expression_error_to_dynamodb)?;
+ let ctx = EvalContext {
+ item: ¤t,
+ names: names_ref,
+ values: values_ref,
+ };
+ let updated = ctx
+ .apply_update(&parsed)
+ .map_err(expression_error_to_dynamodb)?;
+
+ let old = table
+ .storage
+ .put_item(updated.clone())
+ .map_err(storage_error_to_dynamodb)?;
+
+ if table
+ .stream_specification
+ .as_ref()
+ .is_some_and(|s| s.stream_enabled)
+ {
+ let event_name = if existing.is_some() {
+ crate::stream::ChangeEventName::Modify
+ } else {
+ crate::stream::ChangeEventName::Insert
+ };
+ let keys = extract_key_attributes(&updated, &table.key_schema_elements);
+ let size = calculate_item_size(&updated);
+ self.emitter.emit(crate::stream::ChangeEvent {
+ table_name: table.name.clone(),
+ event_name,
+ keys,
+ old_image: old,
+ new_image: Some(updated),
+ size_bytes: size,
+ });
+ }
+ }
+ // ConditionCheck: no mutation needed.
+ }
+
+ Ok(TransactWriteItemsOutput {
+ consumed_capacity: Vec::new(),
+ item_collection_metrics: HashMap::new(),
+ })
+ }
+
+ /// Evaluate a condition expression for a single transaction write item.
+ ///
+ /// Returns `Ok(())` if the condition passes (or no condition exists),
+ /// or a `CancellationReason` if the condition fails.
+ fn evaluate_transact_write_condition(
+ &self,
+ item: &ruststack_dynamodb_model::types::TransactWriteItem,
+ ) -> Result<(), CancellationReason> {
+ if let Some(ref cc) = item.condition_check {
+ self.evaluate_condition_for_key(
+ &cc.table_name,
+ &cc.key,
+ Some(&cc.condition_expression),
+ cc.expression_attribute_names.as_ref(),
+ cc.expression_attribute_values.as_ref(),
+ cc.return_values_on_condition_check_failure.as_deref(),
+ )
+ } else if let Some(ref put) = item.put {
+ if let Some(ref condition) = put.condition_expression {
+ self.evaluate_transact_put_condition(put, condition)
+ } else {
+ Ok(())
+ }
+ } else if let Some(ref del) = item.delete {
+ self.evaluate_condition_for_key(
+ &del.table_name,
+ &del.key,
+ del.condition_expression.as_deref(),
+ del.expression_attribute_names.as_ref(),
+ del.expression_attribute_values.as_ref(),
+ del.return_values_on_condition_check_failure.as_deref(),
+ )
+ } else if let Some(ref upd) = item.update {
+ self.evaluate_condition_for_key(
+ &upd.table_name,
+ &upd.key,
+ upd.condition_expression.as_deref(),
+ upd.expression_attribute_names.as_ref(),
+ upd.expression_attribute_values.as_ref(),
+ upd.return_values_on_condition_check_failure.as_deref(),
+ )
+ } else {
+ Ok(())
+ }
+ }
+
+ /// Evaluate a condition expression for a `TransactPut` action.
+ ///
+ /// Put actions derive their primary key from the item rather than a
+ /// separate `Key` field, requiring special handling.
+ fn evaluate_transact_put_condition(
+ &self,
+ put: &ruststack_dynamodb_model::types::TransactPut,
+ condition: &str,
+ ) -> Result<(), CancellationReason> {
+ let table = self
+ .state
+ .require_table(&put.table_name)
+ .map_err(|e| CancellationReason {
+ code: Some("ValidationException".to_owned()),
+ message: Some(e.message.clone()),
+ item: None,
+ })?;
+ let pk =
+ extract_primary_key(&table.key_schema, &put.item).map_err(|e| CancellationReason {
+ code: Some("ValidationException".to_owned()),
+ message: Some(e.to_string()),
+ item: None,
+ })?;
+ let key_map: HashMap =
+ extract_key_attributes(&put.item, &table.key_schema_elements);
+ let existing = table.storage.get_item(&pk);
+ let empty = HashMap::new();
+ let item_ref = existing.as_ref().unwrap_or(&empty);
+ let empty_names = HashMap::new();
+ let empty_values = HashMap::new();
+ let names = put
+ .expression_attribute_names
+ .as_ref()
+ .unwrap_or(&empty_names);
+ let values = put
+ .expression_attribute_values
+ .as_ref()
+ .unwrap_or(&empty_values);
+
+ let expr = parse_condition(condition).map_err(|e| CancellationReason {
+ code: Some("ValidationException".to_owned()),
+ message: Some(e.to_string()),
+ item: None,
+ })?;
+ let ctx = EvalContext {
+ item: item_ref,
+ names,
+ values,
+ };
+ let result = ctx.evaluate(&expr).map_err(|e| CancellationReason {
+ code: Some("ValidationException".to_owned()),
+ message: Some(e.to_string()),
+ item: None,
+ })?;
+ if !result {
+ let return_item =
+ if put.return_values_on_condition_check_failure.as_deref() == Some("ALL_OLD") {
+ existing
+ } else {
+ None
+ };
+ return Err(CancellationReason {
+ code: Some("ConditionalCheckFailed".to_owned()),
+ message: Some("The conditional request failed".to_owned()),
+ item: return_item.or(Some(key_map)),
+ });
+ }
+ Ok(())
+ }
+
+ /// Evaluate a condition expression for a given table and key.
+ fn evaluate_condition_for_key(
+ &self,
+ table_name: &str,
+ key: &HashMap,
+ condition_expression: Option<&str>,
+ expression_names: Option<&HashMap>,
+ expression_values: Option<&HashMap>,
+ return_values_on_failure: Option<&str>,
+ ) -> Result<(), CancellationReason> {
+ let Some(condition_str) = condition_expression else {
+ return Ok(());
+ };
+
+ let table = self
+ .state
+ .require_table(table_name)
+ .map_err(|e| CancellationReason {
+ code: Some("ValidationException".to_owned()),
+ message: Some(e.message.clone()),
+ item: None,
+ })?;
+ let pk = extract_primary_key(&table.key_schema, key).map_err(|e| CancellationReason {
+ code: Some("ValidationException".to_owned()),
+ message: Some(e.to_string()),
+ item: None,
+ })?;
+ let existing = table.storage.get_item(&pk);
+ let empty = HashMap::new();
+ let item_ref = existing.as_ref().unwrap_or(&empty);
+ let empty_names = HashMap::new();
+ let empty_values = HashMap::new();
+ let names = expression_names.unwrap_or(&empty_names);
+ let values = expression_values.unwrap_or(&empty_values);
+
+ let expr = parse_condition(condition_str).map_err(|e| CancellationReason {
+ code: Some("ValidationException".to_owned()),
+ message: Some(e.to_string()),
+ item: None,
+ })?;
+ let ctx = EvalContext {
+ item: item_ref,
+ names,
+ values,
+ };
+ let result = ctx.evaluate(&expr).map_err(|e| CancellationReason {
+ code: Some("ValidationException".to_owned()),
+ message: Some(e.to_string()),
+ item: None,
+ })?;
+
+ if !result {
+ let return_item = if return_values_on_failure == Some("ALL_OLD") {
+ existing
+ } else {
+ None
+ };
+ return Err(CancellationReason {
+ code: Some("ConditionalCheckFailed".to_owned()),
+ message: Some("The conditional request failed".to_owned()),
+ item: return_item,
+ });
+ }
+
+ Ok(())
+ }
+}
+
// ---------------------------------------------------------------------------
// Legacy API conversion functions
// ---------------------------------------------------------------------------
@@ -2287,7 +2981,8 @@ fn build_condition_fragment(
values.insert(val_ph.clone(), v.clone());
}
format!(
- "(attribute_exists({name_placeholder}) AND NOT contains({name_placeholder}, {val_ph}))"
+ "(attribute_exists({name_placeholder}) AND NOT contains({name_placeholder}, \
+ {val_ph}))"
)
}
ComparisonOperator::BeginsWith => {
@@ -2380,8 +3075,8 @@ fn validate_return_values_on_condition_check_failure(
if v != "NONE" && v != "ALL_OLD" {
return Err(DynamoDBError::validation(format!(
"1 validation error detected: Value '{v}' at \
- 'returnValuesOnConditionCheckFailure' failed to satisfy constraint: \
- Member must satisfy enum value set: [NONE, ALL_OLD]"
+ 'returnValuesOnConditionCheckFailure' failed to satisfy constraint: Member must \
+ satisfy enum value set: [NONE, ALL_OLD]"
)));
}
}
@@ -2422,27 +3117,24 @@ fn validate_comparison_operator(
| ComparisonOperator::BeginsWith => {
if count != 1 {
return Err(DynamoDBError::validation(format!(
- "One or more parameter values were invalid: \
- Invalid number of argument(s) for the {comp_op} \
- ComparisonOperator"
+ "One or more parameter values were invalid: Invalid number of argument(s) for \
+ the {comp_op} ComparisonOperator"
)));
}
}
ComparisonOperator::Contains | ComparisonOperator::NotContains => {
if count != 1 {
return Err(DynamoDBError::validation(format!(
- "One or more parameter values were invalid: \
- Invalid number of argument(s) for the {comp_op} \
- ComparisonOperator"
+ "One or more parameter values were invalid: Invalid number of argument(s) for \
+ the {comp_op} ComparisonOperator"
)));
}
// CONTAINS/NOT_CONTAINS only accept scalar types (S, N, B).
if let Some(val) = value_list.first() {
if !is_scalar_attribute_value(val) {
return Err(DynamoDBError::validation(format!(
- "One or more parameter values were invalid: \
- ComparisonOperator {comp_op} is not valid for {val_type} \
- AttributeValue type",
+ "One or more parameter values were invalid: ComparisonOperator {comp_op} \
+ is not valid for {val_type} AttributeValue type",
val_type = val.type_descriptor(),
)));
}
@@ -2451,27 +3143,24 @@ fn validate_comparison_operator(
ComparisonOperator::Between => {
if count != 2 {
return Err(DynamoDBError::validation(
- "One or more parameter values were invalid: \
- Invalid number of argument(s) for the BETWEEN \
- ComparisonOperator",
+ "One or more parameter values were invalid: Invalid number of argument(s) for \
+ the BETWEEN ComparisonOperator",
));
}
}
ComparisonOperator::In => {
if count == 0 {
return Err(DynamoDBError::validation(
- "One or more parameter values were invalid: \
- Invalid number of argument(s) for the IN \
- ComparisonOperator",
+ "One or more parameter values were invalid: Invalid number of argument(s) for \
+ the IN ComparisonOperator",
));
}
// IN requires all values to be scalar and of the same type.
for val in value_list {
if !is_scalar_attribute_value(val) {
return Err(DynamoDBError::validation(
- "One or more parameter values were invalid: \
- ComparisonOperator IN is not valid for non-scalar \
- AttributeValue type",
+ "One or more parameter values were invalid: ComparisonOperator IN is not \
+ valid for non-scalar AttributeValue type",
));
}
}
@@ -2481,9 +3170,8 @@ fn validate_comparison_operator(
for val in &value_list[1..] {
if val.type_descriptor() != first_type {
return Err(DynamoDBError::validation(
- "One or more parameter values were invalid: \
- AttributeValues inside AttributeValueList must all \
- be of the same type",
+ "One or more parameter values were invalid: AttributeValues inside \
+ AttributeValueList must all be of the same type",
));
}
}
@@ -2492,9 +3180,8 @@ fn validate_comparison_operator(
ComparisonOperator::Null | ComparisonOperator::NotNull => {
if count != 0 {
return Err(DynamoDBError::validation(format!(
- "One or more parameter values were invalid: \
- Invalid number of argument(s) for the {comp_op} \
- ComparisonOperator"
+ "One or more parameter values were invalid: Invalid number of argument(s) for \
+ the {comp_op} ComparisonOperator"
)));
}
}
@@ -2525,15 +3212,14 @@ fn validate_expected(
} else if exp.exists == Some(true) && exp.value.is_none() {
// Exists:True without Value is a validation error.
return Err(DynamoDBError::validation(format!(
- "One or more parameter values were invalid: \
- Exists is set to TRUE for attribute ({attr_name}), \
- Value must also be set"
+ "One or more parameter values were invalid: Exists is set to TRUE for attribute \
+ ({attr_name}), Value must also be set"
)));
} else if exp.exists == Some(false) && exp.value.is_some() {
// Exists:False with Value is a validation error.
return Err(DynamoDBError::validation(format!(
- "One or more parameter values were invalid: \
- Value cannot be used when Exists is set to FALSE for attribute ({attr_name})"
+ "One or more parameter values were invalid: Value cannot be used when Exists is \
+ set to FALSE for attribute ({attr_name})"
)));
}
}
@@ -2552,8 +3238,8 @@ fn validate_no_empty_sets(values: &HashMap) -> Result<()
for (key, val) in values {
if is_empty_set(val) {
return Err(DynamoDBError::validation(format!(
- "One or more parameter values are not valid. The AttributeValue for a member \
- of the ExpressionAttributeValues ({key}) contains an empty set"
+ "One or more parameter values are not valid. The AttributeValue for a member of \
+ the ExpressionAttributeValues ({key}) contains an empty set"
)));
}
}
@@ -2619,8 +3305,8 @@ fn validate_item_no_empty_sets(
for val in item.values() {
if contains_empty_set(val) {
return Err(DynamoDBError::validation(
- "One or more parameter values were invalid: An number of elements of the \
- input set is empty",
+ "One or more parameter values were invalid: An number of elements of the input \
+ set is empty",
));
}
}
@@ -2698,8 +3384,8 @@ fn validate_no_unresolved_names(
for name in used_names {
if name.starts_with('#') && !provided_names.contains_key(name.as_str()) {
return Err(DynamoDBError::validation(format!(
- "Value provided in ExpressionAttributeNames unused in expressions: \
- unresolved attribute name reference: {name}"
+ "Value provided in ExpressionAttributeNames unused in expressions: unresolved \
+ attribute name reference: {name}"
)));
}
}
@@ -2845,14 +3531,14 @@ fn validate_update_paths(
for j in (i + 1)..resolved.len() {
if paths_overlap(&resolved[i], &resolved[j]) {
return Err(DynamoDBError::validation(
- "Invalid UpdateExpression: Two document paths overlap with each other; \
- must remove or rewrite one of these paths",
+ "Invalid UpdateExpression: Two document paths overlap with each other; must \
+ remove or rewrite one of these paths",
));
}
if paths_conflict(&resolved[i], &resolved[j]) {
return Err(DynamoDBError::validation(
- "Invalid UpdateExpression: Two document paths conflict with each other; \
- must remove or rewrite one of these paths",
+ "Invalid UpdateExpression: Two document paths conflict with each other; must \
+ remove or rewrite one of these paths",
));
}
}
@@ -3047,12 +3733,12 @@ fn merge_attribute_values(target: &mut AttributeValue, source: AttributeValue) {
/// - `NONE` / `None`: empty map
/// - `ALL_OLD`: all attributes of the old item (or empty if no old item)
/// - `ALL_NEW`: all attributes of the new item
-/// - `UPDATED_OLD`: for each path targeted by the update expression, return
-/// the old value if it existed (before the update). Only returns the
-/// specific nested sub-path, not the entire top-level attribute.
-/// - `UPDATED_NEW`: for each path targeted by the update expression, return
-/// the new value if it exists (after the update). For REMOVE'd attributes,
-/// the path no longer exists so it is not returned.
+/// - `UPDATED_OLD`: for each path targeted by the update expression, return the old value if it
+/// existed (before the update). Only returns the specific nested sub-path, not the entire
+/// top-level attribute.
+/// - `UPDATED_NEW`: for each path targeted by the update expression, return the new value if it
+/// exists (after the update). For REMOVE'd attributes, the path no longer exists so it is not
+/// returned.
fn compute_update_return_values(
return_values: Option<&ReturnValue>,
old_item: Option<&HashMap>,
@@ -4045,14 +4731,17 @@ fn gsi_build_last_key(
#[cfg(test)]
mod tests {
- use super::*;
- use ruststack_dynamodb_model::error::DynamoDBErrorCode;
- use ruststack_dynamodb_model::input::{BatchWriteItemInput, CreateTableInput, UpdateItemInput};
- use ruststack_dynamodb_model::types::{
- AttributeDefinition, KeySchemaElement, KeyType, PutRequest, ScalarAttributeType,
- WriteRequest,
+ use ruststack_dynamodb_model::{
+ error::DynamoDBErrorCode,
+ input::{BatchWriteItemInput, CreateTableInput, UpdateItemInput},
+ types::{
+ AttributeDefinition, KeySchemaElement, KeyType, PutRequest, ScalarAttributeType,
+ WriteRequest,
+ },
};
+ use super::*;
+
/// Create a provider with a pre-configured test table named "TestTable".
fn setup_provider_with_table() -> RustStackDynamoDB {
let provider = RustStackDynamoDB::new(DynamoDBConfig::default());
diff --git a/crates/ruststack-dynamodb-core/src/state.rs b/crates/ruststack-dynamodb-core/src/state.rs
index e51a9c7..ff997c8 100644
--- a/crates/ruststack-dynamodb-core/src/state.rs
+++ b/crates/ruststack-dynamodb-core/src/state.rs
@@ -3,14 +3,15 @@
use std::sync::Arc;
use dashmap::DashMap;
-
-use ruststack_dynamodb_model::error::DynamoDBError;
-use ruststack_dynamodb_model::types::{
- AttributeDefinition, BillingMode, BillingModeSummary, GlobalSecondaryIndex,
- GlobalSecondaryIndexDescription, IndexStatus, KeySchemaElement, LocalSecondaryIndex,
- LocalSecondaryIndexDescription, ProvisionedThroughput, ProvisionedThroughputDescription,
- SSEDescription, SSESpecification, SseStatus, SseType, StreamSpecification, TableDescription,
- TableStatus, Tag,
+use ruststack_dynamodb_model::{
+ error::DynamoDBError,
+ types::{
+ AttributeDefinition, BillingMode, BillingModeSummary, GlobalSecondaryIndex,
+ GlobalSecondaryIndexDescription, IndexStatus, KeySchemaElement, LocalSecondaryIndex,
+ LocalSecondaryIndexDescription, ProvisionedThroughput, ProvisionedThroughputDescription,
+ SSEDescription, SSESpecification, SseStatus, SseType, StreamSpecification,
+ TableDescription, TableStatus, Tag, TimeToLiveSpecification,
+ },
};
use crate::storage::{KeySchema, TableStorage};
@@ -119,6 +120,8 @@ pub struct DynamoDBTable {
pub sse_specification: Option,
/// Tags.
pub tags: parking_lot::RwLock>,
+ /// Time-to-Live specification.
+ pub ttl: parking_lot::RwLock