@@ -19,6 +19,73 @@ extension NullableMapChecks<K, V> on Subject<Map<K, V>?> {
1919 }
2020}
2121
22+ /// Convert [object] to a pure JSON-like value.
23+ ///
24+ /// The result is similar to `jsonDecode(jsonEncode(object))` , but without
25+ /// passing through a serialized form.
26+ ///
27+ /// All JSON atoms (numbers, booleans, null, and strings) are used directly.
28+ /// All JSON containers (lists, and maps with string keys) are copied
29+ /// as their elements are converted recursively.
30+ /// For any other value, a dynamic call `.toJson()` is made and
31+ /// should return either a JSON atom or a JSON container.
32+ Object ? deepToJson (Object ? object) {
33+ // Implementation is based on the recursion underlying [jsonEncode],
34+ // at [_JsonStringifier.writeObject] in the stdlib's convert/json.dart .
35+ // (We leave out the cycle-checking, for simplicity / out of laziness.)
36+
37+ var (result, success) = _deeplyConvertShallowJsonValue (object);
38+ if (success) return result;
39+
40+ final Object ? shallowlyConverted;
41+ try {
42+ shallowlyConverted = (object as dynamic ).toJson ();
43+ } catch (e) {
44+ throw JsonUnsupportedObjectError (object, cause: e);
45+ }
46+
47+ (result, success) = _deeplyConvertShallowJsonValue (shallowlyConverted);
48+ if (success) return result;
49+ throw JsonUnsupportedObjectError (object);
50+ }
51+
52+ (Object ? result, bool success) _deeplyConvertShallowJsonValue (Object ? object) {
53+ final Object ? result;
54+ switch (object) {
55+ case null || bool () || String () || num ():
56+ result = object;
57+ case List ():
58+ result = object.map ((x) => deepToJson (x)).toList ();
59+ case Map () when object.keys.every ((k) => k is String ):
60+ result = object.map ((k, v) => MapEntry (k, deepToJson (v)));
61+ default :
62+ return (null , false );
63+ }
64+ return (result, true );
65+ }
66+
67+ extension JsonChecks on Subject <Object ?> {
68+ /// Expects that the value is deeply equal to [expected] ,
69+ /// after calling [deepToJson] on both.
70+ ///
71+ /// Deep equality is computed by [MapChecks.deepEquals]
72+ /// or [IterableChecks.deepEquals] .
73+ void jsonEquals (Object ? expected) {
74+ final expectedJson = deepToJson (expected);
75+ final actualJson = has ((e) => deepToJson (e), 'deepToJson' );
76+ switch (expectedJson) {
77+ case null || bool () || String () || num ():
78+ return actualJson.equals (expectedJson);
79+ case List ():
80+ return actualJson.isA <List >().deepEquals (expectedJson);
81+ case Map ():
82+ return actualJson.isA <Map >().deepEquals (expectedJson);
83+ case _:
84+ assert (false );
85+ }
86+ }
87+ }
88+
2289extension UriChecks on Subject <Uri > {
2390 Subject <String > get asString => has ((u) => u.toString (), 'toString' ); // TODO(checks): what's a good convention for this?
2491
0 commit comments