Skip to content

Commit 3600aad

Browse files
CopilotNugine
andauthored
Add operation ListDirectoryBuckets (s3s-project#520)
* Initial plan * feat: add ListDirectoryBuckets operation Remove ListDirectoryBuckets from SKIPPED_OPS and update router codegen to handle x-id based disambiguation for operations sharing the same HTTP method and path pattern. Co-authored-by: Nugine <30099658+Nugine@users.noreply.github.com> * fix * test: add coverage tests for ListDirectoryBuckets routing, deserialization, and serialization Co-authored-by: Nugine <30099658+Nugine@users.noreply.github.com> * fix * prevent codecov from failing * Change coverage status from project to patch * Update Codecov configuration for project status --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Nugine <30099658+Nugine@users.noreply.github.com> Co-authored-by: Nugine <nugine@foxmail.com>
1 parent 5fbf7ab commit 3600aad

File tree

16 files changed

+743
-9
lines changed

16 files changed

+743
-9
lines changed

codecov.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
informational: true
6+
patch:
7+
default:
8+
informational: true

codegen/src/v1/ops.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ pub struct Operation {
3737

3838
pub type Operations = BTreeMap<String, Operation>;
3939

40-
// TODO: handle these operations
41-
pub const SKIPPED_OPS: &[&str] = &["ListDirectoryBuckets"];
40+
pub const SKIPPED_OPS: &[&str] = &[];
4241

4342
pub fn collect_operations(model: &smithy::Model) -> Operations {
4443
let mut operations: Operations = default();
@@ -971,12 +970,22 @@ impl PathPattern {
971970
qs.retain(|(n, v)| n != "x-id" && v.is_empty().not());
972971
qs
973972
}
973+
974+
fn x_id_value(part: &str) -> Option<String> {
975+
let (_, q) = part.split_once('?')?;
976+
let qs: Vec<(String, String)> = serde_urlencoded::from_str(q).unwrap();
977+
qs.into_iter()
978+
.find(|(n, _)| n == "x-id")
979+
.map(|(_, v)| v)
980+
.filter(|v| v.is_empty().not())
981+
}
974982
}
975983

976984
struct Route<'a> {
977985
op: &'a Operation,
978986
query_tag: Option<String>,
979987
query_patterns: Vec<(String, String)>,
988+
x_id: Option<String>,
980989
required_headers: Vec<&'a str>,
981990
required_query_strings: Vec<&'a str>,
982991
needs_full_body: bool,
@@ -998,6 +1007,7 @@ fn collect_routes<'a>(ops: &'a Operations, rust_types: &'a RustTypes) -> HashMap
9981007
op,
9991008
query_tag: PathPattern::query_tag(&op.http_uri),
10001009
query_patterns: PathPattern::query_patterns(&op.http_uri),
1010+
x_id: PathPattern::x_id_value(&op.http_uri),
10011011

10021012
required_headers: required_headers(op, rust_types),
10031013
required_query_strings: required_query_strings(op, rust_types),
@@ -1155,7 +1165,11 @@ fn codegen_router(ops: &Operations, rust_types: &RustTypes) {
11551165
&& route.query_tag.is_none()
11561166
};
11571167
let final_count = group.iter().filter(|r| is_final_op(r)).count();
1158-
assert!(final_count <= 1);
1168+
// When multiple final ops exist, they must be disambiguated by x-id
1169+
if final_count > 1 {
1170+
assert!(group.iter().filter(|r| is_final_op(r)).all(|r| r.x_id.is_some()));
1171+
}
1172+
let fallback_op_name = group.iter().find(|r| is_final_op(r)).map(|r| r.op.name.as_str());
11591173

11601174
g!("if let Some(qs) = qs {{");
11611175
for route in group {
@@ -1223,7 +1237,15 @@ fn codegen_router(ops: &Operations, rust_types: &RustTypes) {
12231237
succ(route, true);
12241238
g!("}}");
12251239
}
1226-
(false, false) => {}
1240+
(false, false) => {
1241+
// When multiple final ops exist, use x-id to disambiguate non-fallback routes
1242+
if final_count > 1 && Some(route.op.name.as_str()) != fallback_op_name {
1243+
let x_id = route.x_id.as_deref().unwrap();
1244+
g!("if super::check_query_pattern(qs, \"x-id\",\"{x_id}\") {{");
1245+
succ(route, true);
1246+
g!("}}");
1247+
}
1248+
}
12271249
}
12281250
}
12291251
g!("}}");
@@ -1272,9 +1294,8 @@ fn codegen_router(ops: &Operations, rust_types: &RustTypes) {
12721294
}
12731295
}
12741296

1275-
if final_count == 1 {
1276-
let route = group.last().unwrap();
1277-
assert!(is_final_op(route));
1297+
if final_count >= 1 {
1298+
let route = group.iter().find(|r| is_final_op(r)).unwrap();
12781299
succ(route, false);
12791300
} else {
12801301
g!("Err(super::unknown_operation())");

crates/s3s-aws/src/conv/generated.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4912,6 +4912,44 @@ impl AwsConversion for s3s::dto::ListBucketsOutput {
49124912
}
49134913
}
49144914

4915+
impl AwsConversion for s3s::dto::ListDirectoryBucketsInput {
4916+
type Target = aws_sdk_s3::operation::list_directory_buckets::ListDirectoryBucketsInput;
4917+
type Error = S3Error;
4918+
4919+
fn try_from_aws(x: Self::Target) -> S3Result<Self> {
4920+
Ok(Self {
4921+
continuation_token: try_from_aws(x.continuation_token)?,
4922+
max_directory_buckets: try_from_aws(x.max_directory_buckets)?,
4923+
})
4924+
}
4925+
4926+
fn try_into_aws(x: Self) -> S3Result<Self::Target> {
4927+
let mut y = Self::Target::builder();
4928+
y = y.set_continuation_token(try_into_aws(x.continuation_token)?);
4929+
y = y.set_max_directory_buckets(try_into_aws(x.max_directory_buckets)?);
4930+
y.build().map_err(S3Error::internal_error)
4931+
}
4932+
}
4933+
4934+
impl AwsConversion for s3s::dto::ListDirectoryBucketsOutput {
4935+
type Target = aws_sdk_s3::operation::list_directory_buckets::ListDirectoryBucketsOutput;
4936+
type Error = S3Error;
4937+
4938+
fn try_from_aws(x: Self::Target) -> S3Result<Self> {
4939+
Ok(Self {
4940+
buckets: try_from_aws(x.buckets)?,
4941+
continuation_token: try_from_aws(x.continuation_token)?,
4942+
})
4943+
}
4944+
4945+
fn try_into_aws(x: Self) -> S3Result<Self::Target> {
4946+
let mut y = Self::Target::builder();
4947+
y = y.set_buckets(try_into_aws(x.buckets)?);
4948+
y = y.set_continuation_token(try_into_aws(x.continuation_token)?);
4949+
Ok(y.build())
4950+
}
4951+
}
4952+
49154953
impl AwsConversion for s3s::dto::ListMultipartUploadsInput {
49164954
type Target = aws_sdk_s3::operation::list_multipart_uploads::ListMultipartUploadsInput;
49174955
type Error = S3Error;

crates/s3s-aws/src/conv/generated_minio.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4916,6 +4916,44 @@ impl AwsConversion for s3s::dto::ListBucketsOutput {
49164916
}
49174917
}
49184918

4919+
impl AwsConversion for s3s::dto::ListDirectoryBucketsInput {
4920+
type Target = aws_sdk_s3::operation::list_directory_buckets::ListDirectoryBucketsInput;
4921+
type Error = S3Error;
4922+
4923+
fn try_from_aws(x: Self::Target) -> S3Result<Self> {
4924+
Ok(Self {
4925+
continuation_token: try_from_aws(x.continuation_token)?,
4926+
max_directory_buckets: try_from_aws(x.max_directory_buckets)?,
4927+
})
4928+
}
4929+
4930+
fn try_into_aws(x: Self) -> S3Result<Self::Target> {
4931+
let mut y = Self::Target::builder();
4932+
y = y.set_continuation_token(try_into_aws(x.continuation_token)?);
4933+
y = y.set_max_directory_buckets(try_into_aws(x.max_directory_buckets)?);
4934+
y.build().map_err(S3Error::internal_error)
4935+
}
4936+
}
4937+
4938+
impl AwsConversion for s3s::dto::ListDirectoryBucketsOutput {
4939+
type Target = aws_sdk_s3::operation::list_directory_buckets::ListDirectoryBucketsOutput;
4940+
type Error = S3Error;
4941+
4942+
fn try_from_aws(x: Self::Target) -> S3Result<Self> {
4943+
Ok(Self {
4944+
buckets: try_from_aws(x.buckets)?,
4945+
continuation_token: try_from_aws(x.continuation_token)?,
4946+
})
4947+
}
4948+
4949+
fn try_into_aws(x: Self) -> S3Result<Self::Target> {
4950+
let mut y = Self::Target::builder();
4951+
y = y.set_buckets(try_into_aws(x.buckets)?);
4952+
y = y.set_continuation_token(try_into_aws(x.continuation_token)?);
4953+
Ok(y.build())
4954+
}
4955+
}
4956+
49194957
impl AwsConversion for s3s::dto::ListMultipartUploadsInput {
49204958
type Target = aws_sdk_s3::operation::list_multipart_uploads::ListMultipartUploadsInput;
49214959
type Error = S3Error;

crates/s3s-aws/src/proxy/generated.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,28 @@ impl S3 for Proxy {
15561556
}
15571557
}
15581558

1559+
#[tracing::instrument(skip(self, req))]
1560+
async fn list_directory_buckets(
1561+
&self,
1562+
req: S3Request<s3s::dto::ListDirectoryBucketsInput>,
1563+
) -> S3Result<S3Response<s3s::dto::ListDirectoryBucketsOutput>> {
1564+
let input = req.input;
1565+
debug!(?input);
1566+
let mut b = self.0.list_directory_buckets();
1567+
b = b.set_continuation_token(try_into_aws(input.continuation_token)?);
1568+
b = b.set_max_directory_buckets(try_into_aws(input.max_directory_buckets)?);
1569+
let result = b.send().await;
1570+
match result {
1571+
Ok(output) => {
1572+
let headers = super::meta::build_headers(&output)?;
1573+
let output = try_from_aws(output)?;
1574+
debug!(?output);
1575+
Ok(S3Response::with_headers(output, headers))
1576+
}
1577+
Err(e) => Err(wrap_sdk_error!(e)),
1578+
}
1579+
}
1580+
15591581
#[tracing::instrument(skip(self, req))]
15601582
async fn list_multipart_uploads(
15611583
&self,

crates/s3s-aws/src/proxy/generated_minio.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,28 @@ impl S3 for Proxy {
15561556
}
15571557
}
15581558

1559+
#[tracing::instrument(skip(self, req))]
1560+
async fn list_directory_buckets(
1561+
&self,
1562+
req: S3Request<s3s::dto::ListDirectoryBucketsInput>,
1563+
) -> S3Result<S3Response<s3s::dto::ListDirectoryBucketsOutput>> {
1564+
let input = req.input;
1565+
debug!(?input);
1566+
let mut b = self.0.list_directory_buckets();
1567+
b = b.set_continuation_token(try_into_aws(input.continuation_token)?);
1568+
b = b.set_max_directory_buckets(try_into_aws(input.max_directory_buckets)?);
1569+
let result = b.send().await;
1570+
match result {
1571+
Ok(output) => {
1572+
let headers = super::meta::build_headers(&output)?;
1573+
let output = try_from_aws(output)?;
1574+
debug!(?output);
1575+
Ok(S3Response::with_headers(output, headers))
1576+
}
1577+
Err(e) => Err(wrap_sdk_error!(e)),
1578+
}
1579+
}
1580+
15591581
#[tracing::instrument(skip(self, req))]
15601582
async fn list_multipart_uploads(
15611583
&self,

crates/s3s/src/access/generated.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,13 @@ pub trait S3Access: Send + Sync + 'static {
512512
Ok(())
513513
}
514514

515+
/// Checks whether the ListDirectoryBuckets request has accesses to the resources.
516+
///
517+
/// This method returns `Ok(())` by default.
518+
async fn list_directory_buckets(&self, _req: &mut S3Request<ListDirectoryBucketsInput>) -> S3Result<()> {
519+
Ok(())
520+
}
521+
515522
/// Checks whether the ListMultipartUploads request has accesses to the resources.
516523
///
517524
/// This method returns `Ok(())` by default.

crates/s3s/src/access/generated_minio.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,13 @@ pub trait S3Access: Send + Sync + 'static {
512512
Ok(())
513513
}
514514

515+
/// Checks whether the ListDirectoryBuckets request has accesses to the resources.
516+
///
517+
/// This method returns `Ok(())` by default.
518+
async fn list_directory_buckets(&self, _req: &mut S3Request<ListDirectoryBucketsInput>) -> S3Result<()> {
519+
Ok(())
520+
}
521+
515522
/// Checks whether the ListMultipartUploads request has accesses to the resources.
516523
///
517524
/// This method returns `Ok(())` by default.

0 commit comments

Comments
 (0)