Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions codegen/src/v1/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use super::ops::Operations;

use crate::declare_codegen;
use crate::v2::post_object;

use heck::ToSnakeCase;
use scoped_writer::g;
Expand Down Expand Up @@ -52,12 +53,9 @@ pub fn codegen(ops: &Operations) {
g!("/// Checks whether the {} request has accesses to the resources.", op.name);
g!("/// ");
g!("/// This method returns `Ok(())` by default.");
if op.name == "PostObject" {
g!("/// ");
g!("/// POST Object is not a synonym for `PutObject`: the request carries a form (its fields,");
g!("/// the file name and the policy) and the response carries the `success_action_*`");
g!("/// semantics. The default path runs the `put_object` check as well, so this hook is an");
g!("/// addition to it rather than a replacement.");
// The synthetic operation adds POST-specific notes to its hook documentation.
if post_object::is_synthetic(&op.name) {
post_object::codegen_access_hook();
}
if op.is_minio {
g!("#[cfg(feature = \"minio\")]");
Expand Down
9 changes: 6 additions & 3 deletions codegen/src/v1/aws_conv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use super::dto::RustTypes;
use super::ops::Operations;
use super::ops::is_op_input;
use super::rust;
use crate::v2::post_object;

use crate::declare_codegen;

Expand All @@ -23,10 +24,12 @@ pub fn codegen(ops: &Operations, rust_types: &RustTypes) {
g!();

for (name, rust_type) in rust_types {
// PostObject is a synthetic API in s3s; aws-sdk-s3 has no corresponding types.
if post_object::is_synthetic_type(name) {
continue;
}

match name.as_str() {
// PostObject is a synthetic API in s3s; aws-sdk-s3 has no corresponding types.
"PostObjectInput" => continue,
"PostObjectOutput" => continue,
// ListenBucketNotification is a MinIO extension; aws-sdk-s3 has no corresponding types.
"ListenBucketNotificationInput" => continue,
"ListenBucketNotificationOutput" => continue,
Expand Down
3 changes: 2 additions & 1 deletion codegen/src/v1/aws_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use super::ops::Operations;
use super::rust;

use crate::declare_codegen;
use crate::v2::post_object;

use std::format as f;

Expand Down Expand Up @@ -35,7 +36,7 @@ pub fn codegen(ops: &Operations, rust_types: &RustTypes) {

for op in ops.values() {
// PostObject is a synthetic API in s3s; aws-sdk-s3 has no corresponding operation.
if op.name == "PostObject" {
if post_object::is_synthetic(&op.name) {
continue;
}
let method_name = op.name.to_snake_case();
Expand Down
133 changes: 7 additions & 126 deletions codegen/src/v1/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use super::{rust, smithy};

use crate::declare_codegen;
use crate::v1::Patch;
use crate::v2::post_object;

use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
Expand Down Expand Up @@ -300,55 +301,10 @@ pub fn collect_rust_types(model: &smithy::Model, ops: &Operations) -> RustTypes
patch_types(&mut space);
unify_operation_types(ops, &mut space);

// POST Object is not a Smithy-modeled operation in the upstream S3 model.
// We still want to distinguish it from PutObject at the trait layer.
// Fork the unified DTO types so behavior can stay identical,
// while leaving room to extend PostObject* with POST-only fields later.
for (src, dst) in [("PutObjectInput", "PostObjectInput"), ("PutObjectOutput", "PostObjectOutput")] {
if let Some(src_ty) = space.get(src).cloned() {
let mut dst_ty = src_ty;
match &mut dst_ty {
rust::Type::Struct(s) => {
dst.clone_into(&mut s.name);
}
_ => {
// PutObject{Input,Output} are expected to be structs.
unimplemented!("{src} is not a struct");
}
}
assert!(space.insert(dst.to_owned(), dst_ty).is_none());
}
}
// The synthetic operation is not in the upstream model; the v2 module owns it.
post_object::codegen_dto_clone(&mut space);

// Add POST Object specific fields to PostObjectInput
if let Some(rust::Type::Struct(post_in)) = space.get_mut("PostObjectInput") {
post_in.fields.push(rust::StructField {
name: o("success_action_redirect"),
type_: o("String"),
option_type: true,
position: o("s3s"),
doc: Some(o("The URL to which the client is redirected upon successful upload.")),
..rust::StructField::default()
});
post_in.fields.push(rust::StructField {
name: o("success_action_status"),
type_: o("i32"),
option_type: true,
position: o("s3s"),
doc: Some(o(
"The status code returned to the client upon successful upload. Valid values are 200, 201, and 204.",
)),
..rust::StructField::default()
});
post_in.fields.push(rust::StructField {
name: o("policy"),
type_: o("PostPolicy"),
option_type: true,
position: o("s3s"),
doc: Some(o("The POST policy document that was included in the request.")),
..rust::StructField::default()
});
}
post_object::codegen_post_only_fields(&mut space);

space
}
Expand Down Expand Up @@ -457,7 +413,7 @@ fn unify_operation_types(ops: &Operations, space: &mut RustTypes) {

// unify operation input type
for op in ops.values() {
if op.name == "PostObject" {
if post_object::is_synthetic(&op.name) {
continue;
}
if op.name == "SelectObjectContent" {
Expand All @@ -484,7 +440,7 @@ fn unify_operation_types(ops: &Operations, space: &mut RustTypes) {

// unify operation output type
for op in ops.values() {
if op.name == "PostObject" {
if post_object::is_synthetic(&op.name) {
continue;
}
let output_ty = if op.smithy_output == "Unit" {
Expand Down Expand Up @@ -699,88 +655,13 @@ pub fn codegen(rust_types: &RustTypes, ops: &Operations, patch: Option<Patch>) {
codegen_builders(rust_types, ops);

codegen_dto_ext(rust_types);
codegen_post_object_mapping_helpers(rust_types);
post_object::codegen_mapping_helpers(rust_types);

if matches!(patch, Some(Patch::Minio)) {
super::minio::codegen_in_dto();
}
}

fn codegen_post_object_mapping_helpers(rust_types: &RustTypes) {
let Some(rust::Type::Struct(put_in)) = rust_types.get("PutObjectInput") else { return };
let Some(rust::Type::Struct(put_out)) = rust_types.get("PutObjectOutput") else { return };
let Some(rust::Type::Struct(post_in)) = rust_types.get("PostObjectInput") else { return };
let Some(rust::Type::Struct(post_out)) = rust_types.get("PostObjectOutput") else { return };

// PostObjectInput has extra fields (success_action_redirect, success_action_status).
// We verify that the common fields (those from PutObjectInput) match.
assert!(post_in.fields.len() >= put_in.fields.len());
for (a, b) in put_in.fields.iter().zip(post_in.fields.iter()) {
assert_eq!(a.name, b.name);
assert_eq!(a.type_, b.type_);
assert_eq!(a.option_type, b.option_type);
}
assert_eq!(put_out.fields.len(), post_out.fields.len());
for (a, b) in put_out.fields.iter().zip(post_out.fields.iter()) {
assert_eq!(a.name, b.name);
assert_eq!(a.type_, b.type_);
assert_eq!(a.option_type, b.option_type);
}

// Collect POST-only field names (those not in PutObjectInput)
let put_in_field_names: std::collections::BTreeSet<_> = put_in.fields.iter().map(|f| f.name.as_str()).collect();
let post_only_fields: Vec<_> = post_in
.fields
.iter()
.filter(|f| !put_in_field_names.contains(f.name.as_str()))
.collect();

g!();
g([
"// NOTE: PostObject is a synthetic API in s3s.",
"// PostObjectInput has extra fields for POST-specific behavior (success_action_redirect, success_action_status).",
]);

g!("pub(crate) fn put_object_input_into_post_object_input(x: PutObjectInput) -> PostObjectInput {{");
g!(" PostObjectInput {{");
for field in &put_in.fields {
g!(" {}: x.{},", field.name, field.name);
}
// POST-only fields get default values
for field in &post_only_fields {
g!(" {}: None,", field.name);
}
g!(" }}");
g!("}}");

g!("pub(crate) fn post_object_input_into_put_object_input(x: PostObjectInput) -> PutObjectInput {{");
g!(" PutObjectInput {{");
// Only copy fields that exist in PutObjectInput
for field in &put_in.fields {
g!(" {}: x.{},", field.name, field.name);
}
g!(" }}");
g!("}}");

g!("pub(crate) fn put_object_output_into_post_object_output(x: PutObjectOutput) -> PostObjectOutput {{");
g!(" PostObjectOutput {{");
for field in &put_out.fields {
g!(" {}: x.{},", field.name, field.name);
}
g!(" }}");
g!("}}");

// This function is currently unused but kept for symmetry and potential future use
g!("#[allow(dead_code)]");
g!("pub(crate) fn post_object_output_into_put_object_output(x: PostObjectOutput) -> PutObjectOutput {{");
g!(" PutObjectOutput {{");
for field in &post_out.fields {
g!(" {}: x.{},", field.name, field.name);
}
g!(" }}");
g!("}}");
}

fn codegen_struct(ty: &rust::Struct, rust_types: &RustTypes, ops: &Operations, needs_serde: bool, needs_custom_default: bool) {
codegen_doc(ty.doc.as_deref());

Expand Down
56 changes: 6 additions & 50 deletions codegen/src/v1/gen_tests/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::v1::dto::RustTypes;
use crate::v1::ops::Operation;
use crate::v1::ops::Operations;
use crate::v1::rust;
use crate::v2::post_object;

use super::debug::Mode;
use super::debug::spec;
Expand All @@ -39,11 +40,6 @@ use heck::ToSnakeCase;
use s3s_model::error_codes;
use scoped_writer::g;

/// The synthetic multipart operation. Its input is parsed from a verified
/// multipart form, which needs a signed request, so the family covers it with a
/// direct call instead of a request-table entry.
const POST_OBJECT: &str = "PostObject";

/// Operations whose optional XML payload still needs a non-empty body: the
/// generated deserializer maps an empty body to `MalformedXML` for them.
const REQUIRES_BODY: &[&str] = &["CompleteMultipartUpload", "PutObjectLegalHold", "PutObjectRetention"];
Expand Down Expand Up @@ -77,7 +73,8 @@ pub(super) fn codegen(ops: &Operations, rust_types_base: &RustTypes, rust_types_
assert_eq!(
cases.len() + 1,
ops.len(),
"every operation except {POST_OBJECT} must have a request case"
"every operation except {} must have a request case",
post_object::name()
);

write_test_file("ops.rs", || {
Expand Down Expand Up @@ -311,22 +308,8 @@ fn emit_harness() {
" String::from_utf8_lossy(&bytes).into_owned()",
"}",
"",
"/// Builds a request for the direct call of the synthetic multipart operation.",
"fn post_object_request() -> S3Request<PostObjectInput> {",
" S3Request {",
" input: PostObjectInput::default(),",
" method: Method::POST,",
" uri: Uri::from_static(\"/bucket/key\"),",
" headers: HeaderMap::new(),",
" extensions: Extensions::new(),",
" credentials: None,",
" region: None,",
" service: None,",
" trailing_headers: None,",
" }",
"}",
"",
]);
post_object::codegen_test_fixture();

g([
"/// Every operation is reached through the real service pipeline, and the",
Expand Down Expand Up @@ -432,41 +415,14 @@ fn emit_harness() {
"",
]);

g([
"/// The synthetic POST Object operation is covered by a direct call: its",
"/// input comes from a verified multipart form, which the request table",
"/// cannot produce. The default body delegates to put_object.",
"#[tokio::test]",
"async fn post_object_delegates_to_put_object() {",
" let err = DefaultS3",
" .post_object(post_object_request())",
" .await",
" .expect_err(\"the default body delegates to put_object, which reports NotImplemented\");",
" assert_eq!(err.code().as_str(), \"NotImplemented\");",
"",
" let recorder = RecordingS3::default();",
" let err = recorder",
" .post_object(post_object_request())",
" .await",
" .expect_err(\"the recording double reports NotImplemented\");",
" assert_eq!(err.code().as_str(), \"NotImplemented\");",
" assert_eq!(recorder.take().as_slice(), [\"PostObject\"].as_slice());",
"",
" let mut req = post_object_request();",
" PassThroughAccess",
" .post_object(&mut req)",
" .await",
" .expect(\"the default access body allows the request\");",
"}",
"",
]);
post_object::codegen_generated_test();
}

fn collect_cases(ops: &Operations, types: &RustTypes) -> Vec<Case> {
let mut cases = Vec::new();

for op in ops.values() {
if op.name == POST_OBJECT {
if post_object::is_synthetic(&op.name) {
continue;
}

Expand Down
12 changes: 10 additions & 2 deletions codegen/src/v1/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023-2026 The s3s Authors

mod rust;
// `v2::post_object` reads the rust type model, so the module is visible crate-wide;
// this is a visibility change only and cannot change the generated output.
pub(crate) mod rust;
mod smithy;
mod utils;

Expand All @@ -28,12 +30,18 @@ use std::io::BufWriter;

pub use self::utils::o;

// `v2::post_object` owns the synthetic operation and reads these v1 items;
// re-exporting them is a visibility change only and cannot change what is generated.
pub(crate) use self::dto::RustTypes;
pub(crate) use self::ops::{OPS_GENERATED_DIR, Operation, Operations, codegen_file_header};

fn write_file(path: &str, f: impl FnOnce()) {
let mut writer = BufWriter::new(File::create(path).unwrap());
scoped_writer::scoped(&mut writer, f);
}

pub(super) fn write_dir_file(dir: &str, name: &str, f: impl FnOnce()) {
/// Visible to `v2::post_object`, which writes the operation module through it.
pub(crate) fn write_dir_file(dir: &str, name: &str, f: impl FnOnce()) {
std::fs::create_dir_all(dir).unwrap();
write_file(&format!("{dir}/{name}"), f);
}
Expand Down
3 changes: 2 additions & 1 deletion codegen/src/v1/oir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use super::ops::{OPS_GENERATED_DIR, Operations, PathPattern, codegen_file_header};
use super::write_dir_file;
use crate::v2::post_object;
use scoped_writer::g;
use std::collections::BTreeMap;

Expand Down Expand Up @@ -100,7 +101,7 @@ struct OirBucket {
fn collect_oir_buckets(ops: &Operations) -> Vec<OirBucket> {
let mut groups: BTreeMap<(String, PathPattern), Vec<(String, bool)>> = BTreeMap::new();
for op in ops.values() {
if op.name == "PostObject" {
if post_object::is_synthetic(&op.name) {
continue;
}
// The official `x-id` query value must equal the operation name so
Expand Down
Loading
Loading