1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! Shared `Uuid` / `Option<Uuid>` primary-key shape detection.
//!
//! This module is the single source of truth for asking "does this PK
//! type need the `Uuid::now_v7()` codegen path?". It serves both
//! `user_attribute` (which decides whether to emit a fresh-UUID PK
//! setter for `init_superuser`, see issue #4237) and `model_derive`
//! (which uses `is_uuid_type` while wiring up auto-generated field
//! defaults). Centralising the detection here prevents the two macros
//! from drifting when we add support for additional UUID wrappers or
//! alternative path patterns. See issue #4246.
use syn::Type;
/// Inspect a syntactic type and report whether it is a `Uuid` (or an
/// `Option<Uuid>`) primary key.
///
/// Returns `(is_uuid, is_option)`:
/// - `is_uuid` is `true` when the (optionally `Option`-wrapped) type
/// has a final path segment of `Uuid`. Both bare `Uuid` and
/// fully-qualified `uuid::Uuid` resolve to `true`.
/// - `is_option` is `true` when the type is syntactically `Option<T>`
/// with a first generic *type* argument (the inner `T` itself is not
/// inspected for `is_option`; only its presence is required, so e.g.
/// bare `Option` with no generic arguments resolves to `false`).
///
/// Detection is deliberately last-segment only: the macros never see
/// the resolved type, so `MyUuid` or `UuidV4` correctly report
/// `is_uuid = false`.
pub(crate) fn pk_uuid_shape(ty: &Type) -> (bool, bool) {
fn last_segment_is(ty: &Type, name: &str) -> bool {
matches!(ty, Type::Path(p) if p.path.segments.last().is_some_and(|s| s.ident == name))
}
if let Type::Path(type_path) = ty
&& let Some(last_segment) = type_path.path.segments.last()
&& last_segment.ident == "Option"
&& let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(syn::GenericArgument::Type(inner)) = args.args.first()
{
return (last_segment_is(inner, "Uuid"), true);
}
(last_segment_is(ty, "Uuid"), false)
}
#[cfg(test)]
mod tests {
use super::pk_uuid_shape;
use syn::parse_quote;
// Regression coverage migrated from `user_attribute.rs` for issue
// #4237: the macro decides whether to emit a `Uuid::now_v7()` PK
// setter based on `pk_uuid_shape`. Each case below corresponds to a
// real-world PK declaration we expect users to write in
// `#[user(full = true)] #[model(...)] ...` types.
#[test]
fn pk_uuid_shape_detects_bare_uuid() {
// Arrange / Act
let ty: syn::Type = parse_quote!(Uuid);
// Assert — bare `Uuid` is the canonical superuser-PK shape.
assert_eq!(pk_uuid_shape(&ty), (true, false));
}
#[test]
fn pk_uuid_shape_detects_qualified_uuid_path() {
// Arrange / Act
let ty: syn::Type = parse_quote!(uuid::Uuid);
// Assert — fully-qualified path must still resolve, otherwise
// users who write `pub id: uuid::Uuid` would silently slip
// back into the nil-UUID bug.
assert_eq!(pk_uuid_shape(&ty), (true, false));
}
#[test]
fn pk_uuid_shape_detects_option_uuid() {
// Arrange / Act
let ty: syn::Type = parse_quote!(Option<Uuid>);
// Assert — `Option<Uuid>` is uncommon for a PK but legal; the
// macro must wrap the seed value in `Some(now_v7())` to keep
// type-checking happy.
assert_eq!(pk_uuid_shape(&ty), (true, true));
}
#[test]
fn pk_uuid_shape_detects_option_qualified_uuid() {
// Arrange / Act
let ty: syn::Type = parse_quote!(Option<uuid::Uuid>);
// Assert — Option around a fully-qualified Uuid still counts.
assert_eq!(pk_uuid_shape(&ty), (true, true));
}
#[test]
fn pk_uuid_shape_skips_integer_pk() {
// Arrange / Act
let ty: syn::Type = parse_quote!(i64);
// Assert — integer PK types must NOT receive the `now_v7()`
// assignment; the existing `Self::default()` path is correct
// for non-Uuid PKs and the macro must not corrupt that.
assert_eq!(pk_uuid_shape(&ty), (false, false));
}
#[test]
fn pk_uuid_shape_skips_string_pk() {
// Arrange / Act
let ty: syn::Type = parse_quote!(String);
// Assert — string PK (e.g. natural keys) is also untouched.
assert_eq!(pk_uuid_shape(&ty), (false, false));
}
#[test]
fn pk_uuid_shape_skips_option_non_uuid() {
// Arrange / Act
let ty: syn::Type = parse_quote!(Option<i64>);
// Assert — Option of a non-Uuid scalar must report
// `is_uuid = false`, so the macro emits no setter.
assert_eq!(pk_uuid_shape(&ty), (false, true));
}
#[test]
fn pk_uuid_shape_does_not_match_lookalike_named_types() {
// Arrange / Act — `MyUuid` shares a substring with `Uuid` but is
// a different identifier. The helper compares whole identifiers
// (not substrings), so it must not falsely seed a v7 UUID into
// an unrelated user-defined PK type.
let aliased: syn::Type = parse_quote!(MyUuid);
let suffixed: syn::Type = parse_quote!(UuidV4);
// Assert — neither lookalike resolves to `Uuid`, so the macro
// will skip the `now_v7()` setter (and the user's existing
// `Default` for the type takes effect, just as before).
assert_eq!(pk_uuid_shape(&aliased), (false, false));
assert_eq!(pk_uuid_shape(&suffixed), (false, false));
}
}