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
113 changes: 113 additions & 0 deletions autumn-cli/src/generate/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,34 @@ fn autumn_web_feature_markers(feature: &str) -> &'static [&'static str] {
// the type (e.g. the template-shipped `tests/integration_test.rs`'s
// "Add DB-backed tests with `TestDb`...") doesn't count as usage.
"test-support" => &["TestDb::"],
// Attachment scaffolds (`generate scaffold ... --attachment`) enable
// both `multipart` and `storage`, but a hand-written route can also
// use these APIs directly. Destroying the last attachment model must
// not strip a feature such a route still needs (PR #1867 review).
//
// Bare `Multipart` (not `extract::Multipart`) so the marker also
// catches a hand-written route that pulls the extractor in through
// `use autumn_web::prelude::*;` and names it UNQUALIFIED — the prelude
// re-exports `Multipart` (`autumn/src/prelude.rs`), so such a route
// contains neither `extract::Multipart` nor any storage path, and the
// narrower marker would let `destroy` wrongly strip the feature (PR
// #1867 review follow-up). This still matches the fully-qualified
// `autumn_web::extract::Multipart` the scaffold itself emits and any
// `use ...::Multipart;` import; the only extra hits are identifiers
// like `MultipartField`/`MultipartError`, which are themselves part of
// the multipart API surface, so over-retaining on them is harmless.
"multipart" => &["Multipart"],
// `autumn_web::storage::` covers the model's blob column type
// (`autumn_web::storage::Blob`) as well as route usage of the store
// (`autumn_web::storage::BlobStoreState`, `save_to_blob_store`
// call sites that reference the `autumn_web::storage::` path, etc.).
// Unlike `multipart`, the prelude does NOT re-export any storage type
// (`Blob`, `BlobStore`, `BlobStoreState`, …), so a hand-written
// storage user must reach them through a `autumn_web::storage::…`
// path — either fully qualified or via a `use autumn_web::storage::{…}`
// import line — both of which this marker already catches. There is no
// prelude-unqualified spelling to miss, so no extra marker is needed.
"storage" => &["autumn_web::storage::"],
_ => &[],
}
}
Expand Down Expand Up @@ -2202,4 +2230,89 @@ mod tests {
just like every other now-empty generated directory"
);
}

#[test]
fn multipart_and_storage_have_feature_markers() {
// Attachment scaffolds enable `autumn-web/multipart` and
// `autumn-web/storage`; both must carry markers so `autumn destroy`
// of the last attachment model can't strip a feature a hand-written
// route still uses (PR #1867 review).
// The marker is the bare `Multipart` substring so it also catches a
// route that names the extractor unqualified via `use
// autumn_web::prelude::*;` (the prelude re-exports `Multipart`), not
// just the fully-qualified `autumn_web::extract::Multipart` spelling.
let multipart = autumn_web_feature_markers("multipart");
assert!(
multipart.contains(&"Multipart"),
"multipart marker must catch bare `Multipart` (covers prelude-unqualified \
and `autumn_web::extract::Multipart` usage), got {multipart:?}"
);

let storage = autumn_web_feature_markers("storage");
assert!(
storage.contains(&"autumn_web::storage::"),
"storage marker must catch `autumn_web::storage::` usage, got {storage:?}"
);
}

#[test]
fn multipart_and_storage_markers_retain_features_for_handwritten_routes() {
// A hand-written route using the multipart extractor and the blob
// store must keep those features alive even when the model/route the
// attachment scaffold generated is being destroyed (excluded). The
// route deliberately imports through `use autumn_web::prelude::*;` and
// names `Multipart` UNQUALIFIED (the prelude re-exports it), the exact
// shape the narrower `extract::Multipart` marker used to miss; the
// blob store stays a `autumn_web::storage::` path since the prelude
// does not re-export storage types.
let tmp = tempfile::TempDir::new().unwrap();
fs::create_dir_all(tmp.path().join("src/routes")).unwrap();
let handwritten = tmp.path().join("src/routes/manual.rs");
fs::write(
&handwritten,
"use autumn_web::prelude::*;\n\n\
async fn upload(mut mp: Multipart, store: autumn_web::storage::BlobStoreState) {}\n",
)
.unwrap();

// The scaffold's own generated files are being destroyed — excluded
// from the scan so they can't self-retain — but the hand-written
// route is not, so both features must stay needed.
let excluding = vec![tmp.path().join("src/models/photo.rs")];
let overrides = HashMap::new();
assert!(
autumn_web_feature_still_needed_elsewhere(
"multipart",
tmp.path(),
&excluding,
&overrides
),
"multipart must be retained while a hand-written route uses the Multipart extractor"
);
assert!(
autumn_web_feature_still_needed_elsewhere(
"storage",
tmp.path(),
&excluding,
&overrides
),
"storage must be retained while a hand-written route references autumn_web::storage::"
);

// With the hand-written route removed too, nothing references the
// features anymore — they become free to strip.
fs::remove_file(&handwritten).unwrap();
assert!(!autumn_web_feature_still_needed_elsewhere(
"multipart",
tmp.path(),
&excluding,
&overrides
));
assert!(!autumn_web_feature_still_needed_elsewhere(
"storage",
tmp.path(),
&excluding,
&overrides
));
}
}
Loading
Loading