@@ -361,6 +361,23 @@ impl Cheatcode for writeJson_1Call {
361
361
}
362
362
}
363
363
364
+ impl Cheatcode for writeJsonUpsertCall {
365
+ fn apply ( & self , state : & mut Cheatcodes ) -> Result {
366
+ let Self { json : value, path, valueKey } = self ;
367
+
368
+ // Read, parse, and update the JSON object
369
+ let data_path = state. config . ensure_path_allowed ( path, FsAccessKind :: Read ) ?;
370
+ let data_string = fs:: read_to_string ( & data_path) ?;
371
+ let mut data =
372
+ serde_json:: from_str ( & data_string) . unwrap_or_else ( |_| Value :: String ( data_string) ) ;
373
+ upsert_json_value ( & mut data, value, valueKey) ?;
374
+
375
+ // Write the updated content back to the file
376
+ let json_string = serde_json:: to_string_pretty ( & data) ?;
377
+ super :: fs:: write_file ( state, path. as_ref ( ) , json_string. as_bytes ( ) )
378
+ }
379
+ }
380
+
364
381
pub ( super ) fn check_json_key_exists ( json : & str , key : & str ) -> Result {
365
382
let json = parse_json_str ( json) ?;
366
383
let values = select ( & json, key) ?;
@@ -643,11 +660,68 @@ pub(super) fn resolve_type(type_description: &str) -> Result<DynSolType> {
643
660
bail ! ( "type description should be a valid Solidity type or a EIP712 `encodeType` string" )
644
661
}
645
662
663
+ /// Upserts a value into a JSON object based on a dot-separated key.
664
+ ///
665
+ /// This function navigates through a mutable `serde_json::Value` object using a
666
+ /// path-like key. It creates nested JSON objects if they do not exist along the path.
667
+ /// The value is inserted at the final key in the path.
668
+ ///
669
+ /// # Arguments
670
+ ///
671
+ /// * `data` - A mutable reference to the `serde_json::Value` to be modified.
672
+ /// * `value` - The string representation of the value to upsert. This string is first parsed as
673
+ /// JSON, and if that fails, it's treated as a plain JSON string.
674
+ /// * `key` - A dot-separated string representing the path to the location for upserting.
675
+ pub ( super ) fn upsert_json_value ( data : & mut Value , value : & str , key : & str ) -> Result < ( ) > {
676
+ // Parse the path key into segments.
677
+ let canonical_key = canonicalize_json_path ( key) ;
678
+ let parts: Vec < & str > = canonical_key
679
+ . strip_prefix ( "$." )
680
+ . unwrap_or ( key)
681
+ . split ( '.' )
682
+ . filter ( |s| !s. is_empty ( ) )
683
+ . collect ( ) ;
684
+
685
+ if parts. is_empty ( ) {
686
+ return Err ( fmt_err ! ( "'valueKey' cannot be empty or just '$'" ) ) ;
687
+ }
688
+
689
+ // Separate the final key from the path.
690
+ // Traverse the objects, creating intermediary ones if necessary.
691
+ if let Some ( ( key_to_insert, path_to_parent) ) = parts. split_last ( ) {
692
+ let mut current_level = data;
693
+
694
+ for segment in path_to_parent {
695
+ if !current_level. is_object ( ) {
696
+ return Err ( fmt_err ! ( "path segment '{segment}' does not resolve to an object." ) ) ;
697
+ }
698
+ current_level = current_level
699
+ . as_object_mut ( )
700
+ . unwrap ( )
701
+ . entry ( segment. to_string ( ) )
702
+ . or_insert ( Value :: Object ( Map :: new ( ) ) ) ;
703
+ }
704
+
705
+ // Upsert the new value
706
+ if let Some ( parent_obj) = current_level. as_object_mut ( ) {
707
+ parent_obj. insert (
708
+ key_to_insert. to_string ( ) ,
709
+ serde_json:: from_str ( value) . unwrap_or_else ( |_| Value :: String ( value. to_owned ( ) ) ) ,
710
+ ) ;
711
+ } else {
712
+ return Err ( fmt_err ! ( "final destination is not an object, cannot insert key." ) ) ;
713
+ }
714
+ }
715
+
716
+ Ok ( ( ) )
717
+ }
718
+
646
719
#[ cfg( test) ]
647
720
mod tests {
648
721
use super :: * ;
649
722
use alloy_primitives:: FixedBytes ;
650
723
use proptest:: strategy:: Strategy ;
724
+ use serde_json:: json;
651
725
652
726
fn contains_tuple ( value : & DynSolValue ) -> bool {
653
727
match value {
@@ -717,4 +791,51 @@ mod tests {
717
791
assert_eq!( value, v) ;
718
792
}
719
793
}
794
+
795
+ #[ test]
796
+ fn test_upsert_json_value ( ) {
797
+ // Tuples of: (initial_json, key, value_to_upsert, expected)
798
+ let scenarios = vec ! [
799
+ // Simple key-value insert with a plain string
800
+ ( json!( { } ) , "foo" , r#""bar""# , json!( { "foo" : "bar" } ) ) ,
801
+ // Overwrite existing value with a number
802
+ ( json!( { "foo" : "bar" } ) , "foo" , "123" , json!( { "foo" : 123 } ) ) ,
803
+ // Create nested objects
804
+ ( json!( { } ) , "a.b.c" , r#""baz""# , json!( { "a" : { "b" : { "c" : "baz" } } } ) ) ,
805
+ // Upsert into existing nested object with a boolean
806
+ ( json!( { "a" : { "b" : { } } } ) , "a.b.c" , "true" , json!( { "a" : { "b" : { "c" : true } } } ) ) ,
807
+ // Upsert a JSON object as a value
808
+ ( json!( { } ) , "a.b" , r#"{"d": "e"}"# , json!( { "a" : { "b" : { "d" : "e" } } } ) ) ,
809
+ // Upsert a JSON array as a value
810
+ ( json!( { } ) , "myArray" , r#"[1, "test", null]"# , json!( { "myArray" : [ 1 , "test" , null] } ) ) ,
811
+ ] ;
812
+
813
+ for ( mut initial, key, value_str, expected) in scenarios {
814
+ upsert_json_value ( & mut initial, value_str, key) . unwrap ( ) ;
815
+ assert_eq ! ( initial, expected) ;
816
+ }
817
+
818
+ let error_scenarios = vec ! [
819
+ // Path traverses a non-object value
820
+ (
821
+ json!( { "a" : "a string value" } ) ,
822
+ "a.b" ,
823
+ r#""bar""# ,
824
+ "final destination is not an object, cannot insert key." ,
825
+ ) ,
826
+ // Empty key should fail
827
+ ( json!( { } ) , "" , r#""bar""# , "'valueKey' cannot be empty or just '$'" ) ,
828
+ // Root path with a trailing dot should fail
829
+ ( json!( { } ) , "$." , r#""bar""# , "'valueKey' cannot be empty or just '$'" ) ,
830
+ ] ;
831
+
832
+ for ( mut initial, key, value_str, error_msg) in error_scenarios {
833
+ let result = upsert_json_value ( & mut initial, value_str, key) ;
834
+ assert ! ( result. is_err( ) , "Expected an error for key: '{key}' but got Ok" ) ;
835
+ assert ! (
836
+ result. unwrap_err( ) . to_string( ) . contains( error_msg) ,
837
+ "Error message for key '{key}' did not contain '{error_msg}'"
838
+ ) ;
839
+ }
840
+ }
720
841
}
0 commit comments