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:
Russell Cohen 2022-12-16 12:56:16 -05:00 committed by GitHub
parent 59de022029
commit 29abdc9b42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 794 additions and 1360 deletions

View File

@ -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 ARNs region when constructing an endpoint instead of the clients 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"]

View File

@ -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)
}

View File

@ -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(

View File

@ -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,

View File

@ -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()
}
}

View File

@ -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(&region));
assert_eq!(
req.properties().get(),
Some(&SigningService::from_static("qldb"))

View File

@ -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()))
}
}

View File

@ -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()
)
}
}

View File

@ -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));

View File

@ -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]

View File

@ -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");

View File

@ -4,6 +4,7 @@
*/
//! AWS SDK endpoint support.
#![allow(deprecated)]
use crate::region::{Region, SigningRegion};
use crate::SigningService;

View File

@ -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()

View File

@ -41,6 +41,7 @@ val allCodegenTests = listOf(
CodegenTest(
"com.amazonaws.apigateway#BackplaneControlService",
"apigateway",
imports = listOf("models/apigateway-rules.smithy"),
extraConfig = """
,
"codegen": {

View File

@ -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" },
}
})

View File

@ -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

View File

@ -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()")
}
}
}

View File

@ -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 {

View File

@ -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());

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -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,
)
}
}
}
}
}

View File

@ -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: "<> `?🐱",

View File

@ -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;

View File

@ -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")
);
}

View File

@ -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()

View File

@ -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
}

View File

@ -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;

View File

@ -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"
);
}

View File

@ -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]);
}

View File

@ -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")

View File

@ -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 {}",

View File

@ -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(())
}

View File

@ -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()

View File

@ -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"

View File

@ -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(),

View File

@ -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);

View File

@ -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(

View File

@ -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" },

View File

@ -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)

View File

@ -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,
)

View File

@ -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,
)
}
}

View File

@ -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 {

View File

@ -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,

View File

@ -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(&params), 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

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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);
/// ```
///

View File

@ -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();

View File

@ -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,

View File

@ -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(

View File

@ -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 ->

View File

@ -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"
}

View File

@ -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()
}

View File

@ -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)

View File

@ -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};