@@ -469,6 +469,21 @@ pub struct SchemaTypeInput {
469469 pub add : Option < Vec < ( String , Type ) > > ,
470470 /// Whether to derive Clone (default: true)
471471 pub derive_clone : bool ,
472+ /// Fields to wrap in `Option<T>` for partial updates.
473+ ///
474+ /// - `partial` (bare) = all fields become `Option<T>`
475+ /// - `partial = ["field1", "field2"]` = only listed fields become `Option<T>`
476+ /// - Fields already `Option<T>` are left unchanged.
477+ pub partial : Option < PartialMode > ,
478+ }
479+
480+ /// Mode for the `partial` keyword in schema_type!
481+ #[ derive( Clone , Debug ) ]
482+ pub enum PartialMode {
483+ /// All fields become Option<T>
484+ All ,
485+ /// Only listed fields become Option<T>
486+ Fields ( Vec < String > ) ,
472487}
473488
474489/// Helper struct to parse an add field: ("field_name": Type)
@@ -533,6 +548,7 @@ impl Parse for SchemaTypeInput {
533548 let mut rename = None ;
534549 let mut add = None ;
535550 let mut derive_clone = true ;
551+ let mut partial = None ;
536552
537553 // Parse optional parameters
538554 while input. peek ( Token ! [ , ] ) {
@@ -583,11 +599,27 @@ impl Parse for SchemaTypeInput {
583599 let value: syn:: LitBool = input. parse ( ) ?;
584600 derive_clone = value. value ( ) ;
585601 }
602+ "partial" => {
603+ if input. peek ( Token ! [ =] ) {
604+ // partial = ["field1", "field2"]
605+ input. parse :: < Token ! [ =] > ( ) ?;
606+ let content;
607+ let _ = bracketed ! ( content in input) ;
608+ let fields: Punctuated < LitStr , Token ! [ , ] > =
609+ content. parse_terminated ( |input| input. parse :: < LitStr > ( ) , Token ! [ , ] ) ?;
610+ partial = Some ( PartialMode :: Fields (
611+ fields. into_iter ( ) . map ( |s| s. value ( ) ) . collect ( ) ,
612+ ) ) ;
613+ } else {
614+ // bare `partial` — all fields
615+ partial = Some ( PartialMode :: All ) ;
616+ }
617+ }
586618 _ => {
587619 return Err ( syn:: Error :: new (
588620 ident. span ( ) ,
589621 format ! (
590- "unknown parameter: `{}`. Expected `omit`, `pick`, `rename`, `add`, or `clone `" ,
622+ "unknown parameter: `{}`. Expected `omit`, `pick`, `rename`, `add`, `clone`, or `partial `" ,
591623 ident_str
592624 ) ,
593625 ) ) ;
@@ -611,6 +643,7 @@ impl Parse for SchemaTypeInput {
611643 rename,
612644 add,
613645 derive_clone,
646+ partial,
614647 } )
615648 }
616649}
@@ -719,12 +752,36 @@ pub fn generate_schema_type_code(
719752 }
720753 }
721754
755+ // Validate partial fields exist (when specific fields are listed)
756+ if let Some ( PartialMode :: Fields ( ref partial_fields) ) = input. partial {
757+ for field in partial_fields {
758+ if !source_field_names. contains ( field) {
759+ return Err ( syn:: Error :: new_spanned (
760+ & input. source_type ,
761+ format ! (
762+ "partial field `{}` does not exist in type `{}`. Available fields: {:?}" ,
763+ field,
764+ source_type_name,
765+ source_field_names. iter( ) . collect:: <Vec <_>>( )
766+ ) ,
767+ ) ) ;
768+ }
769+ }
770+ }
771+
722772 // Build omit set (use Rust field names)
723773 let omit_set: HashSet < String > = input. omit . clone ( ) . unwrap_or_default ( ) . into_iter ( ) . collect ( ) ;
724774
725775 // Build pick set (use Rust field names)
726776 let pick_set: HashSet < String > = input. pick . clone ( ) . unwrap_or_default ( ) . into_iter ( ) . collect ( ) ;
727777
778+ // Build partial set
779+ let partial_all = matches ! ( input. partial, Some ( PartialMode :: All ) ) ;
780+ let partial_set: HashSet < String > = match & input. partial {
781+ Some ( PartialMode :: Fields ( fields) ) => fields. iter ( ) . cloned ( ) . collect ( ) ,
782+ _ => HashSet :: new ( ) ,
783+ } ;
784+
728785 // Build rename map: source_field_name -> new_field_name
729786 let rename_map: std:: collections:: HashMap < String , String > = input
730787 . rename
@@ -743,8 +800,8 @@ pub fn generate_schema_type_code(
743800 // Generate new struct with filtered fields
744801 let new_type_name = & input. new_type ;
745802 let mut field_tokens = Vec :: new ( ) ;
746- // Track field mappings for From impl: (new_field_ident, source_field_ident)
747- let mut field_mappings: Vec < ( syn:: Ident , syn:: Ident ) > = Vec :: new ( ) ;
803+ // Track field mappings for From impl: (new_field_ident, source_field_ident, wrapped_in_option )
804+ let mut field_mappings: Vec < ( syn:: Ident , syn:: Ident , bool ) > = Vec :: new ( ) ;
748805
749806 if let syn:: Fields :: Named ( fields_named) = & parsed_struct. fields {
750807 for field in & fields_named. named {
@@ -764,8 +821,15 @@ pub fn generate_schema_type_code(
764821 continue ;
765822 }
766823
767- // Get field components
768- let field_ty = & field. ty ;
824+ // Get field components, applying partial wrapping if needed
825+ let original_ty = & field. ty ;
826+ let should_wrap_option = ( partial_all || partial_set. contains ( & rust_field_name) )
827+ && !is_option_type ( original_ty) ;
828+ let field_ty: Box < dyn quote:: ToTokens > = if should_wrap_option {
829+ Box :: new ( quote ! { Option <#original_ty> } )
830+ } else {
831+ Box :: new ( quote ! { #original_ty } )
832+ } ;
769833 let vis = & field. vis ;
770834 let source_field_ident = field. ident . clone ( ) . unwrap ( ) ;
771835
@@ -811,7 +875,7 @@ pub fn generate_schema_type_code(
811875 } ) ;
812876
813877 // Track mapping: new field name <- source field name
814- field_mappings. push ( ( new_field_ident, source_field_ident) ) ;
878+ field_mappings. push ( ( new_field_ident, source_field_ident, should_wrap_option ) ) ;
815879 } else {
816880 // No rename, keep field with only serde attrs
817881 let field_ident = field. ident . clone ( ) . unwrap ( ) ;
@@ -822,7 +886,7 @@ pub fn generate_schema_type_code(
822886 } ) ;
823887
824888 // Track mapping: same name
825- field_mappings. push ( ( field_ident. clone ( ) , field_ident) ) ;
889+ field_mappings. push ( ( field_ident. clone ( ) , field_ident, should_wrap_option ) ) ;
826890 }
827891 }
828892 }
@@ -849,8 +913,12 @@ pub fn generate_schema_type_code(
849913 let from_impl = if input. add . is_none ( ) {
850914 let field_assignments: Vec < _ > = field_mappings
851915 . iter ( )
852- . map ( |( new_ident, source_ident) | {
853- quote ! { #new_ident: source. #source_ident }
916+ . map ( |( new_ident, source_ident, wrapped) | {
917+ if * wrapped {
918+ quote ! { #new_ident: Some ( source. #source_ident) }
919+ } else {
920+ quote ! { #new_ident: source. #source_ident }
921+ }
854922 } )
855923 . collect ( ) ;
856924
@@ -1051,6 +1119,119 @@ mod tests {
10511119 assert_eq ! ( add[ 0 ] . 0 , "tags" ) ;
10521120 }
10531121
1122+ // Tests for `partial` parameter
1123+
1124+ #[ test]
1125+ fn test_parse_schema_type_input_with_partial_all ( ) {
1126+ let tokens = quote:: quote!( UpdateUser from User , partial) ;
1127+ let input: SchemaTypeInput = syn:: parse2 ( tokens) . unwrap ( ) ;
1128+ assert ! ( matches!( input. partial, Some ( PartialMode :: All ) ) ) ;
1129+ }
1130+
1131+ #[ test]
1132+ fn test_parse_schema_type_input_with_partial_fields ( ) {
1133+ let tokens = quote:: quote!( UpdateUser from User , partial = [ "name" , "email" ] ) ;
1134+ let input: SchemaTypeInput = syn:: parse2 ( tokens) . unwrap ( ) ;
1135+ match input. partial {
1136+ Some ( PartialMode :: Fields ( fields) ) => {
1137+ assert_eq ! ( fields, vec![ "name" , "email" ] ) ;
1138+ }
1139+ _ => panic ! ( "Expected PartialMode::Fields" ) ,
1140+ }
1141+ }
1142+
1143+ #[ test]
1144+ fn test_parse_schema_type_input_with_pick_and_partial ( ) {
1145+ let tokens = quote:: quote!( UpdateUser from User , pick = [ "name" , "email" ] , partial) ;
1146+ let input: SchemaTypeInput = syn:: parse2 ( tokens) . unwrap ( ) ;
1147+ assert_eq ! ( input. pick. unwrap( ) , vec![ "name" , "email" ] ) ;
1148+ assert ! ( matches!( input. partial, Some ( PartialMode :: All ) ) ) ;
1149+ }
1150+
1151+ #[ test]
1152+ fn test_parse_schema_type_input_with_pick_and_partial_fields ( ) {
1153+ let tokens =
1154+ quote:: quote!( UpdateUser from User , pick = [ "name" , "email" ] , partial = [ "name" ] ) ;
1155+ let input: SchemaTypeInput = syn:: parse2 ( tokens) . unwrap ( ) ;
1156+ assert_eq ! ( input. pick. unwrap( ) , vec![ "name" , "email" ] ) ;
1157+ match input. partial {
1158+ Some ( PartialMode :: Fields ( fields) ) => {
1159+ assert_eq ! ( fields, vec![ "name" ] ) ;
1160+ }
1161+ _ => panic ! ( "Expected PartialMode::Fields" ) ,
1162+ }
1163+ }
1164+
1165+ #[ test]
1166+ fn test_generate_schema_type_code_with_partial_all ( ) {
1167+ let storage = vec ! [ create_test_struct_metadata(
1168+ "User" ,
1169+ "pub struct User { pub id: i32, pub name: String, pub bio: Option<String> }" ,
1170+ ) ] ;
1171+
1172+ let tokens = quote:: quote!( UpdateUser from User , partial) ;
1173+ let input: SchemaTypeInput = syn:: parse2 ( tokens) . unwrap ( ) ;
1174+ let result = generate_schema_type_code ( & input, & storage) ;
1175+
1176+ assert ! ( result. is_ok( ) ) ;
1177+ let output = result. unwrap ( ) . to_string ( ) ;
1178+ // id and name should be wrapped in Option, bio already Option stays unchanged
1179+ assert ! ( output. contains( "Option < i32 >" ) ) ;
1180+ assert ! ( output. contains( "Option < String >" ) ) ;
1181+ }
1182+
1183+ #[ test]
1184+ fn test_generate_schema_type_code_with_partial_fields ( ) {
1185+ let storage = vec ! [ create_test_struct_metadata(
1186+ "User" ,
1187+ "pub struct User { pub id: i32, pub name: String, pub email: String }" ,
1188+ ) ] ;
1189+
1190+ let tokens = quote:: quote!( UpdateUser from User , partial = [ "name" ] ) ;
1191+ let input: SchemaTypeInput = syn:: parse2 ( tokens) . unwrap ( ) ;
1192+ let result = generate_schema_type_code ( & input, & storage) ;
1193+
1194+ assert ! ( result. is_ok( ) ) ;
1195+ let output = result. unwrap ( ) . to_string ( ) ;
1196+ // name should be Option<String>, but id and email should remain unwrapped
1197+ assert ! ( output. contains( "UpdateUser" ) ) ;
1198+ }
1199+
1200+ #[ test]
1201+ fn test_generate_schema_type_code_partial_nonexistent_field ( ) {
1202+ let storage = vec ! [ create_test_struct_metadata(
1203+ "User" ,
1204+ "pub struct User { pub id: i32, pub name: String }" ,
1205+ ) ] ;
1206+
1207+ let tokens = quote:: quote!( UpdateUser from User , partial = [ "nonexistent" ] ) ;
1208+ let input: SchemaTypeInput = syn:: parse2 ( tokens) . unwrap ( ) ;
1209+ let result = generate_schema_type_code ( & input, & storage) ;
1210+
1211+ assert ! ( result. is_err( ) ) ;
1212+ let err = result. unwrap_err ( ) . to_string ( ) ;
1213+ assert ! ( err. contains( "does not exist" ) ) ;
1214+ assert ! ( err. contains( "nonexistent" ) ) ;
1215+ }
1216+
1217+ #[ test]
1218+ fn test_generate_schema_type_code_partial_from_impl_wraps_some ( ) {
1219+ let storage = vec ! [ create_test_struct_metadata(
1220+ "User" ,
1221+ "pub struct User { pub id: i32, pub name: String }" ,
1222+ ) ] ;
1223+
1224+ let tokens = quote:: quote!( UpdateUser from User , partial) ;
1225+ let input: SchemaTypeInput = syn:: parse2 ( tokens) . unwrap ( ) ;
1226+ let result = generate_schema_type_code ( & input, & storage) ;
1227+
1228+ assert ! ( result. is_ok( ) ) ;
1229+ let output = result. unwrap ( ) . to_string ( ) ;
1230+ // From impl should wrap values in Some()
1231+ assert ! ( output. contains( "Some (source . id)" ) ) ;
1232+ assert ! ( output. contains( "Some (source . name)" ) ) ;
1233+ }
1234+
10541235 // =========================================================================
10551236 // Tests for generate_schema_code() - success paths
10561237 // =========================================================================
0 commit comments