mirror of https://github.com/smithy-lang/smithy-rs
Enable Endpoints 2.0 (#2074)
* wip * Fix region decorator * Update S3 tests to succeed * Create 'endpoint_url' setters * Fix SDK adhoc tests * Fix endpoint tests * Fix protocol test generator to have a stub endpoint resolver * Fix some more tests * Fix aws rust runtime tests * Update generator to appease clippy * CR feedback * Fix compilation * Fix tests * Fix doc links * Fix SDK integration tests * Update changelog * Fix s3 control by adding transformer * Throw a specific exception if the service doesn't have ep rules * Add codecatalyst to the list of custom services
This commit is contained in:
parent
59de022029
commit
29abdc9b42
|
@ -11,6 +11,38 @@
|
|||
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
|
||||
# author = "rcoh"
|
||||
|
||||
[[aws-sdk-rust]]
|
||||
message = """Integrate Endpoints 2.0 into the Rust SDK. Endpoints 2.0 enables features like S3 virtual addressing & S3
|
||||
object lambda. As part of this change, there are several breaking changes although efforts have been made to deprecate
|
||||
where possible to smooth the upgrade path.
|
||||
1. `aws_smithy_http::endpoint::Endpoint` and the `endpoint_resolver` methods have been deprecated. In general, these usages
|
||||
should be replaced with usages of `endpoint_url` instead. `endpoint_url` accepts a string so an `aws_smithy_http::Endpoint`
|
||||
does not need to be constructed. This structure and methods will be removed in a future release.
|
||||
2. The `endpoint_resolver` method on `<service>::config::Builder` now accepts a service specific endpoint resolver instead
|
||||
of an implementation of `ResolveAwsEndpoint`. Most users will be able to replace these usages with a usage of `endpoint_url`.
|
||||
3. `ResolveAwsEndpoint` has been deprecated and will be removed in a future version of the SDK.
|
||||
4. The SDK does not support "pseudo regions" anymore. Specifically, regions like `iam-fips` will no longer resolve to a FIPS endpoint.
|
||||
"""
|
||||
references = ["smithy-rs#1784", "smithy-rs#2074"]
|
||||
meta = { "breaking" = true, "tada" = true, "bug" = false }
|
||||
author = "rcoh"
|
||||
|
||||
[[aws-sdk-rust]]
|
||||
message = """Add additional configuration parameters to `aws_sdk_s3::Config`.
|
||||
|
||||
The launch of endpoints 2.0 includes more configuration options for S3. The default behavior for endpoint resolution has
|
||||
been changed. Before, all requests hit the path-style endpoint. Going forward, all requests that can be routed to the
|
||||
virtually hosted bucket will be routed there automatically.
|
||||
- `force_path_style`: Requests will now default to the virtually-hosted endpoint `<bucketname>.s3.<region>.amazonaws.com`
|
||||
- `use_arn_region`: Enables this client to use an ARN’s region when constructing an endpoint instead of the client’s configured region.
|
||||
- `accelerate`: Enables this client to use S3 Transfer Acceleration endpoints.
|
||||
|
||||
Note: the AWS SDK for Rust does not currently support Multi Region Access Points (MRAP).
|
||||
"""
|
||||
references = ["smithy-rs#1784", "smithy-rs#2074"]
|
||||
meta = { "breaking" = true, "tada" = true, "bug" = false }
|
||||
author = "rcoh"
|
||||
|
||||
[[smithy-rs]]
|
||||
message = "In 0.52, `@length`-constrained collection shapes whose members are not constrained made the server code generator crash. This has been fixed."
|
||||
references = ["smithy-rs#2103"]
|
||||
|
|
|
@ -52,7 +52,7 @@ use std::io;
|
|||
use std::net::IpAddr;
|
||||
|
||||
use aws_smithy_client::erase::boxclone::BoxCloneService;
|
||||
use aws_smithy_http::endpoint::Endpoint;
|
||||
use aws_smithy_http::endpoint::apply_endpoint;
|
||||
use aws_smithy_types::error::display::DisplayErrorContext;
|
||||
use aws_types::credentials;
|
||||
use aws_types::credentials::{future, CredentialsError, ProvideCredentials};
|
||||
|
@ -190,10 +190,8 @@ impl Provider {
|
|||
});
|
||||
}
|
||||
};
|
||||
let endpoint =
|
||||
Endpoint::immutable_uri(Uri::from_static(BASE_HOST)).expect("BASE_HOST is valid");
|
||||
endpoint
|
||||
.set_endpoint(&mut relative_uri, None)
|
||||
let endpoint = Uri::from_static(BASE_HOST);
|
||||
apply_endpoint(&mut relative_uri, &endpoint, None)
|
||||
.expect("appending relative URLs to the ECS endpoint should always succeed");
|
||||
Ok(relative_uri)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ use aws_smithy_client::http_connector::ConnectorSettings;
|
|||
use aws_smithy_client::{erase::DynConnector, SdkSuccess};
|
||||
use aws_smithy_client::{retry, SdkError};
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::endpoint::Endpoint;
|
||||
use aws_smithy_http::endpoint::apply_endpoint;
|
||||
use aws_smithy_http::operation;
|
||||
use aws_smithy_http::operation::{Metadata, Operation};
|
||||
use aws_smithy_http::response::ParseStrictResponse;
|
||||
|
@ -128,7 +128,7 @@ pub struct Client {
|
|||
|
||||
#[derive(Debug)]
|
||||
struct ClientInner {
|
||||
endpoint: Endpoint,
|
||||
endpoint: Uri,
|
||||
smithy_client: aws_smithy_client::Client<DynConnector, ImdsMiddleware>,
|
||||
}
|
||||
|
||||
|
@ -235,10 +235,7 @@ impl Client {
|
|||
let mut base_uri: Uri = path.parse().map_err(|_| {
|
||||
ImdsError::unexpected("IMDS path was not a valid URI. Hint: does it begin with `/`?")
|
||||
})?;
|
||||
self.inner
|
||||
.endpoint
|
||||
.set_endpoint(&mut base_uri, None)
|
||||
.map_err(ImdsError::unexpected)?;
|
||||
apply_endpoint(&mut base_uri, &self.inner.endpoint, None).map_err(ImdsError::unexpected)?;
|
||||
let request = http::Request::builder()
|
||||
.uri(base_uri)
|
||||
.body(SdkBody::empty())
|
||||
|
@ -434,7 +431,6 @@ impl Builder {
|
|||
.endpoint
|
||||
.unwrap_or_else(|| EndpointSource::Env(config.env(), config.fs()));
|
||||
let endpoint = endpoint_source.endpoint(self.mode_override).await?;
|
||||
let endpoint = Endpoint::immutable_uri(endpoint)?;
|
||||
let retry_config = retry::Config::default()
|
||||
.with_max_attempts(self.max_attempts.unwrap_or(DEFAULT_ATTEMPTS));
|
||||
let token_loader = token::TokenMiddleware::new(
|
||||
|
|
|
@ -23,7 +23,7 @@ use aws_smithy_async::rt::sleep::AsyncSleep;
|
|||
use aws_smithy_client::erase::DynConnector;
|
||||
use aws_smithy_client::retry;
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::endpoint::Endpoint;
|
||||
use aws_smithy_http::endpoint::apply_endpoint;
|
||||
use aws_smithy_http::middleware::AsyncMapRequest;
|
||||
use aws_smithy_http::operation;
|
||||
use aws_smithy_http::operation::Operation;
|
||||
|
@ -66,7 +66,7 @@ pub(super) struct TokenMiddleware {
|
|||
token_parser: GetTokenResponseHandler,
|
||||
token: ExpiringCache<Token, ImdsError>,
|
||||
time_source: TimeSource,
|
||||
endpoint: Endpoint,
|
||||
endpoint: Uri,
|
||||
token_ttl: Duration,
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ impl TokenMiddleware {
|
|||
pub(super) fn new(
|
||||
connector: DynConnector,
|
||||
time_source: TimeSource,
|
||||
endpoint: Endpoint,
|
||||
endpoint: Uri,
|
||||
token_ttl: Duration,
|
||||
retry_config: retry::Config,
|
||||
timeout_config: TimeoutConfig,
|
||||
|
@ -128,9 +128,7 @@ impl TokenMiddleware {
|
|||
|
||||
async fn get_token(&self) -> Result<(Token, SystemTime), ImdsError> {
|
||||
let mut uri = Uri::from_static("/latest/api/token");
|
||||
self.endpoint
|
||||
.set_endpoint(&mut uri, None)
|
||||
.map_err(ImdsError::unexpected)?;
|
||||
apply_endpoint(&mut uri, &self.endpoint, None).map_err(ImdsError::unexpected)?;
|
||||
let request = http::Request::builder()
|
||||
.header(
|
||||
X_AWS_EC2_METADATA_TOKEN_TTL_SECONDS,
|
||||
|
|
|
@ -174,6 +174,7 @@ mod loader {
|
|||
app_name: Option<AppName>,
|
||||
credentials_provider: Option<SharedCredentialsProvider>,
|
||||
endpoint_resolver: Option<Arc<dyn ResolveAwsEndpoint>>,
|
||||
endpoint_url: Option<String>,
|
||||
region: Option<Box<dyn ProvideRegion>>,
|
||||
retry_config: Option<RetryConfig>,
|
||||
sleep: Option<Arc<dyn AsyncSleep>>,
|
||||
|
@ -315,6 +316,8 @@ mod loader {
|
|||
|
||||
/// Override the endpoint resolver used for **all** AWS Services
|
||||
///
|
||||
/// This method is deprecated. Use [`Self::endpoint_url`] instead.
|
||||
///
|
||||
/// This method will override the endpoint resolver used for **all** AWS services. This mainly
|
||||
/// exists to set a static endpoint for tools like `LocalStack`. For live traffic, AWS services
|
||||
/// require the service-specific endpoint resolver they load by default.
|
||||
|
@ -332,6 +335,7 @@ mod loader {
|
|||
/// .await;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
#[deprecated(note = "use `.endpoint_url(...)` instead")]
|
||||
pub fn endpoint_resolver(
|
||||
mut self,
|
||||
endpoint_resolver: impl ResolveAwsEndpoint + 'static,
|
||||
|
@ -340,6 +344,30 @@ mod loader {
|
|||
self
|
||||
}
|
||||
|
||||
/// Override the endpoint URL used for **all** AWS services.
|
||||
///
|
||||
/// This method will override the endpoint URL used for **all** AWS services. This primarily
|
||||
/// exists to set a static endpoint for tools like `LocalStack`. When sending requests to
|
||||
/// production AWS services, this method should only be used for service-specific behavior.
|
||||
///
|
||||
/// When this method is used, the [`Region`](aws_types::region::Region) is only used for
|
||||
/// signing; it is not used to route the request.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Use a static endpoint for all services
|
||||
/// ```no_run
|
||||
/// # async fn create_config() {
|
||||
/// let sdk_config = aws_config::from_env()
|
||||
/// .endpoint_url("http://localhost:1234")
|
||||
/// .load()
|
||||
/// .await;
|
||||
/// # }
|
||||
pub fn endpoint_url(mut self, endpoint_url: impl Into<String>) -> Self {
|
||||
self.endpoint_url = Some(endpoint_url.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set configuration for all sub-loaders (credentials, region etc.)
|
||||
///
|
||||
/// Update the `ProviderConfig` used for all nested loaders. This can be used to override
|
||||
|
@ -458,6 +486,7 @@ mod loader {
|
|||
builder.set_endpoint_resolver(endpoint_resolver);
|
||||
builder.set_app_name(app_name);
|
||||
builder.set_sleep_impl(sleep_impl);
|
||||
builder.set_endpoint_url(self.endpoint_url);
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,10 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod partition;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use partition::Partition;
|
||||
#[doc(hidden)]
|
||||
pub use partition::PartitionResolver;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use aws_smithy_http::endpoint::error::ResolveEndpointError;
|
||||
use aws_smithy_http::endpoint::ResolveEndpoint;
|
||||
|
@ -18,13 +14,10 @@ use aws_smithy_http::middleware::MapRequest;
|
|||
use aws_smithy_http::operation::Request;
|
||||
use aws_smithy_types::endpoint::Endpoint as SmithyEndpoint;
|
||||
use aws_smithy_types::Document;
|
||||
use aws_types::region::{Region, SigningRegion};
|
||||
use aws_types::SigningService;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use aws_types::endpoint::{AwsEndpoint, BoxError, CredentialScope, ResolveAwsEndpoint};
|
||||
use aws_types::region::{Region, SigningRegion};
|
||||
use aws_types::SigningService;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct Params {
|
||||
|
@ -49,8 +42,12 @@ impl EndpointShim {
|
|||
}
|
||||
}
|
||||
|
||||
impl ResolveEndpoint<Params> for EndpointShim {
|
||||
fn resolve_endpoint(&self, params: &Params) -> Result<SmithyEndpoint, ResolveEndpointError> {
|
||||
impl<T> ResolveEndpoint<T> for EndpointShim
|
||||
where
|
||||
T: Clone + Into<Params>,
|
||||
{
|
||||
fn resolve_endpoint(&self, params: &T) -> Result<SmithyEndpoint, ResolveEndpointError> {
|
||||
let params: Params = params.clone().into();
|
||||
let aws_endpoint = self
|
||||
.0
|
||||
.resolve_endpoint(
|
||||
|
@ -194,41 +191,33 @@ fn smithy_to_aws(value: &SmithyEndpoint) -> Result<EndpointMetadata, Box<dyn Err
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use http::header::HOST;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::endpoint::ResolveEndpoint;
|
||||
use aws_smithy_http::middleware::MapRequest;
|
||||
use aws_smithy_http::operation;
|
||||
use aws_types::endpoint::CredentialScope;
|
||||
use aws_smithy_types::endpoint::Endpoint;
|
||||
use aws_smithy_types::Document;
|
||||
use http::header::HOST;
|
||||
|
||||
use aws_types::region::{Region, SigningRegion};
|
||||
use aws_types::SigningService;
|
||||
|
||||
use crate::partition::endpoint::{Metadata, Protocol, SignatureVersion};
|
||||
use crate::{AwsAuthStage, EndpointShim, Params};
|
||||
use crate::AwsAuthStage;
|
||||
|
||||
#[test]
|
||||
fn default_endpoint_updates_request() {
|
||||
let provider = Arc::new(Metadata {
|
||||
uri_template: "kinesis.{region}.amazonaws.com",
|
||||
protocol: Protocol::Https,
|
||||
credential_scope: Default::default(),
|
||||
signature_versions: SignatureVersion::V4,
|
||||
});
|
||||
let endpoint = Endpoint::builder()
|
||||
.url("kinesis.us-east-1.amazon.com")
|
||||
.build();
|
||||
let req = http::Request::new(SdkBody::from(""));
|
||||
let region = Region::new("us-east-1");
|
||||
let mut req = operation::Request::new(req);
|
||||
{
|
||||
let mut props = req.properties_mut();
|
||||
props.insert(region.clone());
|
||||
props.insert(SigningRegion::from(region.clone()));
|
||||
props.insert(SigningService::from_static("kinesis"));
|
||||
props.insert(
|
||||
EndpointShim::from_arc(provider)
|
||||
.resolve_endpoint(&Params::new(Some(region.clone())))
|
||||
.unwrap(),
|
||||
);
|
||||
props.insert(endpoint);
|
||||
};
|
||||
let req = AwsAuthStage.apply(req).expect("should succeed");
|
||||
assert_eq!(req.properties().get(), Some(&SigningRegion::from(region)));
|
||||
|
@ -239,24 +228,32 @@ mod test {
|
|||
|
||||
assert!(req.http().headers().get(HOST).is_none());
|
||||
assert!(
|
||||
req.properties()
|
||||
.get::<aws_smithy_types::endpoint::Endpoint>()
|
||||
.is_some(),
|
||||
req.properties().get::<Endpoint>().is_some(),
|
||||
"Endpoint middleware MUST leave the result in the bag"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_service_override_when_set() {
|
||||
let provider = Arc::new(Metadata {
|
||||
uri_template: "www.service.com",
|
||||
protocol: Protocol::Http,
|
||||
credential_scope: CredentialScope::builder()
|
||||
.service(SigningService::from_static("qldb-override"))
|
||||
.region(SigningRegion::from_static("us-east-override"))
|
||||
.build(),
|
||||
signature_versions: SignatureVersion::V4,
|
||||
});
|
||||
let endpoint = Endpoint::builder()
|
||||
.url("kinesis.us-east-override.amazon.com")
|
||||
.property(
|
||||
"authSchemes",
|
||||
vec![Document::Object({
|
||||
let mut out = HashMap::new();
|
||||
out.insert("name".to_string(), "sigv4".to_string().into());
|
||||
out.insert(
|
||||
"signingName".to_string(),
|
||||
"qldb-override".to_string().into(),
|
||||
);
|
||||
out.insert(
|
||||
"signingRegion".to_string(),
|
||||
"us-east-override".to_string().into(),
|
||||
);
|
||||
out
|
||||
})],
|
||||
)
|
||||
.build();
|
||||
let req = http::Request::new(SdkBody::from(""));
|
||||
let region = Region::new("us-east-1");
|
||||
let mut req = operation::Request::new(req);
|
||||
|
@ -264,11 +261,7 @@ mod test {
|
|||
let mut props = req.properties_mut();
|
||||
props.insert(region.clone());
|
||||
props.insert(SigningService::from_static("qldb"));
|
||||
props.insert(
|
||||
EndpointShim::from_arc(provider)
|
||||
.resolve_endpoint(&Params::new(Some(region)))
|
||||
.unwrap(),
|
||||
);
|
||||
props.insert(endpoint);
|
||||
};
|
||||
let req = AwsAuthStage.apply(req).expect("should succeed");
|
||||
assert_eq!(
|
||||
|
@ -283,30 +276,18 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn supports_fallback_when_scope_is_unset() {
|
||||
let provider = Arc::new(Metadata {
|
||||
uri_template: "www.service.com",
|
||||
protocol: Protocol::Http,
|
||||
credential_scope: CredentialScope::builder().build(),
|
||||
signature_versions: SignatureVersion::V4,
|
||||
});
|
||||
let endpoint = Endpoint::builder().url("www.service.com").build();
|
||||
let req = http::Request::new(SdkBody::from(""));
|
||||
let region = Region::new("us-east-1");
|
||||
let region = SigningRegion::from_static("us-east-1");
|
||||
let mut req = operation::Request::new(req);
|
||||
{
|
||||
let mut props = req.properties_mut();
|
||||
props.insert(region.clone());
|
||||
props.insert(SigningService::from_static("qldb"));
|
||||
props.insert(
|
||||
EndpointShim::from_arc(provider)
|
||||
.resolve_endpoint(&Params::new(Some(region)))
|
||||
.unwrap(),
|
||||
);
|
||||
props.insert(endpoint);
|
||||
};
|
||||
let req = AwsAuthStage.apply(req).expect("should succeed");
|
||||
assert_eq!(
|
||||
req.properties().get(),
|
||||
Some(&SigningRegion::from(Region::new("us-east-1")))
|
||||
);
|
||||
assert_eq!(req.properties().get(), Some(®ion));
|
||||
assert_eq!(
|
||||
req.properties().get(),
|
||||
Some(&SigningService::from_static("qldb"))
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
use aws_smithy_http::endpoint::Endpoint;
|
||||
use aws_types::endpoint::{AwsEndpoint, BoxError, CredentialScope, ResolveAwsEndpoint};
|
||||
use aws_types::region::Region;
|
||||
|
||||
/// Endpoint metadata
|
||||
///
|
||||
/// Unlike other endpoint implementations, no merging occurs in here. All Endpoint merging occurs
|
||||
/// during code generation allowing us to generate fully formed endpoints.
|
||||
#[derive(Debug)]
|
||||
pub struct Metadata {
|
||||
/// URI for the endpoint.
|
||||
///
|
||||
/// May contain `{region}` which will replaced with the region during endpoint construction
|
||||
pub uri_template: &'static str,
|
||||
|
||||
/// Protocol to use for this endpoint
|
||||
pub protocol: Protocol,
|
||||
|
||||
/// Credential scope to set for requests to this endpoint
|
||||
pub credential_scope: CredentialScope,
|
||||
|
||||
/// Signature versions supported by this endpoint.
|
||||
///
|
||||
/// Currently unused since the SDK only supports SigV4
|
||||
pub signature_versions: SignatureVersion,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
|
||||
pub enum Protocol {
|
||||
Http,
|
||||
Https,
|
||||
}
|
||||
|
||||
impl Protocol {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Protocol::Http => "http",
|
||||
Protocol::Https => "https",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
|
||||
pub enum SignatureVersion {
|
||||
V4,
|
||||
}
|
||||
|
||||
impl ResolveAwsEndpoint for Metadata {
|
||||
fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> {
|
||||
let uri = self.uri_template.replace("{region}", region.as_ref());
|
||||
let uri = format!("{}://{}", self.protocol.as_str(), uri);
|
||||
let endpoint = Endpoint::mutable(uri)?;
|
||||
let mut credential_scope = CredentialScope::builder().region(
|
||||
self.credential_scope
|
||||
.region()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| region.clone().into()),
|
||||
);
|
||||
if let Some(service) = self.credential_scope.service() {
|
||||
credential_scope = credential_scope.service(service.clone());
|
||||
}
|
||||
Ok(AwsEndpoint::new(endpoint, credential_scope.build()))
|
||||
}
|
||||
}
|
|
@ -1,390 +0,0 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
pub mod endpoint;
|
||||
|
||||
use aws_types::endpoint::{AwsEndpoint, BoxError, ResolveAwsEndpoint};
|
||||
use aws_types::region::Region;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::iter;
|
||||
|
||||
/// Root level resolver for an AWS Service
|
||||
///
|
||||
/// PartitionResolver resolves the endpoint for an AWS Service. Each partition will be checked
|
||||
/// in turn, checking if the partition [can resolve](Partition::can_resolve) the given region. If
|
||||
/// no regions match, `base` is used.
|
||||
///
|
||||
/// Once a partition has been identified, endpoint resolution is delegated to the underlying
|
||||
/// partition.
|
||||
#[derive(Debug)]
|
||||
pub struct PartitionResolver {
|
||||
/// Base partition used if no partitions match the region regex
|
||||
base: Partition,
|
||||
|
||||
// base and rest are split so that we can validate that at least 1 partition is defined
|
||||
// at compile time.
|
||||
rest: Vec<Partition>,
|
||||
}
|
||||
|
||||
impl PartitionResolver {
|
||||
/// Construct a new `PartitionResolver` from a list of partitions
|
||||
pub fn new(base: Partition, rest: Vec<Partition>) -> Self {
|
||||
Self { base, rest }
|
||||
}
|
||||
|
||||
fn partitions(&self) -> impl Iterator<Item = &Partition> {
|
||||
iter::once(&self.base).chain(self.rest.iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveAwsEndpoint for PartitionResolver {
|
||||
fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> {
|
||||
let matching_partition = self
|
||||
.partitions()
|
||||
.find(|partition| partition.can_resolve(region))
|
||||
.unwrap_or(&self.base);
|
||||
matching_partition.resolve_endpoint(region)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Partition {
|
||||
_id: &'static str,
|
||||
region_regex: Regex,
|
||||
partition_endpoint: Option<Region>,
|
||||
regionalized: Regionalized,
|
||||
default_endpoint: endpoint::Metadata,
|
||||
endpoints: HashMap<Region, endpoint::Metadata>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Builder {
|
||||
id: Option<&'static str>,
|
||||
region_regex: Option<Regex>,
|
||||
partition_endpoint: Option<Region>,
|
||||
regionalized: Option<Regionalized>,
|
||||
default_endpoint: Option<endpoint::Metadata>,
|
||||
endpoints: HashMap<Region, endpoint::Metadata>,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
pub fn id(mut self, id: &'static str) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn default_endpoint(mut self, default: endpoint::Metadata) -> Self {
|
||||
self.default_endpoint = Some(default);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn region_regex(mut self, regex: &'static str) -> Self {
|
||||
// We use a stripped down version of the regex crate without unicode support
|
||||
// To support `\d` and `\w`, we need to explicitly opt into the ascii-only version.
|
||||
let ascii_only = regex
|
||||
.replace("\\d", "(?-u:\\d)")
|
||||
.replace("\\w", "(?-u:\\w)");
|
||||
self.region_regex = Some(Regex::new(&ascii_only).expect("invalid regex"));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn partition_endpoint(mut self, partition_endpoint: &'static str) -> Self {
|
||||
self.partition_endpoint = Some(Region::new(partition_endpoint));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn regionalized(mut self, regionalized: Regionalized) -> Self {
|
||||
self.regionalized = Some(regionalized);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn endpoint(mut self, region: &'static str, endpoint: endpoint::Metadata) -> Self {
|
||||
self.endpoints.insert(Region::new(region), endpoint);
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct a Partition from the builder
|
||||
///
|
||||
/// Returns `None` if:
|
||||
/// - DefaultEndpoint is not set
|
||||
/// - DefaultEndpoint has an empty list of supported signature versions
|
||||
pub fn build(self) -> Option<Partition> {
|
||||
let default_endpoint = self.default_endpoint?;
|
||||
let endpoints = self.endpoints.into_iter().collect();
|
||||
Some(Partition {
|
||||
_id: self.id?,
|
||||
region_regex: self.region_regex?,
|
||||
partition_endpoint: self.partition_endpoint,
|
||||
regionalized: self.regionalized.unwrap_or_default(),
|
||||
default_endpoint,
|
||||
endpoints,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum Regionalized {
|
||||
Regionalized,
|
||||
NotRegionalized,
|
||||
}
|
||||
|
||||
impl Default for Regionalized {
|
||||
fn default() -> Self {
|
||||
Regionalized::Regionalized
|
||||
}
|
||||
}
|
||||
|
||||
impl Partition {
|
||||
pub fn can_resolve(&self, region: &Region) -> bool {
|
||||
self.region_regex.is_match(region.as_ref())
|
||||
}
|
||||
|
||||
pub fn builder() -> Builder {
|
||||
Builder::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveAwsEndpoint for Partition {
|
||||
fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> {
|
||||
if let Some(endpoint) = self.endpoints.get(region) {
|
||||
return endpoint.resolve_endpoint(region);
|
||||
}
|
||||
let resolved_region = match self.regionalized {
|
||||
Regionalized::NotRegionalized => self.partition_endpoint.as_ref(),
|
||||
Regionalized::Regionalized => Some(region),
|
||||
};
|
||||
let endpoint_for_region = resolved_region
|
||||
.and_then(|region| self.endpoints.get(region))
|
||||
.unwrap_or(&self.default_endpoint);
|
||||
endpoint_for_region.resolve_endpoint(region)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::partition::endpoint::Metadata;
|
||||
use crate::partition::endpoint::Protocol::{Http, Https};
|
||||
use crate::partition::endpoint::SignatureVersion::{self, V4};
|
||||
use crate::partition::{endpoint, Partition};
|
||||
use crate::partition::{PartitionResolver, Regionalized};
|
||||
use crate::{CredentialScope, ResolveAwsEndpoint};
|
||||
use aws_types::region::{Region, SigningRegion};
|
||||
use aws_types::SigningService;
|
||||
use http::Uri;
|
||||
|
||||
fn basic_partition() -> Partition {
|
||||
Partition::builder()
|
||||
.id("part-id-1")
|
||||
.region_regex(r#"^(us)-\w+-\d+$"#)
|
||||
.default_endpoint(endpoint::Metadata {
|
||||
uri_template: "service.{region}.amazonaws.com",
|
||||
protocol: Https,
|
||||
credential_scope: CredentialScope::default(),
|
||||
signature_versions: SignatureVersion::V4,
|
||||
})
|
||||
.partition_endpoint("")
|
||||
.regionalized(Regionalized::Regionalized)
|
||||
.endpoint(
|
||||
"us-west-1",
|
||||
endpoint::Metadata {
|
||||
uri_template: "service.{region}.amazonaws.com",
|
||||
protocol: Https,
|
||||
credential_scope: CredentialScope::default(),
|
||||
signature_versions: SignatureVersion::V4,
|
||||
},
|
||||
)
|
||||
.endpoint(
|
||||
"us-west-1-alt",
|
||||
Metadata {
|
||||
uri_template: "service-alt.us-west-1.amazonaws.com",
|
||||
protocol: Http,
|
||||
credential_scope: CredentialScope::builder()
|
||||
.region(SigningRegion::from_static("us-west-1"))
|
||||
.service(SigningService::from_static("foo"))
|
||||
.build(),
|
||||
signature_versions: V4,
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.expect("valid partition")
|
||||
}
|
||||
|
||||
fn global_partition() -> Partition {
|
||||
Partition::builder()
|
||||
.id("part-id-1")
|
||||
.region_regex(r#"^(cn)-\w+-\d+$"#)
|
||||
.default_endpoint(Metadata {
|
||||
uri_template: "service.{region}.amazonaws.com",
|
||||
protocol: Https,
|
||||
credential_scope: CredentialScope::builder()
|
||||
.service(SigningService::from_static("foo"))
|
||||
.build(),
|
||||
signature_versions: SignatureVersion::V4,
|
||||
})
|
||||
.partition_endpoint("partition")
|
||||
.regionalized(Regionalized::NotRegionalized)
|
||||
.endpoint(
|
||||
"partition",
|
||||
Metadata {
|
||||
uri_template: "some-global-thing.amazonaws.cn",
|
||||
protocol: Https,
|
||||
credential_scope: CredentialScope::builder()
|
||||
.region(SigningRegion::from_static("cn-east-1"))
|
||||
.service(SigningService::from_static("foo"))
|
||||
.build(),
|
||||
signature_versions: SignatureVersion::V4,
|
||||
},
|
||||
)
|
||||
.endpoint(
|
||||
"cn-fips-1",
|
||||
Metadata {
|
||||
uri_template: "fips.amazonaws.cn",
|
||||
protocol: Https,
|
||||
credential_scope: CredentialScope::builder()
|
||||
.region(SigningRegion::from_static("cn-fips"))
|
||||
.build(),
|
||||
signature_versions: SignatureVersion::V4,
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.expect("valid partition")
|
||||
}
|
||||
|
||||
fn partition_resolver() -> PartitionResolver {
|
||||
PartitionResolver::new(
|
||||
basic_partition(),
|
||||
vec![global_partition(), default_partition()],
|
||||
)
|
||||
}
|
||||
|
||||
fn default_partition() -> Partition {
|
||||
Partition::builder()
|
||||
.id("part-id-3")
|
||||
.region_regex(r#"^(eu)-\w+-\d+$"#)
|
||||
.default_endpoint(Metadata {
|
||||
uri_template: "service.{region}.amazonaws.com",
|
||||
protocol: Https,
|
||||
signature_versions: V4,
|
||||
credential_scope: CredentialScope::builder()
|
||||
.service(SigningService::from_static("foo"))
|
||||
.build(),
|
||||
})
|
||||
.build()
|
||||
.expect("valid partition")
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
region: &'static str,
|
||||
uri: &'static str,
|
||||
signing_region: &'static str,
|
||||
signing_service: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// Modeled region with no endpoint overrides
|
||||
const MODELED_REGION: TestCase = TestCase {
|
||||
region: "us-west-1",
|
||||
uri: "https://service.us-west-1.amazonaws.com",
|
||||
signing_region: "us-west-1",
|
||||
signing_service: None,
|
||||
};
|
||||
|
||||
/// Modeled region with endpoint overrides
|
||||
const MODELED_REGION_OVERRIDE: TestCase = TestCase {
|
||||
region: "us-west-1-alt",
|
||||
uri: "http://service-alt.us-west-1.amazonaws.com",
|
||||
signing_region: "us-west-1",
|
||||
signing_service: Some("foo"),
|
||||
};
|
||||
|
||||
/// Validates falling back onto the default endpoint
|
||||
const FALLBACK_REGION: TestCase = TestCase {
|
||||
region: "us-east-1",
|
||||
uri: "https://service.us-east-1.amazonaws.com",
|
||||
signing_region: "us-east-1",
|
||||
signing_service: None,
|
||||
};
|
||||
|
||||
/// Validates "PartitionName"
|
||||
const PARTITION_NAME: TestCase = TestCase {
|
||||
region: "cn-central-1",
|
||||
uri: "https://some-global-thing.amazonaws.cn",
|
||||
signing_region: "cn-east-1",
|
||||
signing_service: Some("foo"),
|
||||
};
|
||||
|
||||
/// Validates non-regionalized endpoints still use endpoints
|
||||
const NON_REGIONALIZED_EXACT_MATCH: TestCase = TestCase {
|
||||
region: "cn-fips-1",
|
||||
uri: "https://fips.amazonaws.cn",
|
||||
signing_region: "cn-fips",
|
||||
signing_service: None,
|
||||
};
|
||||
|
||||
const DEFAULT_ENDPOINT: TestCase = TestCase {
|
||||
region: "eu-west-1",
|
||||
uri: "https://service.eu-west-1.amazonaws.com",
|
||||
signing_region: "eu-west-1",
|
||||
signing_service: Some("foo"),
|
||||
};
|
||||
|
||||
const TEST_CASES: &[TestCase] = &[
|
||||
MODELED_REGION,
|
||||
MODELED_REGION_OVERRIDE,
|
||||
FALLBACK_REGION,
|
||||
PARTITION_NAME,
|
||||
DEFAULT_ENDPOINT,
|
||||
NON_REGIONALIZED_EXACT_MATCH,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn validate_basic_partition() {
|
||||
let p10n = basic_partition();
|
||||
check_endpoint(&p10n, &MODELED_REGION);
|
||||
check_endpoint(&p10n, &MODELED_REGION_OVERRIDE);
|
||||
check_endpoint(&p10n, &FALLBACK_REGION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_global_partition() {
|
||||
let partition = global_partition();
|
||||
check_endpoint(&partition, &PARTITION_NAME);
|
||||
check_endpoint(&partition, &NON_REGIONALIZED_EXACT_MATCH)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_default_endpoint() {
|
||||
check_endpoint(&default_partition(), &DEFAULT_ENDPOINT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_partition_resolver() {
|
||||
let resolver = partition_resolver();
|
||||
for test_case in TEST_CASES {
|
||||
check_endpoint(&resolver, test_case);
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn check_endpoint(resolver: &impl ResolveAwsEndpoint, test_case: &TestCase) {
|
||||
let endpoint = resolver
|
||||
.resolve_endpoint(&Region::new(test_case.region))
|
||||
.expect("valid region");
|
||||
let mut test_uri = Uri::from_static("/");
|
||||
endpoint.set_endpoint(&mut test_uri, None).unwrap();
|
||||
assert_eq!(test_uri, Uri::from_static(test_case.uri));
|
||||
assert_eq!(
|
||||
endpoint.credential_scope().region(),
|
||||
Some(&SigningRegion::from_static(test_case.signing_region))
|
||||
);
|
||||
assert_eq!(
|
||||
endpoint.credential_scope().service(),
|
||||
test_case
|
||||
.signing_service
|
||||
.map(SigningService::from_static)
|
||||
.as_ref()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -9,28 +9,24 @@ use std::fmt;
|
|||
use std::fmt::{Display, Formatter};
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
use aws_smithy_client::erase::DynConnector;
|
||||
use aws_smithy_client::test_connection::TestConnection;
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::operation;
|
||||
use aws_smithy_http::operation::Operation;
|
||||
use aws_smithy_http::response::ParseHttpResponse;
|
||||
use aws_smithy_types::endpoint::Endpoint;
|
||||
use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind};
|
||||
use bytes::Bytes;
|
||||
use http::header::{AUTHORIZATION, USER_AGENT};
|
||||
use http::{self, Uri};
|
||||
|
||||
use aws_endpoint::partition::endpoint::{Protocol, SignatureVersion};
|
||||
use aws_endpoint::{EndpointShim, Params};
|
||||
use aws_http::retry::AwsResponseRetryClassifier;
|
||||
use aws_http::user_agent::AwsUserAgent;
|
||||
use aws_inlineable::middleware::DefaultMiddleware;
|
||||
use aws_sig_auth::signer::OperationSigningConfig;
|
||||
use aws_smithy_client::erase::DynConnector;
|
||||
|
||||
use aws_smithy_client::test_connection::TestConnection;
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::endpoint::ResolveEndpoint;
|
||||
use aws_smithy_http::operation;
|
||||
use aws_smithy_http::operation::Operation;
|
||||
use aws_smithy_http::response::ParseHttpResponse;
|
||||
|
||||
use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind};
|
||||
use aws_types::credentials::SharedCredentialsProvider;
|
||||
use aws_types::region::Region;
|
||||
use aws_types::region::SigningRegion;
|
||||
use aws_types::Credentials;
|
||||
use aws_types::SigningService;
|
||||
|
||||
|
@ -79,20 +75,16 @@ impl ParseHttpResponse for TestOperationParser {
|
|||
fn test_operation() -> Operation<TestOperationParser, AwsResponseRetryClassifier> {
|
||||
let req = operation::Request::new(
|
||||
http::Request::builder()
|
||||
.uri("https://test-service.test-region.amazonaws.com/")
|
||||
.uri("/")
|
||||
.body(SdkBody::from("request body"))
|
||||
.unwrap(),
|
||||
)
|
||||
.augment(|req, conf| {
|
||||
conf.insert(
|
||||
EndpointShim::from_resolver(aws_endpoint::partition::endpoint::Metadata {
|
||||
uri_template: "test-service.{region}.amazonaws.com",
|
||||
protocol: Protocol::Https,
|
||||
credential_scope: Default::default(),
|
||||
signature_versions: SignatureVersion::V4,
|
||||
})
|
||||
.resolve_endpoint(&Params::new(Some(Region::new("test-region")))),
|
||||
);
|
||||
conf.insert(aws_smithy_http::endpoint::Result::Ok(
|
||||
Endpoint::builder()
|
||||
.url("https://test-service.test-region.amazonaws.com")
|
||||
.build(),
|
||||
));
|
||||
aws_http::auth::set_provider(
|
||||
conf,
|
||||
SharedCredentialsProvider::new(Credentials::new(
|
||||
|
@ -103,7 +95,7 @@ fn test_operation() -> Operation<TestOperationParser, AwsResponseRetryClassifier
|
|||
"test",
|
||||
)),
|
||||
);
|
||||
conf.insert(Region::new("test-region"));
|
||||
conf.insert(SigningRegion::from_static("test-region"));
|
||||
conf.insert(OperationSigningConfig::default_config());
|
||||
conf.insert(SigningService::from_static("test-service-signing"));
|
||||
conf.insert(UNIX_EPOCH + Duration::from_secs(1613414417));
|
||||
|
|
|
@ -20,6 +20,7 @@ tracing = "0.1"
|
|||
|
||||
[dev-dependencies]
|
||||
aws-endpoint = { path = "../aws-endpoint" }
|
||||
aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types"}
|
||||
tracing-test = "0.2.1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
|
|
@ -3,20 +3,23 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
use crate::signer::{
|
||||
OperationSigningConfig, RequestConfig, SigV4Signer, SigningError, SigningRequirements,
|
||||
};
|
||||
use aws_sigv4::http_request::SignableBody;
|
||||
use aws_smithy_http::middleware::MapRequest;
|
||||
use aws_smithy_http::operation::Request;
|
||||
use aws_smithy_http::property_bag::PropertyBag;
|
||||
use aws_types::region::SigningRegion;
|
||||
use aws_types::Credentials;
|
||||
use aws_types::SigningService;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use aws_smithy_http::middleware::MapRequest;
|
||||
use aws_smithy_http::operation::Request;
|
||||
use aws_smithy_http::property_bag::PropertyBag;
|
||||
|
||||
use aws_sigv4::http_request::SignableBody;
|
||||
use aws_types::region::SigningRegion;
|
||||
use aws_types::Credentials;
|
||||
use aws_types::SigningService;
|
||||
|
||||
use crate::signer::{
|
||||
OperationSigningConfig, RequestConfig, SigV4Signer, SigningError, SigningRequirements,
|
||||
};
|
||||
|
||||
/// Container for the request signature for use in the property bag.
|
||||
#[non_exhaustive]
|
||||
pub struct Signature(String);
|
||||
|
@ -186,22 +189,23 @@ impl MapRequest for SigV4SigningStage {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::convert::Infallible;
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::middleware::MapRequest;
|
||||
use aws_smithy_http::operation;
|
||||
use http::header::AUTHORIZATION;
|
||||
|
||||
use aws_endpoint::AwsAuthStage;
|
||||
use aws_types::region::{Region, SigningRegion};
|
||||
use aws_types::Credentials;
|
||||
use aws_types::SigningService;
|
||||
|
||||
use crate::middleware::{
|
||||
SigV4SigningStage, Signature, SigningStageError, SigningStageErrorKind,
|
||||
};
|
||||
use crate::signer::{OperationSigningConfig, SigV4Signer};
|
||||
use aws_endpoint::partition::endpoint::{Protocol, SignatureVersion};
|
||||
use aws_endpoint::{AwsAuthStage, Params};
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::endpoint::ResolveEndpoint;
|
||||
use aws_smithy_http::middleware::MapRequest;
|
||||
use aws_smithy_http::operation;
|
||||
use aws_types::region::{Region, SigningRegion};
|
||||
use aws_types::Credentials;
|
||||
use aws_types::SigningService;
|
||||
use http::header::AUTHORIZATION;
|
||||
use std::convert::Infallible;
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
#[test]
|
||||
fn places_signature_in_property_bag() {
|
||||
|
@ -233,29 +237,21 @@ mod test {
|
|||
// check that the endpoint middleware followed by signing middleware produce the expected result
|
||||
#[test]
|
||||
fn endpoint_plus_signer() {
|
||||
let provider = aws_endpoint::EndpointShim::from_resolver(
|
||||
aws_endpoint::partition::endpoint::Metadata {
|
||||
uri_template: "kinesis.{region}.amazonaws.com",
|
||||
protocol: Protocol::Https,
|
||||
credential_scope: Default::default(),
|
||||
signature_versions: SignatureVersion::V4,
|
||||
},
|
||||
);
|
||||
use aws_smithy_types::endpoint::Endpoint;
|
||||
let endpoint = Endpoint::builder()
|
||||
.url("https://kinesis.us-east-1.amazonaws.com")
|
||||
.build();
|
||||
let req = http::Request::builder()
|
||||
.uri("https://kinesis.us-east-1.amazonaws.com")
|
||||
.body(SdkBody::from(""))
|
||||
.unwrap();
|
||||
let region = Region::new("us-east-1");
|
||||
let region = SigningRegion::from_static("us-east-1");
|
||||
let req = operation::Request::new(req)
|
||||
.augment(|req, conf| {
|
||||
conf.insert(region.clone());
|
||||
conf.insert(UNIX_EPOCH + Duration::new(1611160427, 0));
|
||||
conf.insert(SigningService::from_static("kinesis"));
|
||||
conf.insert(
|
||||
provider
|
||||
.resolve_endpoint(&Params::new(Some(region.clone())))
|
||||
.unwrap(),
|
||||
);
|
||||
conf.insert(endpoint);
|
||||
Result::<_, Infallible>::Ok(req)
|
||||
})
|
||||
.expect("succeeds");
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
//! AWS SDK endpoint support.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use crate::region::{Region, SigningRegion};
|
||||
use crate::SigningService;
|
||||
|
|
|
@ -28,6 +28,7 @@ pub struct SdkConfig {
|
|||
credentials_provider: Option<SharedCredentialsProvider>,
|
||||
region: Option<Region>,
|
||||
endpoint_resolver: Option<Arc<dyn ResolveAwsEndpoint>>,
|
||||
endpoint_url: Option<String>,
|
||||
retry_config: Option<RetryConfig>,
|
||||
sleep_impl: Option<Arc<dyn AsyncSleep>>,
|
||||
timeout_config: Option<TimeoutConfig>,
|
||||
|
@ -45,6 +46,7 @@ pub struct Builder {
|
|||
credentials_provider: Option<SharedCredentialsProvider>,
|
||||
region: Option<Region>,
|
||||
endpoint_resolver: Option<Arc<dyn ResolveAwsEndpoint>>,
|
||||
endpoint_url: Option<String>,
|
||||
retry_config: Option<RetryConfig>,
|
||||
sleep_impl: Option<Arc<dyn AsyncSleep>>,
|
||||
timeout_config: Option<TimeoutConfig>,
|
||||
|
@ -88,6 +90,8 @@ impl Builder {
|
|||
|
||||
/// Set the endpoint resolver to use when making requests
|
||||
///
|
||||
/// This method is deprecated. Use [`Self::endpoint_url`] instead.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # fn wrapper() -> Result<(), aws_smithy_http::endpoint::error::InvalidEndpointError> {
|
||||
|
@ -100,6 +104,7 @@ impl Builder {
|
|||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[deprecated(note = "use `endpoint_url` instead")]
|
||||
pub fn endpoint_resolver(
|
||||
mut self,
|
||||
endpoint_resolver: impl ResolveAwsEndpoint + 'static,
|
||||
|
@ -108,6 +113,23 @@ impl Builder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the endpoint url to use when making requests.
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use aws_types::SdkConfig;
|
||||
/// let config = SdkConfig::builder().endpoint_url("http://localhost:8080").build();
|
||||
/// ```
|
||||
pub fn endpoint_url(mut self, endpoint_url: impl Into<String>) -> Self {
|
||||
self.set_endpoint_url(Some(endpoint_url.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the endpoint url to use when making requests.
|
||||
pub fn set_endpoint_url(&mut self, endpoint_url: Option<String>) -> &mut Self {
|
||||
self.endpoint_url = endpoint_url;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the endpoint resolver to use when making requests
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -446,6 +468,7 @@ impl Builder {
|
|||
credentials_provider: self.credentials_provider,
|
||||
region: self.region,
|
||||
endpoint_resolver: self.endpoint_resolver,
|
||||
endpoint_url: self.endpoint_url,
|
||||
retry_config: self.retry_config,
|
||||
sleep_impl: self.sleep_impl,
|
||||
timeout_config: self.timeout_config,
|
||||
|
@ -465,6 +488,11 @@ impl SdkConfig {
|
|||
self.endpoint_resolver.clone()
|
||||
}
|
||||
|
||||
/// Configured endpoint URL
|
||||
pub fn endpoint_url(&self) -> Option<&str> {
|
||||
self.endpoint_url.as_deref()
|
||||
}
|
||||
|
||||
/// Configured retry config
|
||||
pub fn retry_config(&self) -> Option<&RetryConfig> {
|
||||
self.retry_config.as_ref()
|
||||
|
|
|
@ -41,6 +41,7 @@ val allCodegenTests = listOf(
|
|||
CodegenTest(
|
||||
"com.amazonaws.apigateway#BackplaneControlService",
|
||||
"apigateway",
|
||||
imports = listOf("models/apigateway-rules.smithy"),
|
||||
extraConfig = """
|
||||
,
|
||||
"codegen": {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
$version: "1.0"
|
||||
|
||||
namespace com.amazonaws.apigateway
|
||||
|
||||
use smithy.rules#endpointRuleSet
|
||||
apply BackplaneControlService @endpointRuleSet({
|
||||
"version": "1.0",
|
||||
"rules": [{
|
||||
"type": "endpoint",
|
||||
"conditions": [],
|
||||
"endpoint": { "url": "https://www.example.com" }
|
||||
}],
|
||||
"parameters": {
|
||||
"Bucket": { "required": false, "type": "String" },
|
||||
"Region": { "required": false, "type": "String", "builtIn": "AWS::Region" },
|
||||
}
|
||||
})
|
|
@ -15,6 +15,7 @@ import software.amazon.smithy.rustsdk.customize.ec2.Ec2Decorator
|
|||
import software.amazon.smithy.rustsdk.customize.glacier.GlacierDecorator
|
||||
import software.amazon.smithy.rustsdk.customize.route53.Route53Decorator
|
||||
import software.amazon.smithy.rustsdk.customize.s3.S3Decorator
|
||||
import software.amazon.smithy.rustsdk.customize.s3control.S3ControlDecorator
|
||||
import software.amazon.smithy.rustsdk.customize.sts.STSDecorator
|
||||
|
||||
val DECORATORS: List<ClientCodegenDecorator> = listOf(
|
||||
|
@ -35,6 +36,7 @@ val DECORATORS: List<ClientCodegenDecorator> = listOf(
|
|||
AwsPresigningDecorator(),
|
||||
AwsReadmeDecorator(),
|
||||
HttpConnectorDecorator(),
|
||||
AwsEndpointsStdLib(),
|
||||
|
||||
// Service specific decorators
|
||||
ApiGatewayDecorator(),
|
||||
|
@ -43,6 +45,7 @@ val DECORATORS: List<ClientCodegenDecorator> = listOf(
|
|||
GlacierDecorator(),
|
||||
Route53Decorator(),
|
||||
S3Decorator(),
|
||||
S3ControlDecorator(),
|
||||
STSDecorator(),
|
||||
|
||||
// Only build docs-rs for linux to reduce load on docs.rs
|
||||
|
|
|
@ -5,57 +5,72 @@
|
|||
|
||||
package software.amazon.smithy.rustsdk
|
||||
|
||||
import software.amazon.smithy.aws.traits.ServiceTrait
|
||||
import software.amazon.smithy.codegen.core.CodegenException
|
||||
import software.amazon.smithy.model.node.Node
|
||||
import software.amazon.smithy.model.node.ObjectNode
|
||||
import software.amazon.smithy.model.node.StringNode
|
||||
import software.amazon.smithy.model.shapes.OperationShape
|
||||
import software.amazon.smithy.model.Model
|
||||
import software.amazon.smithy.model.shapes.ServiceShape
|
||||
import software.amazon.smithy.model.shapes.ShapeId
|
||||
import software.amazon.smithy.model.transform.ModelTransformer
|
||||
import software.amazon.smithy.rulesengine.language.EndpointRuleSet
|
||||
import software.amazon.smithy.rulesengine.language.syntax.parameters.Builtins
|
||||
import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameter
|
||||
import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameters
|
||||
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointCustomization
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointTypesGenerator
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.generators.EndpointsModule
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rust
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.withBlock
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.withBlockTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationCustomization
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationSection
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsCustomization
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsSection
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.generators.operationBuildError
|
||||
import software.amazon.smithy.rust.codegen.core.util.dq
|
||||
import software.amazon.smithy.rust.codegen.core.util.expectTrait
|
||||
import software.amazon.smithy.rust.codegen.core.util.orNull
|
||||
import kotlin.io.path.readText
|
||||
import software.amazon.smithy.rust.codegen.core.util.letIf
|
||||
|
||||
class AwsEndpointDecorator : ClientCodegenDecorator {
|
||||
override val name: String = "AwsEndpoint"
|
||||
override val order: Byte = 0
|
||||
override val order: Byte = -100
|
||||
|
||||
private var endpointsCache: ObjectNode? = null
|
||||
|
||||
private fun endpoints(sdkSettings: SdkSettings): ObjectNode {
|
||||
if (endpointsCache == null) {
|
||||
val endpointsJson = when (val path = sdkSettings.endpointsConfigPath) {
|
||||
null -> (
|
||||
javaClass.getResource("/default-sdk-endpoints.json")
|
||||
?: throw IllegalStateException("Failed to find default-sdk-endpoints.json in the JAR")
|
||||
).readText()
|
||||
else -> path.readText()
|
||||
}
|
||||
endpointsCache = Node.parse(endpointsJson).expectObjectNode()
|
||||
override fun transformModel(service: ServiceShape, model: Model): Model {
|
||||
val customServices = setOf(
|
||||
ShapeId.from("com.amazonaws.s3#AmazonS3"),
|
||||
ShapeId.from("com.amazonaws.s3control#AWSS3ControlServiceV20180820"),
|
||||
ShapeId.from("com.amazonaws.codecatalyst#CodeCatalyst"),
|
||||
)
|
||||
if (customServices.contains(service.id)) {
|
||||
return model
|
||||
}
|
||||
// currently, most models incorrectly model region is optional when it is actually required—fix these models:
|
||||
return ModelTransformer.create().mapTraits(model) { _, trait ->
|
||||
when (trait) {
|
||||
is EndpointRuleSetTrait -> {
|
||||
val epRules = EndpointRuleSet.fromNode(trait.ruleSet)
|
||||
val newParameters = Parameters.builder()
|
||||
epRules.parameters.toList()
|
||||
.map { param ->
|
||||
param.letIf(param.builtIn == Builtins.REGION.builtIn) {
|
||||
it.toBuilder().required(true).build()
|
||||
}
|
||||
}
|
||||
.forEach(newParameters::addParameter)
|
||||
|
||||
val newTrait = epRules.toBuilder().parameters(
|
||||
newParameters.build(),
|
||||
).build()
|
||||
EndpointRuleSetTrait.builder().ruleSet(newTrait.toNode()).build()
|
||||
}
|
||||
|
||||
else -> trait
|
||||
}
|
||||
}
|
||||
return endpointsCache!!
|
||||
}
|
||||
|
||||
override fun configCustomizations(
|
||||
|
@ -64,135 +79,149 @@ class AwsEndpointDecorator : ClientCodegenDecorator {
|
|||
): List<ConfigCustomization> {
|
||||
return baseCustomizations + EndpointConfigCustomization(
|
||||
codegenContext,
|
||||
endpoints(SdkSettings.from(codegenContext.settings)),
|
||||
)
|
||||
}
|
||||
|
||||
override fun operationCustomizations(
|
||||
codegenContext: ClientCodegenContext,
|
||||
operation: OperationShape,
|
||||
baseCustomizations: List<OperationCustomization>,
|
||||
): List<OperationCustomization> {
|
||||
return baseCustomizations + EndpointResolverFeature(codegenContext.runtimeConfig)
|
||||
}
|
||||
|
||||
override fun libRsCustomizations(
|
||||
codegenContext: ClientCodegenContext,
|
||||
baseCustomizations: List<LibRsCustomization>,
|
||||
): List<LibRsCustomization> {
|
||||
return baseCustomizations + PubUseEndpoint(codegenContext.runtimeConfig)
|
||||
}
|
||||
|
||||
override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) {
|
||||
val epTypes = EndpointTypesGenerator.fromContext(codegenContext)
|
||||
if (epTypes.defaultResolver() == null) {
|
||||
throw CodegenException(
|
||||
"${codegenContext.serviceShape} did not provide endpoint rules. " +
|
||||
"This is a bug and the generated client will not work. All AWS services MUST define endpoint rules.",
|
||||
)
|
||||
}
|
||||
// generate a region converter if params has a region
|
||||
if (!epTypes.params.toList().any { it.builtIn == Builtins.REGION.builtIn }) {
|
||||
println("not generating a resolver for ${codegenContext.serviceShape}")
|
||||
return
|
||||
}
|
||||
rustCrate.withModule(EndpointsModule) {
|
||||
// TODO(https://github.com/awslabs/smithy-rs/issues/1784) cleanup task
|
||||
rustTemplate(
|
||||
"""
|
||||
/// Temporary shim to allow new and old endpoint resolvers to co-exist
|
||||
///
|
||||
/// This enables converting from the actual parameters type to the placeholder parameters type that
|
||||
/// contains a region
|
||||
##[doc(hidden)]
|
||||
impl From<#{Params}> for #{PlaceholderParams} {
|
||||
fn from(params: #{Params}) -> Self {
|
||||
Self::new(params.region().map(|r|#{Region}::new(r.to_string())))
|
||||
}
|
||||
}
|
||||
""",
|
||||
"Params" to epTypes.paramsStruct(),
|
||||
"Region" to AwsRuntimeType.awsTypes(codegenContext.runtimeConfig).resolve("region::Region"),
|
||||
"PlaceholderParams" to AwsRuntimeType.awsEndpoint(codegenContext.runtimeConfig).resolve("Params"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun endpointCustomizations(codegenContext: ClientCodegenContext): List<EndpointCustomization> {
|
||||
return listOf(object : EndpointCustomization {
|
||||
override fun builtInDefaultValue(parameter: Parameter, configRef: String): Writable? {
|
||||
return when (parameter.builtIn) {
|
||||
Builtins.SDK_ENDPOINT.builtIn -> writable { rust("$configRef.endpoint_url().map(|url|url.to_string())") }
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EndpointConfigCustomization(
|
||||
private val codegenContext: CodegenContext,
|
||||
private val endpointData: ObjectNode,
|
||||
codegenContext: CodegenContext,
|
||||
) :
|
||||
ConfigCustomization() {
|
||||
private val runtimeConfig = codegenContext.runtimeConfig
|
||||
private val resolveAwsEndpoint = AwsRuntimeType.awsEndpoint(runtimeConfig).resolve("ResolveAwsEndpoint")
|
||||
private val endpointShim = AwsRuntimeType.awsEndpoint(runtimeConfig).resolve("EndpointShim")
|
||||
private val moduleUseName = codegenContext.moduleUseName()
|
||||
private val codegenScope = arrayOf(
|
||||
"SmithyResolver" to RuntimeType.smithyHttp(runtimeConfig).resolve("endpoint::ResolveEndpoint"),
|
||||
"PlaceholderParams" to AwsRuntimeType.awsEndpoint(runtimeConfig).resolve("Params"),
|
||||
"ResolveAwsEndpoint" to AwsRuntimeType.awsEndpoint(runtimeConfig).resolve("ResolveAwsEndpoint"),
|
||||
"EndpointShim" to AwsRuntimeType.awsEndpoint(runtimeConfig).resolve("EndpointShim"),
|
||||
"ResolveAwsEndpoint" to resolveAwsEndpoint,
|
||||
"EndpointShim" to endpointShim,
|
||||
"aws_types" to AwsRuntimeType.awsTypes(runtimeConfig),
|
||||
)
|
||||
|
||||
override fun section(section: ServiceConfig): Writable = writable {
|
||||
when (section) {
|
||||
is ServiceConfig.ConfigStruct -> rustTemplate(
|
||||
"pub (crate) endpoint_resolver: std::sync::Arc<dyn #{SmithyResolver}<#{PlaceholderParams}>>,",
|
||||
ServiceConfig.BuilderImpl -> rustTemplate(
|
||||
"""
|
||||
/// Overrides the endpoint resolver to use when making requests.
|
||||
///
|
||||
/// This method is deprecated, use [`Builder::endpoint_url`] or [`Builder::endpoint_resolver`] instead.
|
||||
///
|
||||
/// When unset, the client will used a generated endpoint resolver based on the endpoint metadata
|
||||
/// for `$moduleUseName`.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```no_run
|
||||
/// ## fn wrapper() -> Result<(), aws_smithy_http::endpoint::error::InvalidEndpointError> {
|
||||
/// use #{aws_types}::region::Region;
|
||||
/// use $moduleUseName::config::{Builder, Config};
|
||||
/// use $moduleUseName::Endpoint;
|
||||
///
|
||||
/// let config = $moduleUseName::Config::builder()
|
||||
/// .endpoint_resolver(Endpoint::immutable("http://localhost:8080")?)
|
||||
/// .build();
|
||||
/// ## Ok(())
|
||||
/// ## }
|
||||
/// ```
|
||||
##[deprecated(note = "use endpoint_url or set the endpoint resolver directly")]
|
||||
pub fn aws_endpoint_resolver(mut self, endpoint_resolver: impl #{ResolveAwsEndpoint} + 'static) -> Self {
|
||||
self.endpoint_resolver = Some(std::sync::Arc::new(#{EndpointShim}::from_resolver(endpoint_resolver)) as _);
|
||||
self
|
||||
}
|
||||
|
||||
##[deprecated(note = "use endpoint_url or set the endpoint resolver directly")]
|
||||
/// Sets the endpoint resolver to use when making requests.
|
||||
///
|
||||
/// This method is deprecated, use [`Builder::endpoint_url`] or [`Builder::endpoint_resolver`] instead.
|
||||
pub fn set_aws_endpoint_resolver(&mut self, endpoint_resolver: Option<std::sync::Arc<dyn #{ResolveAwsEndpoint}>>) -> &mut Self {
|
||||
self.endpoint_resolver = endpoint_resolver.map(|res|std::sync::Arc::new(#{EndpointShim}::from_arc(res) ) as _);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the endpoint url used to communicate with this service
|
||||
///
|
||||
/// Note: this is used in combination with other endpoint rules, e.g. an API that applies a host-label prefix
|
||||
/// will be prefixed onto this URL. To fully override the endpoint resolver, use
|
||||
/// [`Builder::endpoint_resolver`].
|
||||
pub fn endpoint_url(mut self, endpoint_url: impl Into<String>) -> Self {
|
||||
self.endpoint_url = Some(endpoint_url.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the endpoint url used to communicate with this service
|
||||
///
|
||||
/// Note: this is used in combination with other endpoint rules, e.g. an API that applies a host-label prefix
|
||||
/// will be prefixed onto this URL. To fully override the endpoint resolver, use
|
||||
/// [`Builder::endpoint_resolver`].
|
||||
pub fn set_endpoint_url(&mut self, endpoint_url: Option<String>) -> &mut Self {
|
||||
self.endpoint_url = endpoint_url;
|
||||
self
|
||||
}
|
||||
""",
|
||||
*codegenScope,
|
||||
)
|
||||
is ServiceConfig.ConfigImpl -> emptySection
|
||||
// TODO(https://github.com/awslabs/smithy-rs/issues/1780): Uncomment once endpoints 2.0 project is completed
|
||||
// rustTemplate(
|
||||
// """
|
||||
// /// Returns the endpoint resolver.
|
||||
// pub fn endpoint_resolver(&self) -> std::sync::Arc<dyn #{SmithyResolver}<#{PlaceholderParams}>> {
|
||||
// self.endpoint_resolver.clone()
|
||||
// }
|
||||
// """,
|
||||
// *codegenScope,
|
||||
// )
|
||||
is ServiceConfig.BuilderStruct ->
|
||||
rustTemplate("endpoint_resolver: Option<std::sync::Arc<dyn #{SmithyResolver}<#{PlaceholderParams}>>>,", *codegenScope)
|
||||
ServiceConfig.BuilderImpl ->
|
||||
rustTemplate(
|
||||
"""
|
||||
/// Overrides the endpoint resolver to use when making requests.
|
||||
///
|
||||
/// When unset, the client will used a generated endpoint resolver based on the endpoint metadata
|
||||
/// for `$moduleUseName`.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```no_run
|
||||
/// ## fn wrapper() -> Result<(), aws_smithy_http::endpoint::error::InvalidEndpointError> {
|
||||
/// use #{aws_types}::region::Region;
|
||||
/// use $moduleUseName::config::{Builder, Config};
|
||||
/// use $moduleUseName::Endpoint;
|
||||
///
|
||||
/// let config = $moduleUseName::Config::builder()
|
||||
/// .endpoint_resolver(Endpoint::immutable("http://localhost:8080")?)
|
||||
/// .build();
|
||||
/// ## Ok(())
|
||||
/// ## }
|
||||
/// ```
|
||||
pub fn endpoint_resolver(mut self, endpoint_resolver: impl #{ResolveAwsEndpoint} + 'static) -> Self {
|
||||
self.endpoint_resolver = Some(std::sync::Arc::new(#{EndpointShim}::from_resolver(endpoint_resolver)) as _);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the endpoint resolver to use when making requests.
|
||||
pub fn set_endpoint_resolver(&mut self, endpoint_resolver: Option<std::sync::Arc<dyn #{ResolveAwsEndpoint}>>) -> &mut Self {
|
||||
self.endpoint_resolver = endpoint_resolver.map(|res|std::sync::Arc::new(#{EndpointShim}::from_arc(res) ) as _);
|
||||
self
|
||||
}
|
||||
""",
|
||||
*codegenScope,
|
||||
)
|
||||
|
||||
ServiceConfig.BuilderBuild -> {
|
||||
val resolverGenerator = EndpointResolverGenerator(codegenContext, endpointData)
|
||||
rustTemplate(
|
||||
"""
|
||||
endpoint_resolver: self.endpoint_resolver.unwrap_or_else(||
|
||||
std::sync::Arc::new(#{EndpointShim}::from_resolver(#{Resolver}()))
|
||||
),
|
||||
""",
|
||||
*codegenScope, "Resolver" to resolverGenerator.resolver(),
|
||||
)
|
||||
ServiceConfig.BuilderBuild -> rust("endpoint_url: self.endpoint_url")
|
||||
ServiceConfig.BuilderStruct -> rust("endpoint_url: Option<String>")
|
||||
ServiceConfig.ConfigImpl -> {
|
||||
Attribute.AllowDeadCode.render(this)
|
||||
rust("pub(crate) fn endpoint_url(&self) -> Option<&str> { self.endpoint_url.as_deref() }")
|
||||
}
|
||||
|
||||
else -> emptySection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EndpointResolverFeature(runtimeConfig: RuntimeConfig) :
|
||||
OperationCustomization() {
|
||||
private val placeholderEndpointParams = AwsRuntimeType.awsEndpoint(runtimeConfig).resolve("Params")
|
||||
private val codegenScope = arrayOf(
|
||||
"PlaceholderParams" to placeholderEndpointParams,
|
||||
"BuildError" to runtimeConfig.operationBuildError(),
|
||||
)
|
||||
override fun section(section: OperationSection): Writable {
|
||||
return when (section) {
|
||||
is OperationSection.MutateRequest -> writable {
|
||||
// insert the endpoint resolution _result_ into the bag (note that this won't bail if endpoint resolution failed)
|
||||
rustTemplate(
|
||||
"""
|
||||
let endpoint_params = #{PlaceholderParams}::new(${section.config}.region.clone());
|
||||
${section.request}.properties_mut()
|
||||
.insert::<aws_smithy_http::endpoint::Result>(
|
||||
${section.config}.endpoint_resolver.resolve_endpoint(&endpoint_params)
|
||||
);
|
||||
""",
|
||||
*codegenScope,
|
||||
)
|
||||
}
|
||||
else -> emptySection
|
||||
ServiceConfig.ConfigStruct -> rust("endpoint_url: Option<String>")
|
||||
ServiceConfig.ConfigStructAdditionalDocs -> emptySection
|
||||
ServiceConfig.Extras -> emptySection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -206,223 +235,8 @@ class PubUseEndpoint(private val runtimeConfig: RuntimeConfig) : LibRsCustomizat
|
|||
CargoDependency.smithyHttp(runtimeConfig).toType(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> emptySection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EndpointResolverGenerator(codegenContext: CodegenContext, private val endpointData: ObjectNode) {
|
||||
private val runtimeConfig = codegenContext.runtimeConfig
|
||||
private val endpointPrefix = codegenContext.serviceShape.expectTrait<ServiceTrait>().endpointPrefix
|
||||
private val awsEndpoint = AwsRuntimeType.awsEndpoint(runtimeConfig)
|
||||
private val awsTypes = AwsRuntimeType.awsTypes(runtimeConfig)
|
||||
private val codegenScope =
|
||||
arrayOf(
|
||||
"Partition" to awsEndpoint.resolve("Partition"),
|
||||
"endpoint" to awsEndpoint.resolve("partition::endpoint"),
|
||||
"CredentialScope" to awsEndpoint.resolve("CredentialScope"),
|
||||
"Regionalized" to awsEndpoint.resolve("partition::Regionalized"),
|
||||
"Protocol" to awsEndpoint.resolve("partition::endpoint::Protocol"),
|
||||
"SignatureVersion" to awsEndpoint.resolve("partition::endpoint::SignatureVersion"),
|
||||
"PartitionResolver" to awsEndpoint.resolve("PartitionResolver"),
|
||||
"ResolveAwsEndpoint" to awsEndpoint.resolve("ResolveAwsEndpoint"),
|
||||
"SigningService" to awsTypes.resolve("SigningService"),
|
||||
"SigningRegion" to awsTypes.resolve("region::SigningRegion"),
|
||||
)
|
||||
|
||||
fun resolver(): RuntimeType {
|
||||
val partitionsData = endpointData.expectArrayMember("partitions").getElementsAs(Node::expectObjectNode)
|
||||
|
||||
val partitions = partitionsData.map {
|
||||
PartitionNode(endpointPrefix, it)
|
||||
}.sortedWith { x, y ->
|
||||
// always put the aws constructor first
|
||||
if (x.id == "aws") {
|
||||
-1
|
||||
} else {
|
||||
x.id.compareTo(y.id)
|
||||
}
|
||||
}
|
||||
val base = partitions.first()
|
||||
val rest = partitions.drop(1)
|
||||
val fnName = "endpoint_resolver"
|
||||
return RuntimeType.forInlineFun(fnName, RustModule.private("aws_endpoint")) {
|
||||
rustBlockTemplate("pub fn $fnName() -> impl #{ResolveAwsEndpoint}", *codegenScope) {
|
||||
withBlockTemplate("#{PartitionResolver}::new(", ")", *codegenScope) {
|
||||
renderPartition(base)
|
||||
rust(",")
|
||||
withBlock("vec![", "]") {
|
||||
rest.forEach {
|
||||
renderPartition(it)
|
||||
rust(",")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun RustWriter.renderPartition(partition: PartitionNode) {
|
||||
/* Example:
|
||||
Partition::builder()
|
||||
.id("part-id-3")
|
||||
.region_regex(r#"^(eu)-\w+-\d+$"#)
|
||||
.default_endpoint(endpoint::Metadata {
|
||||
uri_template: "service.{region}.amazonaws.com",
|
||||
protocol: Https,
|
||||
signature_versions: &[V4],
|
||||
credential_scope: CredentialScope::builder()
|
||||
.service("foo")
|
||||
.build()
|
||||
})
|
||||
.endpoint(...)
|
||||
.build()
|
||||
.expect("valid partition")
|
||||
*/
|
||||
rustTemplate(
|
||||
"""
|
||||
#{Partition}::builder()
|
||||
.id(${partition.id.dq()})
|
||||
.region_regex(r##"${partition.regionRegex}"##)""",
|
||||
*codegenScope,
|
||||
)
|
||||
withBlock(".default_endpoint(", ")") {
|
||||
with(partition.defaults) {
|
||||
render()
|
||||
}
|
||||
}
|
||||
partition.partitionEndpoint?.also { ep ->
|
||||
rust(".partition_endpoint(${ep.dq()})")
|
||||
}
|
||||
when (partition.regionalized) {
|
||||
true -> rustTemplate(".regionalized(#{Regionalized}::Regionalized)", *codegenScope)
|
||||
false -> rustTemplate(".regionalized(#{Regionalized}::NotRegionalized)", *codegenScope)
|
||||
}
|
||||
partition.endpoints.forEach { (region, endpoint) ->
|
||||
withBlock(".endpoint(${region.dq()}, ", ")") {
|
||||
with(endpoint) {
|
||||
render()
|
||||
}
|
||||
}
|
||||
}
|
||||
rust(""".build().expect("invalid partition")""")
|
||||
}
|
||||
|
||||
inner class EndpointMeta(private val endpoint: ObjectNode, service: String, dnsSuffix: String) {
|
||||
private val uriTemplate =
|
||||
(endpoint.getStringMember("hostname").orNull() ?: throw CodegenException("endpoint must be defined"))
|
||||
.value
|
||||
.replace("{service}", service)
|
||||
.replace("{dnsSuffix}", dnsSuffix)
|
||||
private val credentialScope =
|
||||
CredentialScope(endpoint.getObjectMember("credentialScope").orElse(Node.objectNode()))
|
||||
|
||||
private fun protocol(): String {
|
||||
val protocols = endpoint.expectArrayMember("protocols").map { it.expectStringNode().value }
|
||||
return if (protocols.contains("https")) {
|
||||
"Https"
|
||||
} else if (protocols.contains("http")) {
|
||||
"Http"
|
||||
} else {
|
||||
throw CodegenException("No protocol supported")
|
||||
}
|
||||
}
|
||||
|
||||
private fun signatureVersion(): String {
|
||||
val signatureVersions = endpoint.expectArrayMember("signatureVersions").map { it.expectStringNode().value }
|
||||
// TODO(https://github.com/awslabs/smithy-rs/issues/977): we can use this to change the signing options instead of customizing S3 specifically
|
||||
if (!(signatureVersions.contains("v4") || signatureVersions.contains("s3v4"))) {
|
||||
throw CodegenException("endpoint does not support sigv4, unsupported: $signatureVersions")
|
||||
}
|
||||
return "V4"
|
||||
}
|
||||
|
||||
fun RustWriter.render() {
|
||||
rustBlockTemplate("#{endpoint}::Metadata", *codegenScope) {
|
||||
rust("uri_template: ${uriTemplate.dq()},")
|
||||
rustTemplate("protocol: #{Protocol}::${protocol()},", *codegenScope)
|
||||
rustTemplate("signature_versions: #{SignatureVersion}::${signatureVersion()},", *codegenScope)
|
||||
withBlock("credential_scope: ", ",") {
|
||||
with(credentialScope) {
|
||||
render()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a partition from endpoints.json
|
||||
*/
|
||||
private inner class PartitionNode(endpointPrefix: String, config: ObjectNode) {
|
||||
// the partition id/name (e.g. "aws")
|
||||
val id: String = config.expectStringMember("partition").value
|
||||
|
||||
// the node associated with [endpointPrefix] (or empty node)
|
||||
val service: ObjectNode = config
|
||||
.getObjectMember("services").orElse(Node.objectNode())
|
||||
.getObjectMember(endpointPrefix).orElse(Node.objectNode())
|
||||
|
||||
// endpoints belonging to the service with the given [endpointPrefix] (or empty node)
|
||||
|
||||
val dnsSuffix: String = config.expectStringMember("dnsSuffix").value
|
||||
|
||||
// service specific defaults
|
||||
val defaults: EndpointMeta
|
||||
|
||||
val endpoints: List<Pair<String, EndpointMeta>>
|
||||
|
||||
init {
|
||||
|
||||
val partitionDefaults = config.expectObjectMember("defaults")
|
||||
val serviceDefaults = service.getObjectMember("defaults").orElse(Node.objectNode())
|
||||
val mergedDefaults = partitionDefaults.merge(serviceDefaults)
|
||||
endpoints = service.getObjectMember("endpoints").orElse(Node.objectNode()).members.mapNotNull { (k, v) ->
|
||||
val endpointObject = mergedDefaults.merge(v.expectObjectNode())
|
||||
// There is no point in generating lots of endpoints that are just empty
|
||||
if (endpointObject != mergedDefaults) {
|
||||
k.value to EndpointMeta(endpointObject, endpointPrefix, dnsSuffix)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
defaults = EndpointMeta(mergedDefaults, endpointPrefix, dnsSuffix)
|
||||
}
|
||||
|
||||
val regionalized: Boolean = service.getBooleanMemberOrDefault("isRegionalized", true)
|
||||
|
||||
// regionalized services always use regionalized endpoints
|
||||
val partitionEndpoint: String? = if (regionalized) {
|
||||
null
|
||||
} else {
|
||||
service.getStringMember("partitionEndpoint").map(StringNode::getValue).orNull()
|
||||
}
|
||||
|
||||
val regionRegex: String = config.expectStringMember("regionRegex").value
|
||||
}
|
||||
|
||||
inner class CredentialScope(private val objectNode: ObjectNode) {
|
||||
fun RustWriter.render() {
|
||||
rustTemplate(
|
||||
"""
|
||||
#{CredentialScope}::builder()
|
||||
""",
|
||||
*codegenScope,
|
||||
)
|
||||
objectNode.getStringMember("service").map {
|
||||
rustTemplate(
|
||||
".service(${it.value.dq()})",
|
||||
*codegenScope,
|
||||
)
|
||||
}
|
||||
objectNode.getStringMember("region").map {
|
||||
rustTemplate(
|
||||
".region(${it.value.dq()})",
|
||||
*codegenScope,
|
||||
)
|
||||
}
|
||||
rust(".build()")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,15 +105,14 @@ class RegionDecorator : ClientCodegenDecorator {
|
|||
return listOf(
|
||||
object : EndpointCustomization {
|
||||
override fun builtInDefaultValue(parameter: Parameter, configRef: String): Writable? {
|
||||
return when (parameter) {
|
||||
Builtins.REGION -> writable { rust("$configRef.region.as_ref().map(|r|r.as_ref().to_owned())") }
|
||||
return when (parameter.builtIn) {
|
||||
Builtins.REGION.builtIn -> writable { rust("$configRef.region.as_ref().map(|r|r.as_ref().to_owned())") }
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun setBuiltInOnConfig(name: String, value: Node, configBuilderRef: String): Writable? {
|
||||
if (name != Builtins.REGION.builtIn.get()) {
|
||||
println("not handling: $name")
|
||||
return null
|
||||
}
|
||||
return writable {
|
||||
|
|
|
@ -45,7 +45,8 @@ class SdkConfigDecorator : ClientCodegenDecorator {
|
|||
fn from(input: &#{SdkConfig}) -> Self {
|
||||
let mut builder = Builder::default();
|
||||
builder = builder.region(input.region().cloned());
|
||||
builder.set_endpoint_resolver(input.endpoint_resolver().clone());
|
||||
builder.set_aws_endpoint_resolver(input.endpoint_resolver().clone());
|
||||
builder.set_endpoint_url(input.endpoint_url().map(|url|url.to_string()));
|
||||
builder.set_retry_config(input.retry_config().cloned());
|
||||
builder.set_timeout_config(input.timeout_config().cloned());
|
||||
builder.set_sleep_impl(input.sleep_impl());
|
||||
|
|
|
@ -68,7 +68,7 @@ class S3Decorator : ClientCodegenDecorator {
|
|||
logger.info("Adding AllowInvalidXmlRoot trait to $it")
|
||||
(it as StructureShape).toBuilder().addTrait(AllowInvalidXmlRoot()).build()
|
||||
}
|
||||
}
|
||||
}.let(StripBucketFromHttpPath()::transform)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,6 +130,7 @@ class S3PubUse : LibRsCustomization() {
|
|||
AwsRuntimeType.S3Errors,
|
||||
)
|
||||
}
|
||||
|
||||
else -> emptySection
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rustsdk.customize.s3
|
||||
|
||||
import software.amazon.smithy.model.Model
|
||||
import software.amazon.smithy.model.pattern.UriPattern
|
||||
import software.amazon.smithy.model.shapes.StructureShape
|
||||
import software.amazon.smithy.model.traits.HttpTrait
|
||||
import software.amazon.smithy.model.transform.ModelTransformer
|
||||
import software.amazon.smithy.rust.codegen.core.util.letIf
|
||||
|
||||
class StripBucketFromHttpPath {
|
||||
private val transformer = ModelTransformer.create()
|
||||
fun transform(model: Model): Model {
|
||||
// Remove `/{Bucket}` from the path (http trait)
|
||||
// The endpoints 2.0 rules handle either placing the bucket into the virtual host or adding it to the path
|
||||
return transformer.mapTraits(model) { shape, trait ->
|
||||
when (trait) {
|
||||
is HttpTrait -> {
|
||||
val appliedToOperation = shape
|
||||
.asOperationShape()
|
||||
.map { operation ->
|
||||
model.expectShape(operation.inputShape, StructureShape::class.java)
|
||||
.getMember("Bucket").isPresent
|
||||
}.orElse(false)
|
||||
trait.letIf(appliedToOperation) {
|
||||
it.toBuilder().uri(UriPattern.parse(transformUri(trait.uri.toString()))).build()
|
||||
}
|
||||
}
|
||||
|
||||
else -> trait
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun transformUri(uri: String): String {
|
||||
if (!uri.startsWith("/{Bucket}")) {
|
||||
throw IllegalStateException("tried to transform `$uri` that was not a standard bucket URI")
|
||||
}
|
||||
val withoutBucket = uri.replace("/{Bucket}", "")
|
||||
return if (!withoutBucket.startsWith("/")) {
|
||||
"/$withoutBucket"
|
||||
} else {
|
||||
withoutBucket
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rustsdk.customize.s3control
|
||||
|
||||
import software.amazon.smithy.model.Model
|
||||
import software.amazon.smithy.model.shapes.ServiceShape
|
||||
import software.amazon.smithy.model.shapes.ShapeId
|
||||
import software.amazon.smithy.model.traits.EndpointTrait
|
||||
import software.amazon.smithy.model.transform.ModelTransformer
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
|
||||
|
||||
class S3ControlDecorator : ClientCodegenDecorator {
|
||||
override val name: String = "S3Control"
|
||||
override val order: Byte = 0
|
||||
private fun applies(service: ServiceShape) =
|
||||
service.id == ShapeId.from("com.amazonaws.s3control#AWSS3ControlServiceV20180820")
|
||||
|
||||
override fun transformModel(service: ServiceShape, model: Model): Model {
|
||||
if (!applies(service)) {
|
||||
return model
|
||||
}
|
||||
return ModelTransformer.create()
|
||||
.removeTraitsIf(model) { _, trait ->
|
||||
trait is EndpointTrait && trait.hostPrefix.labels.any {
|
||||
it.isLabel && it.content == "AccountId"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rustsdk
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import software.amazon.smithy.model.node.Node
|
||||
import software.amazon.smithy.model.node.ObjectNode
|
||||
import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
|
||||
import java.io.File
|
||||
|
||||
internal class EndpointConfigCustomizationTest {
|
||||
private val placeholderEndpointParams = AwsRuntimeType.awsEndpoint(AwsTestRuntimeConfig).resolve("Params")
|
||||
private val codegenScope = arrayOf(
|
||||
"http" to RuntimeType.Http,
|
||||
"PlaceholderParams" to placeholderEndpointParams,
|
||||
"aws_types" to AwsRuntimeType.awsTypes(AwsTestRuntimeConfig),
|
||||
)
|
||||
|
||||
private val model = """
|
||||
namespace test
|
||||
use aws.protocols#restJson1
|
||||
|
||||
@title("test")
|
||||
@restJson1
|
||||
@aws.api#service(sdkId: "Test", endpointPrefix: "service-with-prefix")
|
||||
service TestService {
|
||||
version: "123",
|
||||
operations: [Nop]
|
||||
}
|
||||
|
||||
@http(uri: "/foo", method: "GET")
|
||||
operation Nop {
|
||||
}
|
||||
|
||||
@aws.api#service(sdkId: "Test", endpointPrefix: "iam")
|
||||
@title("test")
|
||||
@restJson1
|
||||
service NoRegions {
|
||||
version: "123",
|
||||
operations: [Nop]
|
||||
}
|
||||
|
||||
@aws.api#service(sdkId: "Test")
|
||||
@title("test")
|
||||
@restJson1
|
||||
service NoEndpointPrefix {
|
||||
version: "123",
|
||||
operations: [Nop]
|
||||
}
|
||||
""".asSmithyModel()
|
||||
|
||||
private val endpointConfig = """
|
||||
{
|
||||
"partitions" : [ {
|
||||
"defaults" : {
|
||||
"hostname" : "{service}.{region}.{dnsSuffix}",
|
||||
"protocols" : [ "https" ],
|
||||
"signatureVersions" : [ "v4" ]
|
||||
},
|
||||
"dnsSuffix" : "amazonaws.com",
|
||||
"partition" : "aws",
|
||||
"partitionName" : "AWS Standard",
|
||||
"regionRegex" : "^(us|eu|ap|sa|ca|me|af)\\-\\w+\\-\\d+${'$'}",
|
||||
"regions" : {
|
||||
"af-south-1" : {
|
||||
"description" : "Africa (Cape Town)"
|
||||
},
|
||||
"us-west-2" : {
|
||||
"description" : "US West (Oregon)"
|
||||
}
|
||||
},
|
||||
"services" : {
|
||||
"service-with-prefix" : {
|
||||
"endpoints" : {
|
||||
"fips-ca-central-1" : {
|
||||
"credentialScope" : {
|
||||
"region" : "ca-central-1"
|
||||
},
|
||||
"hostname" : "access-analyzer-fips.ca-central-1.amazonaws.com"
|
||||
},
|
||||
"fips-us-west-1" : {
|
||||
"credentialScope" : {
|
||||
"region" : "us-west-1"
|
||||
},
|
||||
"hostname" : "access-analyzer-fips.us-west-1.amazonaws.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"iam" : {
|
||||
"endpoints" : {
|
||||
"aws-global" : {
|
||||
"credentialScope" : {
|
||||
"region" : "us-east-1"
|
||||
},
|
||||
"hostname" : "iam.amazonaws.com"
|
||||
},
|
||||
"iam-fips" : {
|
||||
"credentialScope" : {
|
||||
"region" : "us-east-1"
|
||||
},
|
||||
"hostname" : "iam-fips.amazonaws.com"
|
||||
}
|
||||
},
|
||||
"isRegionalized" : false,
|
||||
"partitionEndpoint" : "aws-global"
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
""".let { ObjectNode.parse(it).expectObjectNode() }
|
||||
|
||||
private fun validateEndpointCustomizationForService(service: String, test: ((RustCrate) -> Unit)? = null) {
|
||||
val endpointsFile = File.createTempFile("endpoints", ".json")
|
||||
endpointsFile.writeText(Node.printJson(endpointConfig))
|
||||
clientIntegrationTest(
|
||||
model,
|
||||
listOf(),
|
||||
service = service,
|
||||
runtimeConfig = AwsTestRuntimeConfig,
|
||||
additionalSettings = ObjectNode.builder()
|
||||
.withMember(
|
||||
"customizationConfig",
|
||||
ObjectNode.builder()
|
||||
.withMember(
|
||||
"awsSdk",
|
||||
ObjectNode.builder()
|
||||
.withMember("integrationTestPath", "../sdk/integration-tests")
|
||||
.withMember("endpointsConfigPath", endpointsFile.absolutePath)
|
||||
.build(),
|
||||
).build(),
|
||||
)
|
||||
.withMember("codegen", ObjectNode.builder().withMember("includeFluentClient", false).build()).build(),
|
||||
) { _, rustCrate ->
|
||||
if (test != null) {
|
||||
test(rustCrate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generates valid code`() {
|
||||
validateEndpointCustomizationForService("test#TestService")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generates valid code when no endpoint prefix is provided`() {
|
||||
validateEndpointCustomizationForService("test#NoEndpointPrefix")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `support region-specific endpoint overrides`() {
|
||||
validateEndpointCustomizationForService("test#TestService") { crate ->
|
||||
crate.lib {
|
||||
unitTest("region_override") {
|
||||
rustTemplate(
|
||||
"""
|
||||
let conf = crate::config::Config::builder().build();
|
||||
let endpoint = conf.endpoint_resolver
|
||||
.resolve_endpoint(&::#{PlaceholderParams}::new(Some(#{aws_types}::region::Region::new("fips-ca-central-1")))).expect("default resolver produces a valid endpoint");
|
||||
assert_eq!(endpoint.url(), "https://access-analyzer-fips.ca-central-1.amazonaws.com/");
|
||||
""",
|
||||
*codegenScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `support region-agnostic services`() {
|
||||
validateEndpointCustomizationForService("test#NoRegions") { crate ->
|
||||
crate.lib {
|
||||
unitTest("global_services") {
|
||||
rustTemplate(
|
||||
"""
|
||||
let conf = crate::config::Config::builder().build();
|
||||
let endpoint = conf.endpoint_resolver
|
||||
.resolve_endpoint(&::#{PlaceholderParams}::new(Some(#{aws_types}::region::Region::new("us-east-1")))).expect("default resolver produces a valid endpoint");
|
||||
assert_eq!(endpoint.url(), "https://iam.amazonaws.com/");
|
||||
|
||||
let endpoint = conf.endpoint_resolver
|
||||
.resolve_endpoint(&::#{PlaceholderParams}::new(Some(#{aws_types}::region::Region::new("iam-fips")))).expect("default resolver produces a valid endpoint");
|
||||
assert_eq!(endpoint.url(), "https://iam-fips.amazonaws.com/");
|
||||
""",
|
||||
*codegenScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -109,7 +109,7 @@ apply PutBucketLifecycleConfiguration @httpRequestTests([
|
|||
documentation: "This test validates that the content md5 header is set correctly",
|
||||
method: "PUT",
|
||||
protocol: "aws.protocols#restXml",
|
||||
uri: "/test-bucket",
|
||||
uri: "/",
|
||||
headers: {
|
||||
// we can assert this, but when this test is promoted, it can't assert
|
||||
// on the exact contents
|
||||
|
@ -151,7 +151,7 @@ apply CreateMultipartUpload @httpRequestTests([
|
|||
documentation: "This test validates that the URI for CreateMultipartUpload is created correctly",
|
||||
method: "POST",
|
||||
protocol: "aws.protocols#restXml",
|
||||
uri: "/test-bucket/object.txt",
|
||||
uri: "/object.txt",
|
||||
queryParams: [
|
||||
"uploads",
|
||||
"x-id=CreateMultipartUpload"
|
||||
|
@ -176,7 +176,7 @@ apply PutObject @httpRequestTests([
|
|||
documentation: "This test validates that if a content-type is specified, that only one content-type header is sent",
|
||||
method: "PUT",
|
||||
protocol: "aws.protocols#restXml",
|
||||
uri: "/test-bucket/test-key",
|
||||
uri: "/test-key",
|
||||
headers: { "content-type": "text/html" },
|
||||
params: {
|
||||
Bucket: "test-bucket",
|
||||
|
@ -196,7 +196,7 @@ apply PutObject @httpRequestTests([
|
|||
documentation: "This test validates that if a content-length is specified, that only one content-length header is sent",
|
||||
method: "PUT",
|
||||
protocol: "aws.protocols#restXml",
|
||||
uri: "/test-bucket/test-key",
|
||||
uri: "/test-key",
|
||||
headers: { "content-length": "2" },
|
||||
params: {
|
||||
Bucket: "test-bucket",
|
||||
|
@ -221,7 +221,7 @@ apply HeadObject @httpRequestTests([
|
|||
|
||||
method: "HEAD",
|
||||
protocol: "aws.protocols#restXml",
|
||||
uri: "/test-bucket/%3C%3E%20%60%3F%F0%9F%90%B1",
|
||||
uri: "/%3C%3E%20%60%3F%F0%9F%90%B1",
|
||||
params: {
|
||||
Bucket: "test-bucket",
|
||||
Key: "<> `?🐱",
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
*/
|
||||
|
||||
use aws_sdk_dynamodb::{Credentials, Region};
|
||||
use aws_smithy_http::endpoint::Endpoint;
|
||||
use http::Uri;
|
||||
|
||||
/// Iterative test of loading clients from shared configuration
|
||||
|
@ -14,7 +13,7 @@ async fn endpoints_can_be_overridden_globally() {
|
|||
let shared_config = aws_types::SdkConfig::builder()
|
||||
.region(Region::new("us-east-4"))
|
||||
.http_connector(conn)
|
||||
.endpoint_resolver(Endpoint::immutable("http://localhost:8000").expect("valid endpoint"))
|
||||
.endpoint_url("http://localhost:8000")
|
||||
.build();
|
||||
let conf = aws_sdk_dynamodb::config::Builder::from(&shared_config)
|
||||
.credentials_provider(Credentials::new("asdf", "asdf", None, None, "test"))
|
||||
|
@ -36,7 +35,7 @@ async fn endpoints_can_be_overridden_locally() {
|
|||
.build();
|
||||
let conf = aws_sdk_dynamodb::config::Builder::from(&shared_config)
|
||||
.credentials_provider(Credentials::new("asdf", "asdf", None, None, "test"))
|
||||
.endpoint_resolver(Endpoint::immutable("http://localhost:8000").expect("valid endpoint"))
|
||||
.endpoint_url("http://localhost:8000")
|
||||
.build();
|
||||
let svc = aws_sdk_dynamodb::Client::from_conf(conf);
|
||||
let _ = svc.list_tables().send().await;
|
||||
|
|
|
@ -16,11 +16,12 @@ async fn shared_config_testbed() {
|
|||
let conf = aws_sdk_dynamodb::config::Builder::from(&shared_config)
|
||||
.credentials_provider(Credentials::new("asdf", "asdf", None, None, "test"))
|
||||
.http_connector(conn)
|
||||
.endpoint_url("http://localhost:8000")
|
||||
.build();
|
||||
let svc = aws_sdk_dynamodb::Client::from_conf(conf);
|
||||
let _ = svc.list_tables().send().await;
|
||||
assert_eq!(
|
||||
request.expect_request().uri(),
|
||||
&Uri::from_static("https://dynamodb.us-east-4.amazonaws.com")
|
||||
&Uri::from_static("http://localhost:8000")
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
use aws_sdk_iam::{Credentials, Region};
|
||||
use aws_smithy_client::test_connection::capture_request;
|
||||
|
||||
// this test is ignored because pseudoregions have been removed. This test should be re-enabled
|
||||
// once FIPS support is added in aws-config
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn correct_endpoint_resolver() {
|
||||
let (conn, request) = capture_request(None);
|
||||
let conf = aws_sdk_iam::Config::builder()
|
||||
|
|
|
@ -9,6 +9,7 @@ use aws_sdk_s3::{model::ChecksumAlgorithm, output::GetObjectOutput, Client, Cred
|
|||
use aws_smithy_client::test_connection::{capture_request, TestConnection};
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_types::credentials::SharedCredentialsProvider;
|
||||
use http::header::AUTHORIZATION;
|
||||
use http::{HeaderValue, Uri};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
|
@ -30,7 +31,7 @@ fn new_checksum_validated_response_test_connection(
|
|||
.header("x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
||||
.header("x-amz-user-agent", "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0")
|
||||
.header("authorization", "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-checksum-mode;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=eb9e58fa4fb04c8e6f160705017fdbb497ccff0efee4227b3a56f900006c3882")
|
||||
.uri(Uri::from_static("https://s3.us-east-1.amazonaws.com/some-test-bucket/test.txt?x-id=GetObject")).body(SdkBody::empty()).unwrap(),
|
||||
.uri(Uri::from_static("https://some-test-bucket.s3.us-east-1.amazonaws.com/test.txt?x-id=GetObject")).body(SdkBody::empty()).unwrap(),
|
||||
http::Response::builder()
|
||||
.header("x-amz-request-id", "4B4NGF0EAWN0GE63")
|
||||
.header("content-length", "11")
|
||||
|
@ -90,7 +91,10 @@ async fn test_checksum_on_streaming_response(
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
conn.assert_requests_match(&[http::header::HeaderName::from_static("x-amz-checksum-mode")]);
|
||||
conn.assert_requests_match(&[
|
||||
http::header::HeaderName::from_static("x-amz-checksum-mode"),
|
||||
AUTHORIZATION,
|
||||
]);
|
||||
|
||||
res
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ use std::net::SocketAddr;
|
|||
use std::sync::Arc;
|
||||
|
||||
use aws_sdk_s3::Client;
|
||||
use aws_smithy_http::endpoint::Endpoint;
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
use aws_types::credentials::SharedCredentialsProvider;
|
||||
use aws_types::region::Region;
|
||||
|
@ -47,9 +46,7 @@ async fn test_concurrency_on_multi_thread_against_dummy_server() {
|
|||
"test",
|
||||
)))
|
||||
.region(Region::new("us-east-1"))
|
||||
.endpoint_resolver(
|
||||
Endpoint::immutable(format!("http://{server_addr}")).expect("valid endpoint"),
|
||||
)
|
||||
.endpoint_url(format!("http://{server_addr}"))
|
||||
.build();
|
||||
|
||||
test_concurrency(sdk_config).await;
|
||||
|
@ -68,9 +65,7 @@ async fn test_concurrency_on_single_thread_against_dummy_server() {
|
|||
"test",
|
||||
)))
|
||||
.region(Region::new("us-east-1"))
|
||||
.endpoint_resolver(
|
||||
Endpoint::immutable(format!("http://{server_addr}")).expect("valid endpoint"),
|
||||
)
|
||||
.endpoint_url(format!("http://{server_addr}"))
|
||||
.build();
|
||||
|
||||
test_concurrency(sdk_config).await;
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
use aws_config::SdkConfig;
|
||||
use aws_sdk_s3::config::Builder;
|
||||
use aws_sdk_s3::{Client, Credentials, Region};
|
||||
use aws_smithy_client::test_connection::capture_request;
|
||||
use aws_types::credentials::SharedCredentialsProvider;
|
||||
|
||||
#[tokio::test]
|
||||
async fn virtual_hosted_buckets() {
|
||||
let (conn, captured_request) = capture_request(None);
|
||||
let sdk_config = SdkConfig::builder()
|
||||
.credentials_provider(SharedCredentialsProvider::new(Credentials::new(
|
||||
"ANOTREAL",
|
||||
"notrealrnrELgWzOk3IfjzDKtFBhDby",
|
||||
Some("notarealsessiontoken".to_string()),
|
||||
None,
|
||||
"test",
|
||||
)))
|
||||
.region(Region::new("us-west-4"))
|
||||
.http_connector(conn.clone())
|
||||
.build();
|
||||
let client = Client::new(&sdk_config);
|
||||
let _ = client.list_objects_v2().bucket("test-bucket").send().await;
|
||||
assert_eq!(
|
||||
captured_request.expect_request().uri().to_string(),
|
||||
"https://test-bucket.s3.us-west-4.amazonaws.com/?list-type=2"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn force_path_style() {
|
||||
let (conn, captured_request) = capture_request(None);
|
||||
let sdk_config = SdkConfig::builder()
|
||||
.credentials_provider(SharedCredentialsProvider::new(Credentials::new(
|
||||
"ANOTREAL",
|
||||
"notrealrnrELgWzOk3IfjzDKtFBhDby",
|
||||
Some("notarealsessiontoken".to_string()),
|
||||
None,
|
||||
"test",
|
||||
)))
|
||||
.region(Region::new("us-west-4"))
|
||||
.http_connector(conn.clone())
|
||||
.build();
|
||||
let force_path_style =
|
||||
Client::from_conf(Builder::from(&sdk_config).force_path_style(true).build());
|
||||
let _ = force_path_style
|
||||
.list_objects_v2()
|
||||
.bucket("test-bucket")
|
||||
.send()
|
||||
.await;
|
||||
assert_eq!(
|
||||
captured_request.expect_request().uri().to_string(),
|
||||
"https://s3.us-west-4.amazonaws.com/test-bucket/?list-type=2"
|
||||
);
|
||||
}
|
|
@ -8,6 +8,7 @@ use aws_sdk_s3::{model::ObjectAttributes, Client, Credentials, Region};
|
|||
use aws_smithy_client::test_connection::TestConnection;
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_types::{credentials::SharedCredentialsProvider, SdkConfig};
|
||||
use http::header::AUTHORIZATION;
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
time::{Duration, UNIX_EPOCH},
|
||||
|
@ -25,7 +26,7 @@ async fn ignore_invalid_xml_body_root() {
|
|||
.header("authorization", "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-object-attributes;x-amz-security-token;x-amz-user-agent, Signature=0e6ec749db5a0af07890a83f553319eda95be0e498d058c64880471a474c5378")
|
||||
.header("x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
||||
.header("x-amz-security-token", "notarealsessiontoken")
|
||||
.uri(http::Uri::from_static("https://s3.us-east-1.amazonaws.com/some-test-bucket/test.txt?attributes"))
|
||||
.uri(http::Uri::from_static("https://some-test-bucket.s3.us-east-1.amazonaws.com/test.txt?attributes"))
|
||||
.body(SdkBody::empty())
|
||||
.unwrap(),
|
||||
http::Response::builder()
|
||||
|
@ -76,5 +77,5 @@ async fn ignore_invalid_xml_body_root() {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
conn.assert_requests_match(&[]);
|
||||
conn.assert_requests_match(&[AUTHORIZATION]);
|
||||
}
|
||||
|
|
|
@ -63,8 +63,11 @@ async fn test_s3_signer_with_naughty_string_metadata() {
|
|||
.region(Region::new("us-east-1"))
|
||||
.http_connector(conn.clone())
|
||||
.build();
|
||||
let config = aws_sdk_s3::config::Builder::from(&sdk_config)
|
||||
.force_path_style(true)
|
||||
.build();
|
||||
|
||||
let client = Client::new(&sdk_config);
|
||||
let client = Client::from_conf(config);
|
||||
let mut builder = client
|
||||
.put_object()
|
||||
.bucket("test-bucket")
|
||||
|
|
|
@ -44,7 +44,7 @@ async fn test_operation_should_not_normalize_uri_path() {
|
|||
.insert(UNIX_EPOCH + Duration::from_secs(1669257290));
|
||||
op.properties_mut().insert(AwsUserAgent::for_tests());
|
||||
|
||||
Result::Ok::<_, Infallible>(op)
|
||||
Ok::<_, Infallible>(op)
|
||||
})
|
||||
.unwrap()
|
||||
.send()
|
||||
|
@ -56,10 +56,10 @@ async fn test_operation_should_not_normalize_uri_path() {
|
|||
std::str::from_utf8(request.headers().get("authorization").unwrap().as_bytes()).unwrap();
|
||||
|
||||
let actual_uri = request.uri().path();
|
||||
let expected_uri = format!("/{}/a/.././b.txt", bucket_name);
|
||||
let expected_uri = "/a/.././b.txt";
|
||||
assert_eq!(actual_uri, expected_uri);
|
||||
|
||||
let expected_sig = "Signature=65001f8822b83876a9f6f8666a417582bb00641af3b91fb13f240b0f36c094f8";
|
||||
let expected_sig = "Signature=4803b8b8c794b5ecc055933befd7c5547f8bf6585bb18e4ae33ff65220d5cdd7";
|
||||
assert!(
|
||||
actual_auth.contains(expected_sig),
|
||||
"authorization header signature did not match expected signature: expected {} but not found in {}",
|
||||
|
|
|
@ -54,8 +54,12 @@ async fn test_presigning() -> Result<(), Box<dyn Error>> {
|
|||
let mut query_params: Vec<&str> = query.split('&').collect();
|
||||
query_params.sort();
|
||||
|
||||
assert_eq!(
|
||||
"test-bucket.s3.us-east-1.amazonaws.com",
|
||||
presigned.uri().authority().unwrap()
|
||||
);
|
||||
assert_eq!("GET", presigned.method().as_str());
|
||||
assert_eq!("/test-bucket/test-key", path);
|
||||
assert_eq!("/test-key", path);
|
||||
assert_eq!(
|
||||
&[
|
||||
"X-Amz-Algorithm=AWS4-HMAC-SHA256",
|
||||
|
@ -63,7 +67,7 @@ async fn test_presigning() -> Result<(), Box<dyn Error>> {
|
|||
"X-Amz-Date=20090213T233131Z",
|
||||
"X-Amz-Expires=30",
|
||||
"X-Amz-Security-Token=notarealsessiontoken",
|
||||
"X-Amz-Signature=b5a3e99da3c8b5ba152d828105afe8efb6ecb2732b5b5175a693fc3902d709c5",
|
||||
"X-Amz-Signature=758353318739033a850182c7b3435076eebbbd095f8dcf311383a6a1e124c4cb",
|
||||
"X-Amz-SignedHeaders=host",
|
||||
"x-id=GetObject"
|
||||
][..],
|
||||
|
@ -89,8 +93,12 @@ async fn test_presigning_with_payload_headers() -> Result<(), Box<dyn Error>> {
|
|||
let mut query_params: Vec<&str> = query.split('&').collect();
|
||||
query_params.sort();
|
||||
|
||||
assert_eq!(
|
||||
"test-bucket.s3.us-east-1.amazonaws.com",
|
||||
presigned.uri().authority().unwrap()
|
||||
);
|
||||
assert_eq!("PUT", presigned.method().as_str());
|
||||
assert_eq!("/test-bucket/test-key", path);
|
||||
assert_eq!("/test-key", path);
|
||||
assert_eq!(
|
||||
&[
|
||||
"X-Amz-Algorithm=AWS4-HMAC-SHA256",
|
||||
|
@ -98,7 +106,7 @@ async fn test_presigning_with_payload_headers() -> Result<(), Box<dyn Error>> {
|
|||
"X-Amz-Date=20090213T233131Z",
|
||||
"X-Amz-Expires=30",
|
||||
"X-Amz-Security-Token=notarealsessiontoken",
|
||||
"X-Amz-Signature=6a22b8bf422d17fe25e7d9fcbd26df31397ca5e3ad07d1cec95326ffdbe4a0a2",
|
||||
"X-Amz-Signature=be1d41dc392f7019750e4f5e577234fb9059dd20d15f6a99734196becce55e52",
|
||||
"X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost",
|
||||
"x-id=PutObject"
|
||||
][..],
|
||||
|
@ -124,7 +132,7 @@ async fn test_presigned_upload_part() -> Result<(), Box<dyn Error>> {
|
|||
.build()?);
|
||||
assert_eq!(
|
||||
presigned.uri().to_string(),
|
||||
"https://s3.us-east-1.amazonaws.com/bucket/key?x-id=UploadPart&partNumber=0&uploadId=upload-id&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ANOTREAL%2F20090213%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20090213T233131Z&X-Amz-Expires=30&X-Amz-SignedHeaders=content-length%3Bhost&X-Amz-Signature=59777f7ddd2f324dfe0749685e06b978433d03e6f090dceb96eb23cc9540c30c&X-Amz-Security-Token=notarealsessiontoken"
|
||||
"https://bucket.s3.us-east-1.amazonaws.com/key?x-id=UploadPart&partNumber=0&uploadId=upload-id&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ANOTREAL%2F20090213%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20090213T233131Z&X-Amz-Expires=30&X-Amz-SignedHeaders=content-length%3Bhost&X-Amz-Signature=a702867244f0bd1fb4d161e2a062520dcbefae3b9992d2e5366bcd61a60c6ddd&X-Amz-Security-Token=notarealsessiontoken",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ async fn test_s3_signer_query_string_with_all_valid_chars() {
|
|||
|
||||
// This is a snapshot test taken from a known working test result
|
||||
let snapshot_signature =
|
||||
"Signature=775f88213304a5233ff295f869571554140e3db171a2d4a64f63902c49f79880";
|
||||
"Signature=647aa91c7f91f1f1c498ef376fea370b48d0cd8c80a53c8e2cd64e3fc527a5e0";
|
||||
assert!(
|
||||
auth_header
|
||||
.to_str()
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"action": {
|
||||
"Request": {
|
||||
"request": {
|
||||
"uri": "https://s3.us-east-2.amazonaws.com/aws-rust-sdk/sample_data.csv?select&select-type=2&x-id=SelectObjectContent",
|
||||
"uri": "https://aws-rust-sdk.s3.us-east-2.amazonaws.com/sample_data.csv?select&select-type=2&x-id=SelectObjectContent",
|
||||
"headers": {
|
||||
"x-amz-date": [
|
||||
"20211126T205841Z"
|
||||
|
|
|
@ -16,8 +16,8 @@ use std::time::{Duration, UNIX_EPOCH};
|
|||
async fn test_signer() {
|
||||
let conn = TestConnection::new(vec![(
|
||||
http::Request::builder()
|
||||
.header("authorization", "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=6233614b69271e15db079287874a654183916e509909b5719b00cd8d5f31299e")
|
||||
.uri("https://s3.us-east-1.amazonaws.com/test-bucket?list-type=2&prefix=prefix~")
|
||||
.header("authorization", "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=ae78f74d26b6b0c3a403d9e8cc7ec3829d6264a2b33db672bf2b151bbb901786")
|
||||
.uri("https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=prefix~")
|
||||
.body(SdkBody::empty())
|
||||
.unwrap(),
|
||||
http::Response::builder().status(200).body("").unwrap(),
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
use aws_config::SdkConfig;
|
||||
use aws_sdk_s3::{Client, Credentials, Endpoint, Region};
|
||||
use aws_sdk_s3::{Client, Credentials, Region};
|
||||
use aws_smithy_types::error::display::DisplayErrorContext;
|
||||
use aws_types::credentials::SharedCredentialsProvider;
|
||||
use bytes::BytesMut;
|
||||
|
@ -30,9 +30,7 @@ async fn test_streaming_response_fails_when_eof_comes_before_content_length_reac
|
|||
"test",
|
||||
)))
|
||||
.region(Region::new("us-east-1"))
|
||||
.endpoint_resolver(
|
||||
Endpoint::immutable(format!("http://{server_addr}")).expect("valid endpoint"),
|
||||
)
|
||||
.endpoint_url(format!("http://{server_addr}"))
|
||||
.build();
|
||||
|
||||
let client = Client::new(&sdk_config);
|
||||
|
|
|
@ -8,7 +8,7 @@ use aws_sdk_s3::model::{
|
|||
CompressionType, CsvInput, CsvOutput, ExpressionType, FileHeaderInfo, InputSerialization,
|
||||
OutputSerialization,
|
||||
};
|
||||
use aws_sdk_s3::{Client, Credentials, Endpoint, Region};
|
||||
use aws_sdk_s3::{Client, Credentials, Region};
|
||||
use aws_smithy_async::assert_elapsed;
|
||||
use aws_smithy_async::rt::sleep::{default_async_sleep, TokioSleep};
|
||||
use aws_smithy_client::never::NeverConnector;
|
||||
|
@ -103,9 +103,7 @@ async fn test_read_timeout() {
|
|||
.read_timeout(Duration::from_millis(300))
|
||||
.build(),
|
||||
)
|
||||
.endpoint_resolver(
|
||||
Endpoint::immutable(format!("http://{server_addr}")).expect("valid endpoint"),
|
||||
)
|
||||
.endpoint_url(format!("http://{server_addr}"))
|
||||
.region(Some(Region::from_static("us-east-1")))
|
||||
.credentials_provider(SharedCredentialsProvider::new(Credentials::new(
|
||||
"test", "test", None, None, "test",
|
||||
|
@ -147,12 +145,9 @@ async fn test_connect_timeout() {
|
|||
.connect_timeout(Duration::from_millis(300))
|
||||
.build(),
|
||||
)
|
||||
.endpoint_resolver(
|
||||
Endpoint::immutable(
|
||||
// Emulate a connect timeout error by hitting an unroutable IP
|
||||
"http://172.255.255.0:18104",
|
||||
)
|
||||
.expect("valid endpoint"),
|
||||
.endpoint_url(
|
||||
// Emulate a connect timeout error by hitting an unroutable IP
|
||||
"http://172.255.255.0:18104",
|
||||
)
|
||||
.region(Some(Region::from_static("us-east-1")))
|
||||
.credentials_provider(SharedCredentialsProvider::new(Credentials::new(
|
||||
|
|
|
@ -13,7 +13,11 @@ use aws.protocols#awsJson1_1
|
|||
@awsJson1_1
|
||||
@endpointRuleSet({
|
||||
"version": "1.0",
|
||||
"rules": [],
|
||||
"rules": [{
|
||||
"type": "endpoint",
|
||||
"conditions": [],
|
||||
"endpoint": { "url": "https://www.example.com" }
|
||||
}],
|
||||
"parameters": {
|
||||
"Bucket": { "required": false, "type": "String" },
|
||||
"Region": { "required": false, "type": "String", "builtIn": "AWS::Region" },
|
||||
|
|
|
@ -70,12 +70,14 @@ class ClientCodegenVisitor(
|
|||
nullabilityCheckMode = NullableIndex.CheckMode.CLIENT_ZERO_VALUE_V1,
|
||||
)
|
||||
val baseModel = baselineTransform(context.model)
|
||||
val service = settings.getService(baseModel)
|
||||
val untransformedService = settings.getService(baseModel)
|
||||
val (protocol, generator) = ClientProtocolLoader(
|
||||
codegenDecorator.protocols(service.id, ClientProtocolLoader.DefaultProtocols),
|
||||
).protocolFor(context.model, service)
|
||||
codegenDecorator.protocols(untransformedService.id, ClientProtocolLoader.DefaultProtocols),
|
||||
).protocolFor(context.model, untransformedService)
|
||||
protocolGeneratorFactory = generator
|
||||
model = codegenDecorator.transformModel(service, baseModel)
|
||||
model = codegenDecorator.transformModel(untransformedService, baseModel)
|
||||
// the model transformer _might_ change the service shape
|
||||
val service = settings.getService(model)
|
||||
symbolProvider = RustClientCodegenPlugin.baseSymbolProvider(model, service, symbolVisitorConfig)
|
||||
|
||||
codegenContext = ClientCodegenContext(model, symbolProvider, service, protocol, settings, codegenDecorator)
|
||||
|
|
|
@ -14,6 +14,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegen
|
|||
import software.amazon.smithy.rust.codegen.client.smithy.customize.CombinedClientCodegenDecorator
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.customize.NoOpEventStreamSigningDecorator
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.customize.RequiredCustomizations
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointsDecorator
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientDecorator
|
||||
import software.amazon.smithy.rust.codegen.client.testutil.DecoratableBuildPlugin
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.NonExhaustive
|
||||
|
@ -52,6 +53,7 @@ class RustClientCodegenPlugin : DecoratableBuildPlugin() {
|
|||
ClientCustomizations(),
|
||||
RequiredCustomizations(),
|
||||
FluentClientDecorator(),
|
||||
EndpointsDecorator(),
|
||||
NoOpEventStreamSigningDecorator(),
|
||||
*decorator,
|
||||
)
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
package software.amazon.smithy.rust.codegen.client.smithy.endpoint
|
||||
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.generators.EndpointsModule
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
|
||||
|
||||
/**
|
||||
* Customization which injects an Endpoints 2.0 Endpoint Resolver into the service config struct
|
||||
|
@ -124,10 +126,28 @@ internal class EndpointConfigCustomization(
|
|||
"DefaultResolver" to defaultResolver,
|
||||
)
|
||||
} else {
|
||||
val alwaysFailsResolver = RuntimeType.forInlineFun("MissingResolver", EndpointsModule) {
|
||||
rustTemplate(
|
||||
"""
|
||||
pub(crate) struct MissingResolver;
|
||||
impl<T> #{ResolveEndpoint}<T> for MissingResolver {
|
||||
fn resolve_endpoint(&self, _params: &T) -> #{Result} {
|
||||
Err(#{ResolveEndpointError}::message("an endpoint resolver must be provided."))
|
||||
}
|
||||
}
|
||||
""",
|
||||
"ResolveEndpoint" to types.resolveEndpoint,
|
||||
"ResolveEndpointError" to types.resolveEndpointError,
|
||||
"Result" to types.smithyHttpEndpointModule.resolve("Result"),
|
||||
)
|
||||
}
|
||||
// To keep this diff under control, rather than `.expect` here, insert a resolver that will
|
||||
// always fail. In the future, this will be changed to an `expect()`
|
||||
rustTemplate(
|
||||
"""
|
||||
endpoint_resolver: self.endpoint_resolver.expect("an endpoint resolver must be provided")
|
||||
endpoint_resolver: self.endpoint_resolver.unwrap_or_else(||std::sync::Arc::new(#{FailingResolver})),
|
||||
""",
|
||||
"FailingResolver" to alwaysFailsResolver,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,9 +23,13 @@ internal class EndpointRulesetIndex : KnowledgeIndex {
|
|||
|
||||
fun endpointRulesForService(serviceShape: ServiceShape) = ruleSets.computeIfAbsent(
|
||||
serviceShape,
|
||||
) { serviceShape.getTrait<EndpointRuleSetTrait>()?.ruleSet?.let { EndpointRuleSet.fromNode(it) }?.also { it.typecheck() } }
|
||||
) {
|
||||
serviceShape.getTrait<EndpointRuleSetTrait>()?.ruleSet?.let { EndpointRuleSet.fromNode(it) }
|
||||
?.also { it.typecheck() }
|
||||
}
|
||||
|
||||
fun endpointTests(serviceShape: ServiceShape) = serviceShape.getTrait<EndpointTestsTrait>()?.testCases ?: emptyList()
|
||||
fun endpointTests(serviceShape: ServiceShape) =
|
||||
serviceShape.getTrait<EndpointTestsTrait>()?.testCases ?: emptyList()
|
||||
|
||||
companion object {
|
||||
fun of(model: Model): EndpointRulesetIndex {
|
||||
|
|
|
@ -33,19 +33,20 @@ class EndpointTypesGenerator(
|
|||
.flatMap { it.customRuntimeFunctions(codegenContext) }
|
||||
|
||||
companion object {
|
||||
fun fromContext(codegenContext: ClientCodegenContext): EndpointTypesGenerator? {
|
||||
fun fromContext(codegenContext: ClientCodegenContext): EndpointTypesGenerator {
|
||||
val index = EndpointRulesetIndex.of(codegenContext.model)
|
||||
val rulesOrNull = index.endpointRulesForService(codegenContext.serviceShape)
|
||||
return rulesOrNull?.let { rules ->
|
||||
EndpointTypesGenerator(codegenContext, rules, index.endpointTests(codegenContext.serviceShape))
|
||||
}
|
||||
return EndpointTypesGenerator(codegenContext, rulesOrNull, index.endpointTests(codegenContext.serviceShape))
|
||||
}
|
||||
}
|
||||
|
||||
fun paramsStruct(): RuntimeType = EndpointParamsGenerator(params).paramsStruct()
|
||||
fun defaultResolver(): RuntimeType? = rules?.let { EndpointResolverGenerator(stdlib, runtimeConfig).defaultEndpointResolver(it) }
|
||||
fun defaultResolver(): RuntimeType? =
|
||||
rules?.let { EndpointResolverGenerator(stdlib, runtimeConfig).defaultEndpointResolver(it) }
|
||||
|
||||
fun testGenerator(): Writable =
|
||||
defaultResolver()?.let { EndpointTestGenerator(tests, paramsStruct(), it, params, runtimeConfig).generate() } ?: {}
|
||||
defaultResolver()?.let { EndpointTestGenerator(tests, paramsStruct(), it, params, runtimeConfig).generate() }
|
||||
?: {}
|
||||
|
||||
/**
|
||||
* Load the builtIn value for [parameter] from the endpoint customizations. If the built-in comes from service config,
|
||||
|
|
|
@ -28,7 +28,6 @@ import software.amazon.smithy.rust.codegen.core.rustlang.writable
|
|||
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationCustomization
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationSection
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.generators.operationBuildError
|
||||
import software.amazon.smithy.rust.codegen.core.util.dq
|
||||
import software.amazon.smithy.rust.codegen.core.util.orNull
|
||||
|
||||
|
@ -80,15 +79,11 @@ class EndpointsDecorator : ClientCodegenDecorator {
|
|||
operation: OperationShape,
|
||||
baseCustomizations: List<OperationCustomization>,
|
||||
): List<OperationCustomization> {
|
||||
return listOfNotNull(
|
||||
EndpointTypesGenerator.fromContext(codegenContext)?.let { endpointTypes ->
|
||||
InjectEndpointInMakeOperation(
|
||||
codegenContext,
|
||||
endpointTypes,
|
||||
operation,
|
||||
)
|
||||
},
|
||||
) + baseCustomizations
|
||||
return baseCustomizations + InjectEndpointInMakeOperation(
|
||||
codegenContext,
|
||||
EndpointTypesGenerator.fromContext(codegenContext),
|
||||
operation,
|
||||
)
|
||||
}
|
||||
|
||||
override fun endpointCustomizations(codegenContext: ClientCodegenContext): List<EndpointCustomization> {
|
||||
|
@ -105,19 +100,15 @@ class EndpointsDecorator : ClientCodegenDecorator {
|
|||
codegenContext: ClientCodegenContext,
|
||||
baseCustomizations: List<ConfigCustomization>,
|
||||
): List<ConfigCustomization> {
|
||||
return baseCustomizations + ClientContextDecorator(codegenContext) + listOfNotNull(
|
||||
EndpointTypesGenerator.fromContext(
|
||||
codegenContext,
|
||||
)?.let { EndpointConfigCustomization(codegenContext, it) },
|
||||
)
|
||||
return baseCustomizations + ClientContextDecorator(codegenContext) +
|
||||
EndpointConfigCustomization(codegenContext, EndpointTypesGenerator.fromContext(codegenContext))
|
||||
}
|
||||
|
||||
override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) {
|
||||
EndpointTypesGenerator.fromContext(codegenContext)?.also { generator ->
|
||||
rustCrate.withModule(EndpointsModule) {
|
||||
withInlineModule(EndpointTests) {
|
||||
generator.testGenerator()(this)
|
||||
}
|
||||
val generator = EndpointTypesGenerator.fromContext(codegenContext)
|
||||
rustCrate.withModule(EndpointsModule) {
|
||||
withInlineModule(EndpointTests) {
|
||||
generator.testGenerator()(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,19 +134,23 @@ class EndpointsDecorator : ClientCodegenDecorator {
|
|||
OperationCustomization() {
|
||||
|
||||
private val idx = ContextIndex.of(ctx.model)
|
||||
private val types = Types(ctx.runtimeConfig)
|
||||
|
||||
override fun section(section: OperationSection): Writable {
|
||||
val codegenScope = arrayOf(
|
||||
"Params" to typesGenerator.paramsStruct(),
|
||||
"BuildError" to ctx.runtimeConfig.operationBuildError(),
|
||||
"ResolveEndpointError" to types.resolveEndpointError,
|
||||
)
|
||||
return when (section) {
|
||||
is OperationSection.MutateInput -> writable {
|
||||
rustTemplate(
|
||||
"""
|
||||
let endpoint_params = #{Params}::builder()#{builderFields:W}.build()
|
||||
.map_err(#{BuildError}::other)?;
|
||||
let endpoint_result = ${section.config}.endpoint_resolver.resolve_endpoint(&endpoint_params);
|
||||
let params_result = #{Params}::builder()#{builderFields:W}.build()
|
||||
.map_err(|err|#{ResolveEndpointError}::from_source("could not construct endpoint parameters", err));
|
||||
let (endpoint_result, params) = match params_result {
|
||||
Ok(params) => (${section.config}.endpoint_resolver.resolve_endpoint(¶ms), Some(params)),
|
||||
Err(e) => (Err(e), None)
|
||||
};
|
||||
""",
|
||||
"builderFields" to builderFields(typesGenerator.params, section),
|
||||
*codegenScope,
|
||||
|
@ -164,8 +159,8 @@ class EndpointsDecorator : ClientCodegenDecorator {
|
|||
|
||||
is OperationSection.MutateRequest -> writable {
|
||||
// insert the endpoint the bag
|
||||
rustTemplate("${section.request}.properties_mut().insert(endpoint_params);")
|
||||
rustTemplate("${section.request}.properties_mut().insert(endpoint_result);")
|
||||
rustTemplate("""if let Some(params) = params { ${section.request}.properties_mut().insert(params); }""")
|
||||
}
|
||||
|
||||
else -> emptySection
|
||||
|
|
|
@ -8,7 +8,6 @@ package software.amazon.smithy.rust.codegen.client.smithy.endpoint.generators
|
|||
import software.amazon.smithy.rulesengine.language.Endpoint
|
||||
import software.amazon.smithy.rulesengine.language.EndpointRuleSet
|
||||
import software.amazon.smithy.rulesengine.language.eval.Type
|
||||
import software.amazon.smithy.rulesengine.language.syntax.Identifier
|
||||
import software.amazon.smithy.rulesengine.language.syntax.expr.Expression
|
||||
import software.amazon.smithy.rulesengine.language.syntax.expr.Reference
|
||||
import software.amazon.smithy.rulesengine.language.syntax.fn.Function
|
||||
|
@ -128,6 +127,19 @@ internal class EndpointResolverGenerator(stdlib: List<CustomRuntimeFunction>, ru
|
|||
"EndpointError" to types.resolveEndpointError,
|
||||
"DiagnosticCollector" to endpointsLib("diagnostic").toType().resolve("DiagnosticCollector"),
|
||||
)
|
||||
|
||||
private val allowLintsForResolver = listOf(
|
||||
// we generate if x { if y { if z { ... } } }
|
||||
"clippy::collapsible_if",
|
||||
// we generate `if (true) == expr { ... }`
|
||||
"clippy::bool_comparison",
|
||||
// we generate `if !(a == b)`
|
||||
"clippy::nonminimal_bool",
|
||||
// we generate `if x == "" { ... }`
|
||||
"clippy::comparison_to_empty",
|
||||
// we generate `if let Some(_) = ... { ... }`
|
||||
"clippy::redundant_pattern_matching",
|
||||
)
|
||||
private val context = Context(registry, runtimeConfig)
|
||||
|
||||
companion object {
|
||||
|
@ -190,6 +202,7 @@ internal class EndpointResolverGenerator(stdlib: List<CustomRuntimeFunction>, ru
|
|||
fnsUsed: List<CustomRuntimeFunction>,
|
||||
): RuntimeType {
|
||||
return RuntimeType.forInlineFun("resolve_endpoint", EndpointsImpl) {
|
||||
allowLintsForResolver.map { Attribute.Custom("allow($it)") }.map { it.render(this) }
|
||||
rustTemplate(
|
||||
"""
|
||||
pub(super) fn resolve_endpoint($ParamsName: &#{Params}, $DiagnosticCollector: &mut #{DiagnosticCollector}, #{additional_args}) -> #{endpoint}::Result {
|
||||
|
@ -270,7 +283,7 @@ internal class EndpointResolverGenerator(stdlib: List<CustomRuntimeFunction>, ru
|
|||
// 2. the RHS returns a boolean which we need to gate on
|
||||
// 3. the RHS is infallible (e.g. uriEncode)
|
||||
val resultName =
|
||||
(condition.result.orNull() ?: (fn as? Reference)?.name ?: Identifier.of("_")).rustName()
|
||||
(condition.result.orNull() ?: (fn as? Reference)?.name)?.rustName() ?: "_"
|
||||
val target = generator.generate(fn)
|
||||
val next = generateRuleInternal(rule, rest)
|
||||
when {
|
||||
|
|
|
@ -49,9 +49,13 @@ class ExpressionGenerator(
|
|||
}
|
||||
|
||||
override fun visitRef(ref: Reference) = writable {
|
||||
rust(ref.name.rustName())
|
||||
if (ownership == Ownership.Owned) {
|
||||
rust(".to_owned()")
|
||||
when (ref.type()) {
|
||||
is Type.Bool -> rust("*${ref.name.rustName()}")
|
||||
else -> rust("${ref.name.rustName()}.to_owned()")
|
||||
}
|
||||
} else {
|
||||
rust(ref.name.rustName())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,7 +68,7 @@ class ExpressionGenerator(
|
|||
is GetAttr.Part.Index -> rust(".get(${part.index()}).cloned()") // we end up with Option<&&T>, we need to get to Option<&T>
|
||||
}
|
||||
}
|
||||
if (ownership == Ownership.Owned) {
|
||||
if (ownership == Ownership.Owned && getAttr.type() != Type.bool()) {
|
||||
if (getAttr.type() is Type.Option) {
|
||||
rust(".map(|t|t.to_owned())")
|
||||
} else {
|
||||
|
|
|
@ -47,7 +47,11 @@ class TemplateGenerator(
|
|||
}
|
||||
|
||||
override fun visitStaticElement(str: String) = writable {
|
||||
rust("out.push_str(${str.dq()});")
|
||||
when (str.length) {
|
||||
0 -> {}
|
||||
1 -> rust("out.push('$str');")
|
||||
else -> rust("out.push_str(${str.dq()});")
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitDynamicElement(expr: Expression) = writable {
|
||||
|
|
|
@ -136,7 +136,7 @@ class GenericFluentClient(codegenContext: CodegenContext) : FluentClientCustomiz
|
|||
/// ## */
|
||||
/// ## .middleware_fn(|r| r)
|
||||
/// .build();
|
||||
/// let config = Config::builder().build();
|
||||
/// let config = Config::builder().endpoint_resolver("https://www.myurl.com").build();
|
||||
/// let client = Client::with_config(smithy_client, config);
|
||||
/// ```
|
||||
///
|
||||
|
|
|
@ -180,7 +180,7 @@ class ProtocolTestGenerator(
|
|||
} ?: writable { }
|
||||
rustTemplate(
|
||||
"""
|
||||
let builder = #{Config}::Config::builder()$customToken;
|
||||
let builder = #{Config}::Config::builder().endpoint_resolver("https://example.com")$customToken;
|
||||
#{customParams}
|
||||
let config = builder.build();
|
||||
|
||||
|
|
|
@ -27,18 +27,18 @@ import java.nio.file.Path
|
|||
*/
|
||||
fun clientIntegrationTest(
|
||||
model: Model,
|
||||
addtionalDecorators: List<ClientCodegenDecorator> = listOf(),
|
||||
additionalDecorators: List<ClientCodegenDecorator> = listOf(),
|
||||
addModuleToEventStreamAllowList: Boolean = false,
|
||||
service: String? = null,
|
||||
runtimeConfig: RuntimeConfig? = null,
|
||||
additionalSettings: ObjectNode = ObjectNode.builder().build(),
|
||||
command: ((Path) -> Unit)? = null,
|
||||
test: (ClientCodegenContext, RustCrate) -> Unit,
|
||||
test: (ClientCodegenContext, RustCrate) -> Unit = { _, _ -> },
|
||||
): Path {
|
||||
return codegenIntegrationTest(
|
||||
model,
|
||||
RustClientCodegenPlugin(),
|
||||
addtionalDecorators,
|
||||
additionalDecorators,
|
||||
addModuleToEventStreamAllowList = addModuleToEventStreamAllowList,
|
||||
service = service,
|
||||
runtimeConfig = runtimeConfig,
|
||||
|
|
|
@ -12,6 +12,8 @@ import org.junit.jupiter.params.provider.MethodSource
|
|||
import software.amazon.smithy.codegen.core.CodegenException
|
||||
import software.amazon.smithy.model.node.Node
|
||||
import software.amazon.smithy.rulesengine.language.Endpoint
|
||||
import software.amazon.smithy.rulesengine.language.eval.Scope
|
||||
import software.amazon.smithy.rulesengine.language.eval.Type
|
||||
import software.amazon.smithy.rulesengine.language.syntax.expr.Expression
|
||||
import software.amazon.smithy.rulesengine.language.syntax.expr.Literal
|
||||
import software.amazon.smithy.rulesengine.testutil.TestDiscovery
|
||||
|
@ -30,7 +32,8 @@ import java.util.stream.Stream
|
|||
class EndpointResolverGeneratorTest {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun testSuites(): Stream<TestDiscovery.RulesTestSuite> = TestDiscovery().testSuites()
|
||||
fun testSuites(): Stream<TestDiscovery.RulesTestSuite> =
|
||||
TestDiscovery().testSuites().map { it.ruleSet().typecheck(); it }
|
||||
}
|
||||
|
||||
// for tests, load partitions.json from smithy—for real usage, this file will be inserted at codegen time
|
||||
|
@ -105,6 +108,9 @@ class EndpointResolverGeneratorTest {
|
|||
hashMapOf("signingName" to Literal.of("service"), "signingScope" to Literal.of("{Region}")),
|
||||
)
|
||||
.build()
|
||||
val scope = Scope<Type>()
|
||||
scope.insert("Region", Type.string())
|
||||
endpoint.typeCheck(scope)
|
||||
val generator = EndpointResolverGenerator(listOf(), TestRuntimeConfig)
|
||||
TestWorkspace.testProject().unitTest {
|
||||
rustTemplate(
|
||||
|
|
|
@ -8,7 +8,6 @@ package software.amazon.smithy.rust.codegen.client.endpoint
|
|||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
import org.junit.jupiter.api.Test
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointsDecorator
|
||||
import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rust
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.TokioTest
|
||||
|
@ -36,7 +35,11 @@ class EndpointsDecoratorTest {
|
|||
@endpointRuleSet({
|
||||
"version": "1.0",
|
||||
"rules": [{
|
||||
"conditions": [{"fn": "isSet", "argv": [{"ref":"Region"}]}],
|
||||
"conditions": [
|
||||
{"fn": "isSet", "argv": [{"ref":"Region"}]},
|
||||
{"fn": "isSet", "argv": [{"ref":"ABoolParam"}]},
|
||||
{"fn": "booleanEquals", "argv": [{"ref": "ABoolParam"}, false]}
|
||||
],
|
||||
"type": "endpoint",
|
||||
"endpoint": {
|
||||
"url": "https://www.{Region}.example.com"
|
||||
|
@ -94,7 +97,6 @@ class EndpointsDecoratorTest {
|
|||
fun `set an endpoint in the property bag`() {
|
||||
val testDir = clientIntegrationTest(
|
||||
model,
|
||||
addtionalDecorators = listOf(EndpointsDecorator()),
|
||||
// just run integration tests
|
||||
command = { "cargo test --test *".runWithWarnings(it) },
|
||||
) { clientCodegenContext, rustCrate ->
|
||||
|
|
|
@ -12,8 +12,8 @@ import software.amazon.smithy.aws.traits.protocols.RestJson1Trait
|
|||
import software.amazon.smithy.model.shapes.OperationShape
|
||||
import software.amazon.smithy.model.shapes.ShapeId
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenVisitor
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
|
||||
import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.escape
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rust
|
||||
|
@ -31,11 +31,9 @@ import software.amazon.smithy.rust.codegen.core.smithy.protocols.ProtocolGenerat
|
|||
import software.amazon.smithy.rust.codegen.core.smithy.protocols.ProtocolMap
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.protocols.RestJson
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.generatePluginContext
|
||||
import software.amazon.smithy.rust.codegen.core.util.CommandFailed
|
||||
import software.amazon.smithy.rust.codegen.core.util.dq
|
||||
import software.amazon.smithy.rust.codegen.core.util.outputShape
|
||||
import software.amazon.smithy.rust.codegen.core.util.runCommand
|
||||
import java.nio.file.Path
|
||||
|
||||
private class TestProtocolPayloadGenerator(private val body: String) : ProtocolPayloadGenerator {
|
||||
|
@ -53,7 +51,11 @@ private class TestProtocolTraitImplGenerator(
|
|||
) : ProtocolTraitImplGenerator {
|
||||
private val symbolProvider = codegenContext.symbolProvider
|
||||
|
||||
override fun generateTraitImpls(operationWriter: RustWriter, operationShape: OperationShape, customizations: List<OperationCustomization>) {
|
||||
override fun generateTraitImpls(
|
||||
operationWriter: RustWriter,
|
||||
operationShape: OperationShape,
|
||||
customizations: List<OperationCustomization>,
|
||||
) {
|
||||
operationWriter.rustTemplate(
|
||||
"""
|
||||
impl #{parse_strict} for ${operationShape.id.name}{
|
||||
|
@ -216,12 +218,11 @@ class ProtocolTestGeneratorTest {
|
|||
*
|
||||
* Returns the [Path] the service was generated at, suitable for running `cargo test`
|
||||
*/
|
||||
private fun generateService(
|
||||
private fun testService(
|
||||
httpRequestBuilder: String,
|
||||
body: String = "${correctBody.dq()}.to_string()",
|
||||
correctResponse: String = """Ok(crate::output::SayHelloOutput::builder().value("hey there!").build())""",
|
||||
): Path {
|
||||
val (pluginContext, testDir) = generatePluginContext(model)
|
||||
val codegenDecorator = object : ClientCodegenDecorator {
|
||||
override val name: String = "mock"
|
||||
override val order: Byte = 0
|
||||
|
@ -233,42 +234,31 @@ class ProtocolTestGeneratorTest {
|
|||
// Intentionally replace the builtin implementation of RestJson1 with our fake protocol
|
||||
mapOf(RestJson1Trait.ID to TestProtocolFactory(httpRequestBuilder, body, correctResponse))
|
||||
}
|
||||
val visitor = ClientCodegenVisitor(
|
||||
pluginContext,
|
||||
codegenDecorator,
|
||||
)
|
||||
visitor.execute()
|
||||
println("file:///$testDir/src/operation.rs")
|
||||
return testDir
|
||||
return clientIntegrationTest(model, additionalDecorators = listOf(codegenDecorator))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passing e2e protocol request test`() {
|
||||
val path = generateService(
|
||||
testService(
|
||||
"""
|
||||
.uri("/?Hi=Hello%20there&required")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
)
|
||||
|
||||
val testOutput = "cargo test".runCommand(path)
|
||||
// Verify the test actually ran
|
||||
testOutput shouldContain "say_hello_request ... ok"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test incorrect response parsing`() {
|
||||
val path = generateService(
|
||||
"""
|
||||
.uri("/?Hi=Hello%20there&required")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
correctResponse = "Ok(crate::output::SayHelloOutput::builder().build())",
|
||||
)
|
||||
val err = assertThrows<CommandFailed> {
|
||||
"cargo test".runCommand(path)
|
||||
testService(
|
||||
"""
|
||||
.uri("/?Hi=Hello%20there&required")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
correctResponse = "Ok(crate::output::SayHelloOutput::builder().build())",
|
||||
)
|
||||
}
|
||||
|
||||
err.message shouldContain "basic_response_test_response ... FAILED"
|
||||
|
@ -276,17 +266,15 @@ class ProtocolTestGeneratorTest {
|
|||
|
||||
@Test
|
||||
fun `test invalid body`() {
|
||||
val path = generateService(
|
||||
"""
|
||||
.uri("/?Hi=Hello%20there&required")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
""""{}".to_string()""",
|
||||
)
|
||||
|
||||
val err = assertThrows<CommandFailed> {
|
||||
"cargo test".runCommand(path)
|
||||
testService(
|
||||
"""
|
||||
.uri("/?Hi=Hello%20there&required")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
""""{}".to_string()""",
|
||||
)
|
||||
}
|
||||
|
||||
err.message shouldContain "say_hello_request ... FAILED"
|
||||
|
@ -295,17 +283,14 @@ class ProtocolTestGeneratorTest {
|
|||
|
||||
@Test
|
||||
fun `test invalid url parameter`() {
|
||||
// Hard coded implementation for this 1 test
|
||||
val path = generateService(
|
||||
"""
|
||||
.uri("/?Hi=INCORRECT&required")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
)
|
||||
|
||||
val err = assertThrows<CommandFailed> {
|
||||
"cargo test".runCommand(path)
|
||||
testService(
|
||||
"""
|
||||
.uri("/?Hi=INCORRECT&required")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
)
|
||||
}
|
||||
// Verify the test actually ran
|
||||
err.message shouldContain "say_hello_request ... FAILED"
|
||||
|
@ -314,16 +299,14 @@ class ProtocolTestGeneratorTest {
|
|||
|
||||
@Test
|
||||
fun `test forbidden url parameter`() {
|
||||
val path = generateService(
|
||||
"""
|
||||
.uri("/?goodbye&Hi=Hello%20there&required")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
)
|
||||
|
||||
val err = assertThrows<CommandFailed> {
|
||||
"cargo test".runCommand(path)
|
||||
testService(
|
||||
"""
|
||||
.uri("/?goodbye&Hi=Hello%20there&required")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
)
|
||||
}
|
||||
// Verify the test actually ran
|
||||
err.message shouldContain "say_hello_request ... FAILED"
|
||||
|
@ -333,17 +316,16 @@ class ProtocolTestGeneratorTest {
|
|||
@Test
|
||||
fun `test required url parameter`() {
|
||||
// Hard coded implementation for this 1 test
|
||||
val path = generateService(
|
||||
"""
|
||||
.uri("/?Hi=Hello%20there")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
)
|
||||
|
||||
val err = assertThrows<CommandFailed> {
|
||||
"cargo test".runCommand(path)
|
||||
testService(
|
||||
"""
|
||||
.uri("/?Hi=Hello%20there")
|
||||
.header("X-Greeting", "Hi")
|
||||
.method("POST")
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
// Verify the test actually ran
|
||||
err.message shouldContain "say_hello_request ... FAILED"
|
||||
err.message shouldContain "required query param missing"
|
||||
|
@ -351,18 +333,17 @@ class ProtocolTestGeneratorTest {
|
|||
|
||||
@Test
|
||||
fun `invalid header`() {
|
||||
val path = generateService(
|
||||
"""
|
||||
.uri("/?Hi=Hello%20there&required")
|
||||
// should be "Hi"
|
||||
.header("X-Greeting", "Hey")
|
||||
.method("POST")
|
||||
""",
|
||||
)
|
||||
|
||||
val err = assertThrows<CommandFailed> {
|
||||
"cargo test".runCommand(path)
|
||||
testService(
|
||||
"""
|
||||
.uri("/?Hi=Hello%20there&required")
|
||||
// should be "Hi"
|
||||
.header("X-Greeting", "Hey")
|
||||
.method("POST")
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
err.message shouldContain "say_hello_request ... FAILED"
|
||||
err.message shouldContain "invalid header value"
|
||||
}
|
||||
|
|
|
@ -57,13 +57,13 @@ private fun tempDir(directory: File? = null): File {
|
|||
*/
|
||||
object TestWorkspace {
|
||||
private val baseDir by lazy {
|
||||
val homeDir = System.getProperty("APPDATA")
|
||||
val appDataDir = System.getProperty("APPDATA")
|
||||
?: System.getenv("XDG_DATA_HOME")
|
||||
?: System.getProperty("user.home")
|
||||
?.let { Path.of(it, "Library/Application Support").absolutePathString() }
|
||||
?.takeIf { File(it).exists() }
|
||||
if (homeDir != null) {
|
||||
File(Path.of(homeDir, "smithy-test-workspace").absolutePathString())
|
||||
?.let { Path.of(it, ".local", "share").absolutePathString() }
|
||||
?.also { File(it).mkdirs() }
|
||||
if (appDataDir != null) {
|
||||
File(Path.of(appDataDir, "smithy-test-workspace").absolutePathString())
|
||||
} else {
|
||||
System.getenv("SMITHY_TEST_WORKSPACE")?.let { File(it) } ?: tempDir()
|
||||
}
|
||||
|
|
|
@ -116,6 +116,7 @@ pub struct ValidateRequest {
|
|||
impl ValidateRequest {
|
||||
pub fn assert_matches(&self, ignore_headers: &[HeaderName]) {
|
||||
let (actual, expected) = (&self.actual, &self.expected);
|
||||
assert_eq!(actual.uri(), expected.uri());
|
||||
for (name, value) in expected.headers() {
|
||||
if !ignore_headers.contains(name) {
|
||||
let actual_header = actual
|
||||
|
@ -146,7 +147,6 @@ impl ValidateRequest {
|
|||
(Ok(actual), Ok(expected)) => assert_ok(validate_body(actual, expected, media_type)),
|
||||
_ => assert_eq!(actual.body().bytes(), expected.body().bytes()),
|
||||
};
|
||||
assert_eq!(actual.uri(), expected.uri());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,6 +201,7 @@ impl<B> TestConnection<B> {
|
|||
self.requests.lock().unwrap()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_requests_match(&self, ignore_headers: &[HeaderName]) {
|
||||
for req in self.requests().iter() {
|
||||
req.assert_matches(ignore_headers)
|
||||
|
|
|
@ -21,12 +21,20 @@ pub trait ResolveEndpoint<Params>: Send + Sync {
|
|||
fn resolve_endpoint(&self, params: &Params) -> Result;
|
||||
}
|
||||
|
||||
// TODO(endpoints 2.0): when `endpoint_url` is added, deprecate & delete `Endpoint`
|
||||
impl<T> ResolveEndpoint<T> for &'static str {
|
||||
fn resolve_endpoint(&self, _params: &T) -> Result {
|
||||
Ok(aws_smithy_types::endpoint::Endpoint::builder()
|
||||
.url(*self)
|
||||
.build())
|
||||
}
|
||||
}
|
||||
|
||||
/// API Endpoint
|
||||
///
|
||||
/// This implements an API endpoint as specified in the
|
||||
/// [Smithy Endpoint Specification](https://awslabs.github.io/smithy/1.0/spec/core/endpoint-traits.html)
|
||||
#[derive(Clone, Debug)]
|
||||
#[deprecated(note = "Use `.endpoint_url(...)` directly instead")]
|
||||
pub struct Endpoint {
|
||||
uri: http::Uri,
|
||||
|
||||
|
@ -34,6 +42,16 @@ pub struct Endpoint {
|
|||
immutable: bool,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
/// This allows customers that use `Endpoint` to override the endpoint to continue to do so
|
||||
impl<T> ResolveEndpoint<T> for Endpoint {
|
||||
fn resolve_endpoint(&self, _params: &T) -> Result {
|
||||
Ok(aws_smithy_types::endpoint::Endpoint::builder()
|
||||
.url(self.uri.to_string())
|
||||
.build())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct EndpointPrefix(String);
|
||||
impl EndpointPrefix {
|
||||
|
@ -57,9 +75,6 @@ impl EndpointPrefix {
|
|||
/// Apply `endpoint` to `uri`
|
||||
///
|
||||
/// This method mutates `uri` by setting the `endpoint` on it
|
||||
///
|
||||
/// # Panics
|
||||
/// This method panics if `uri` does not have a scheme
|
||||
pub fn apply_endpoint(
|
||||
uri: &mut Uri,
|
||||
endpoint: &Uri,
|
||||
|
@ -84,13 +99,14 @@ pub fn apply_endpoint(
|
|||
let new_uri = Uri::builder()
|
||||
.authority(authority)
|
||||
.scheme(scheme.clone())
|
||||
.path_and_query(Endpoint::merge_paths(endpoint, uri).as_ref())
|
||||
.path_and_query(merge_paths(endpoint, uri).as_ref())
|
||||
.build()
|
||||
.map_err(InvalidEndpointError::failed_to_construct_uri)?;
|
||||
*uri = new_uri;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl Endpoint {
|
||||
/// Create a new endpoint from a URI
|
||||
///
|
||||
|
@ -177,26 +193,27 @@ impl Endpoint {
|
|||
Ok(endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_paths<'a>(endpoint: &'a Uri, uri: &'a Uri) -> Cow<'a, str> {
|
||||
if let Some(query) = endpoint.path_and_query().and_then(|pq| pq.query()) {
|
||||
tracing::warn!(query = %query, "query specified in endpoint will be ignored during endpoint resolution");
|
||||
}
|
||||
let endpoint_path = endpoint.path();
|
||||
let uri_path_and_query = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("");
|
||||
if endpoint_path.is_empty() {
|
||||
Cow::Borrowed(uri_path_and_query)
|
||||
} else {
|
||||
let ep_no_slash = endpoint_path.strip_suffix('/').unwrap_or(endpoint_path);
|
||||
let uri_path_no_slash = uri_path_and_query
|
||||
.strip_prefix('/')
|
||||
.unwrap_or(uri_path_and_query);
|
||||
Cow::Owned(format!("{}/{}", ep_no_slash, uri_path_no_slash))
|
||||
}
|
||||
fn merge_paths<'a>(endpoint: &'a Uri, uri: &'a Uri) -> Cow<'a, str> {
|
||||
if let Some(query) = endpoint.path_and_query().and_then(|pq| pq.query()) {
|
||||
tracing::warn!(query = %query, "query specified in endpoint will be ignored during endpoint resolution");
|
||||
}
|
||||
let endpoint_path = endpoint.path();
|
||||
let uri_path_and_query = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("");
|
||||
if endpoint_path.is_empty() {
|
||||
Cow::Borrowed(uri_path_and_query)
|
||||
} else {
|
||||
let ep_no_slash = endpoint_path.strip_suffix('/').unwrap_or(endpoint_path);
|
||||
let uri_path_no_slash = uri_path_and_query
|
||||
.strip_prefix('/')
|
||||
.unwrap_or(uri_path_and_query);
|
||||
Cow::Owned(format!("{}/{}", ep_no_slash, uri_path_no_slash))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(deprecated)]
|
||||
mod test {
|
||||
use crate::endpoint::error::{InvalidEndpointError, InvalidEndpointErrorKind};
|
||||
use crate::endpoint::{Endpoint, EndpointPrefix};
|
||||
|
|
Loading…
Reference in New Issue