@@ -251,3 +251,260 @@ def test_none_handling(self):
251251 assert proto_utils .ToProto .provider (None ) is None
252252 assert proto_utils .ToProto .security (None ) is None
253253 assert proto_utils .ToProto .security_schemes (None ) is None
254+
255+ def test_metadata_conversion (self ):
256+ """Test metadata conversion with various data types."""
257+ metadata = {
258+ 'null_value' : None ,
259+ 'bool_value' : True ,
260+ 'int_value' : 42 ,
261+ 'float_value' : 3.14 ,
262+ 'string_value' : 'hello' ,
263+ 'dict_value' : {'nested' : 'dict' , 'count' : 10 },
264+ 'list_value' : [1 , 'two' , 3.0 , True , None ],
265+ 'tuple_value' : (1 , 2 , 3 ),
266+ 'complex_list' : [
267+ {'name' : 'item1' , 'values' : [1 , 2 , 3 ]},
268+ {'name' : 'item2' , 'values' : [4 , 5 , 6 ]},
269+ ],
270+ }
271+
272+ # Convert to proto
273+ proto_metadata = proto_utils .ToProto .metadata (metadata )
274+ assert proto_metadata is not None
275+
276+ # Convert back to Python
277+ roundtrip_metadata = proto_utils .FromProto .metadata (proto_metadata )
278+
279+ # Verify all values are preserved correctly
280+ assert roundtrip_metadata ['null_value' ] is None
281+ assert roundtrip_metadata ['bool_value' ] is True
282+ assert roundtrip_metadata ['int_value' ] == 42
283+ assert roundtrip_metadata ['float_value' ] == 3.14
284+ assert roundtrip_metadata ['string_value' ] == 'hello'
285+ assert roundtrip_metadata ['dict_value' ]['nested' ] == 'dict'
286+ assert roundtrip_metadata ['dict_value' ]['count' ] == 10
287+ assert roundtrip_metadata ['list_value' ] == [1 , 'two' , 3.0 , True , None ]
288+ assert roundtrip_metadata ['tuple_value' ] == [
289+ 1 ,
290+ 2 ,
291+ 3 ,
292+ ] # tuples become lists
293+ assert len (roundtrip_metadata ['complex_list' ]) == 2
294+ assert roundtrip_metadata ['complex_list' ][0 ]['name' ] == 'item1'
295+
296+ def test_metadata_with_custom_objects (self ):
297+ """Test metadata conversion with custom objects using preprocessing utility."""
298+
299+ class CustomObject :
300+ def __str__ (self ):
301+ return 'custom_object_str'
302+
303+ def __repr__ (self ):
304+ return 'CustomObject()'
305+
306+ metadata = {
307+ 'custom_obj' : CustomObject (),
308+ 'list_with_custom' : [1 , CustomObject (), 'text' ],
309+ 'nested_custom' : {'obj' : CustomObject (), 'normal' : 'value' },
310+ }
311+
312+ # Use preprocessing utility to make it serializable
313+ serializable_metadata = proto_utils .make_dict_serializable (metadata )
314+
315+ # Convert to proto
316+ proto_metadata = proto_utils .ToProto .metadata (serializable_metadata )
317+ assert proto_metadata is not None
318+
319+ # Convert back to Python
320+ roundtrip_metadata = proto_utils .FromProto .metadata (proto_metadata )
321+
322+ # Custom objects should be converted to strings
323+ assert roundtrip_metadata ['custom_obj' ] == 'custom_object_str'
324+ assert roundtrip_metadata ['list_with_custom' ] == [
325+ 1 ,
326+ 'custom_object_str' ,
327+ 'text' ,
328+ ]
329+ assert roundtrip_metadata ['nested_custom' ]['obj' ] == 'custom_object_str'
330+ assert roundtrip_metadata ['nested_custom' ]['normal' ] == 'value'
331+
332+ def test_metadata_edge_cases (self ):
333+ """Test metadata conversion with edge cases."""
334+ metadata = {
335+ 'empty_dict' : {},
336+ 'empty_list' : [],
337+ 'zero' : 0 ,
338+ 'false' : False ,
339+ 'empty_string' : '' ,
340+ 'unicode_string' : 'string test' ,
341+ 'safe_number' : 9007199254740991 , # JavaScript MAX_SAFE_INTEGER
342+ 'negative_number' : - 42 ,
343+ 'float_precision' : 0.123456789 ,
344+ 'numeric_string' : '12345' ,
345+ }
346+
347+ # Convert to proto and back
348+ proto_metadata = proto_utils .ToProto .metadata (metadata )
349+ roundtrip_metadata = proto_utils .FromProto .metadata (proto_metadata )
350+
351+ # Verify edge cases are handled correctly
352+ assert roundtrip_metadata ['empty_dict' ] == {}
353+ assert roundtrip_metadata ['empty_list' ] == []
354+ assert roundtrip_metadata ['zero' ] == 0
355+ assert roundtrip_metadata ['false' ] is False
356+ assert roundtrip_metadata ['empty_string' ] == ''
357+ assert roundtrip_metadata ['unicode_string' ] == 'string test'
358+ assert roundtrip_metadata ['safe_number' ] == 9007199254740991
359+ assert roundtrip_metadata ['negative_number' ] == - 42
360+ assert abs (roundtrip_metadata ['float_precision' ] - 0.123456789 ) < 1e-10
361+ assert roundtrip_metadata ['numeric_string' ] == '12345'
362+
363+ def test_make_dict_serializable (self ):
364+ """Test the make_dict_serializable utility function."""
365+
366+ class CustomObject :
367+ def __str__ (self ):
368+ return 'custom_str'
369+
370+ test_data = {
371+ 'string' : 'hello' ,
372+ 'int' : 42 ,
373+ 'float' : 3.14 ,
374+ 'bool' : True ,
375+ 'none' : None ,
376+ 'custom' : CustomObject (),
377+ 'list' : [1 , 'two' , CustomObject ()],
378+ 'tuple' : (1 , 2 , CustomObject ()),
379+ 'nested' : {'inner_custom' : CustomObject (), 'inner_normal' : 'value' },
380+ }
381+
382+ result = proto_utils .make_dict_serializable (test_data )
383+
384+ # Basic types should be unchanged
385+ assert result ['string' ] == 'hello'
386+ assert result ['int' ] == 42
387+ assert result ['float' ] == 3.14
388+ assert result ['bool' ] is True
389+ assert result ['none' ] is None
390+
391+ # Custom objects should be converted to strings
392+ assert result ['custom' ] == 'custom_str'
393+ assert result ['list' ] == [1 , 'two' , 'custom_str' ]
394+ assert result ['tuple' ] == [1 , 2 , 'custom_str' ] # tuples become lists
395+ assert result ['nested' ]['inner_custom' ] == 'custom_str'
396+ assert result ['nested' ]['inner_normal' ] == 'value'
397+
398+ def test_normalize_large_integers_to_strings (self ):
399+ """Test the normalize_large_integers_to_strings utility function."""
400+
401+ test_data = {
402+ 'small_int' : 42 ,
403+ 'large_int' : 9999999999999999999 , # > 15 digits
404+ 'negative_large' : - 9999999999999999999 ,
405+ 'float' : 3.14 ,
406+ 'string' : 'hello' ,
407+ 'list' : [123 , 9999999999999999999 , 'text' ],
408+ 'nested' : {'inner_large' : 9999999999999999999 , 'inner_small' : 100 },
409+ }
410+
411+ result = proto_utils .normalize_large_integers_to_strings (test_data )
412+
413+ # Small integers should remain as integers
414+ assert result ['small_int' ] == 42
415+ assert isinstance (result ['small_int' ], int )
416+
417+ # Large integers should be converted to strings
418+ assert result ['large_int' ] == '9999999999999999999'
419+ assert isinstance (result ['large_int' ], str )
420+ assert result ['negative_large' ] == '-9999999999999999999'
421+ assert isinstance (result ['negative_large' ], str )
422+
423+ # Other types should be unchanged
424+ assert result ['float' ] == 3.14
425+ assert result ['string' ] == 'hello'
426+
427+ # Lists should be processed recursively
428+ assert result ['list' ] == [123 , '9999999999999999999' , 'text' ]
429+
430+ # Nested dicts should be processed recursively
431+ assert result ['nested' ]['inner_large' ] == '9999999999999999999'
432+ assert result ['nested' ]['inner_small' ] == 100
433+
434+ def test_parse_string_integers_in_dict (self ):
435+ """Test the parse_string_integers_in_dict utility function."""
436+
437+ test_data = {
438+ 'regular_string' : 'hello' ,
439+ 'numeric_string_small' : '123' , # small, should stay as string
440+ 'numeric_string_large' : '9999999999999999999' , # > 15 digits, should become int
441+ 'negative_large_string' : '-9999999999999999999' ,
442+ 'float_string' : '3.14' , # not all digits, should stay as string
443+ 'mixed_string' : '123abc' , # not all digits, should stay as string
444+ 'int' : 42 ,
445+ 'list' : ['hello' , '9999999999999999999' , '123' ],
446+ 'nested' : {
447+ 'inner_large_string' : '9999999999999999999' ,
448+ 'inner_regular' : 'value' ,
449+ },
450+ }
451+
452+ result = proto_utils .parse_string_integers_in_dict (test_data )
453+
454+ # Regular strings should remain unchanged
455+ assert result ['regular_string' ] == 'hello'
456+ assert (
457+ result ['numeric_string_small' ] == '123'
458+ ) # too small, stays string
459+ assert result ['float_string' ] == '3.14' # not all digits
460+ assert result ['mixed_string' ] == '123abc' # not all digits
461+
462+ # Large numeric strings should be converted to integers
463+ assert result ['numeric_string_large' ] == 9999999999999999999
464+ assert isinstance (result ['numeric_string_large' ], int )
465+ assert result ['negative_large_string' ] == - 9999999999999999999
466+ assert isinstance (result ['negative_large_string' ], int )
467+
468+ # Other types should be unchanged
469+ assert result ['int' ] == 42
470+
471+ # Lists should be processed recursively
472+ assert result ['list' ] == ['hello' , 9999999999999999999 , '123' ]
473+
474+ # Nested dicts should be processed recursively
475+ assert result ['nested' ]['inner_large_string' ] == 9999999999999999999
476+ assert result ['nested' ]['inner_regular' ] == 'value'
477+
478+ def test_large_integer_roundtrip_with_utilities (self ):
479+ """Test large integer handling with preprocessing and post-processing utilities."""
480+
481+ original_data = {
482+ 'large_int' : 9999999999999999999 ,
483+ 'small_int' : 42 ,
484+ 'nested' : {'another_large' : 12345678901234567890 , 'normal' : 'text' },
485+ }
486+
487+ # Step 1: Preprocess to convert large integers to strings
488+ preprocessed = proto_utils .normalize_large_integers_to_strings (
489+ original_data
490+ )
491+
492+ # Step 2: Convert to proto
493+ proto_metadata = proto_utils .ToProto .metadata (preprocessed )
494+ assert proto_metadata is not None
495+
496+ # Step 3: Convert back from proto
497+ dict_from_proto = proto_utils .FromProto .metadata (proto_metadata )
498+
499+ # Step 4: Post-process to convert large integer strings back to integers
500+ final_result = proto_utils .parse_string_integers_in_dict (
501+ dict_from_proto
502+ )
503+
504+ # Verify roundtrip preserved the original data
505+ assert final_result ['large_int' ] == 9999999999999999999
506+ assert isinstance (final_result ['large_int' ], int )
507+ assert final_result ['small_int' ] == 42
508+ assert final_result ['nested' ]['another_large' ] == 12345678901234567890
509+ assert isinstance (final_result ['nested' ]['another_large' ], int )
510+ assert final_result ['nested' ]['normal' ] == 'text'
0 commit comments