From e52a8ac245ded5aeb459912045e9ea3ec6dd542d Mon Sep 17 00:00:00 2001 From: Pana g Date: Mon, 16 Oct 2023 13:44:41 +0300 Subject: [PATCH 1/9] Add support for unflatten --- README.md | 59 ++++++++++++++++++++++++++++++++++++----------- example/main.dart | 18 +++++++++++---- lib/flat.dart | 49 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a5b45d6..2842b07 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Take a nested Map and flatten it with delimited keys. Entirely based on node.js [flat](https://www.npmjs.com/package/flat). -> It does not achieve feature parity yet, as some options are missing from `flatten` (maxDepth, transformKey) and `unflatten` is not yet implemented. +> It does not achieve feature parity yet, as some options are missing from `flatten` (maxDepth, transformKey). > Currently, it bails out of a tree if it finds something different than a `Map` or `List`. @@ -11,21 +11,54 @@ Take a nested Map and flatten it with delimited keys. Entirely based on node.js ```dart import 'package:flat/flat.dart'; -flatten({ - 'key1': {'keyA': 'valueI'}, - 'key2': {'keyB': 'valueII'}, - 'key3': { - 'a': { - 'b': {'c': 2} - } - } +flatten( + { + "a": 1, + "list1": ["item1", "item2"], + "f": { + "list2": ["item3", "item4", "item5"], + "g": 2, + "h": true, + "j": "text", + }, + }, + ); + +// { +// "a": 1, +// "list1.0": "item1", +// "list1.1": "item2", +// "f.list2.0": "item3", +// "f.list2.1": "item4", +// "f.list2.2": "item5", +// "f.g": 2, +// "f.h": true, +// "f.j": "text" +// } + +unflatten({ + "a": 1, + "list1.0": "item1", + "list1.1": "item2", + "f.list2.0": "item3", + "f.list2.1": "item4", + "f.list2.2": "item5", + "f.g": 2, + "f.h": true, + "f.j": "text" }); // { -// 'key1.keyA': 'valueI', -// 'key2.keyB': 'valueII', -// 'key3.a.b.c': 2 -// }; +// "a": 1, +// "list1": ["item1", "item2"], +// "f": { +// "list2": ["item3", "item4", "item5"], +// "g": 2, +// "h": true, +// "j": "text", +// }, +// } + ``` ## Options diff --git a/example/main.dart b/example/main.dart index 6d8a2bb..48c03c1 100644 --- a/example/main.dart +++ b/example/main.dart @@ -3,9 +3,19 @@ import 'package:flat/flat.dart'; // ignore_for_file: avoid_print void main() { - final flat = flatten({ - "a": 1, - "b": {"c": 2} - }); + final flat = flatten( + { + "a": 1, + "list1": ["item1", "item2"], + "f": { + "list2": ["item3", "item4", "item5"], + "g": 2, + "h": true, + "j": "text", + }, + }, + ); print(flat); + + print(unflatten(flat)); } diff --git a/lib/flat.dart b/lib/flat.dart index c9f0277..846306f 100644 --- a/lib/flat.dart +++ b/lib/flat.dart @@ -48,3 +48,52 @@ Map flatten( Map _listToMap(List list) => list.asMap().map((key, value) => MapEntry(key.toString(), value)); + +Map unflatten( + Map flatMap, { + String delimiter = ".", +}) { + final Map result = {}; + + flatMap.forEach((key, value) { + // Split the flattened key by the specified delimiter. + final keys = key.split( + delimiter, + ); + + dynamic current = result; + + for (int i = 0; i < keys.length; i++) { + final k = keys[i]; + if (i == keys.length - 1) { + if (_isInteger(k)) { + // This means that we have to do with a list instead + (current as List).add(value); + } else { + // If we're at the last key in the hierarchy, assign the value. + (current as Map)[k] = value; + } + } else { + // If the key doesn't exist, create a new map or list for it. + final nextKey = keys[i + 1]; + if (_isInteger(nextKey)) { + // This means that we have to do with a list so we create a list instead of a Map + (current as Map)[k] ??= []; + } else { + (current as Map)[k] ??= {}; + } + // Move one level deeper into the map. + current = current[k]; + } + } + }); + + return result; +} + +bool _isInteger(String? value) { + if (value == null) { + return false; + } + return int.tryParse(value) != null; +} From 58adc0a25011d0d2c62c201023afbb79d123d2fa Mon Sep 17 00:00:00 2001 From: Danilo Fuchs Date: Tue, 7 Nov 2023 09:37:17 -0300 Subject: [PATCH 2/9] chore: Move to separate files --- lib/flat.dart | 101 +---------------------------------------- lib/src/flatten.dart | 50 ++++++++++++++++++++ lib/src/unflatten.dart | 48 ++++++++++++++++++++ 3 files changed, 100 insertions(+), 99 deletions(-) create mode 100644 lib/src/flatten.dart create mode 100644 lib/src/unflatten.dart diff --git a/lib/flat.dart b/lib/flat.dart index 846306f..605f1ba 100644 --- a/lib/flat.dart +++ b/lib/flat.dart @@ -1,99 +1,2 @@ -/// Flatten a nested Map into a single level map -/// -/// If no [delimiter] is specified, will separate depth levels by `.`. -/// -/// If you don't want to flatten arrays (with 0, 1,... indexes), -/// use [safe] mode. -/// -/// To avoid circular reference issues or huge calculations, -/// you can specify the [maxDepth] the function will traverse. -Map flatten( - Map target, { - String delimiter = ".", - bool safe = false, - int? maxDepth, -}) { - final result = {}; - - void step( - Map obj, [ - String? previousKey, - int currentDepth = 1, - ]) { - obj.forEach((key, value) { - final newKey = previousKey != null ? "$previousKey$delimiter$key" : key; - - if (maxDepth != null && currentDepth >= maxDepth) { - result[newKey] = value; - return; - } - if (value is Map) { - return step(value, newKey, currentDepth + 1); - } - if (value is List && !safe) { - return step( - _listToMap(value), - newKey, - currentDepth + 1, - ); - } - result[newKey] = value; - }); - } - - step(target); - - return result; -} - -Map _listToMap(List list) => - list.asMap().map((key, value) => MapEntry(key.toString(), value)); - -Map unflatten( - Map flatMap, { - String delimiter = ".", -}) { - final Map result = {}; - - flatMap.forEach((key, value) { - // Split the flattened key by the specified delimiter. - final keys = key.split( - delimiter, - ); - - dynamic current = result; - - for (int i = 0; i < keys.length; i++) { - final k = keys[i]; - if (i == keys.length - 1) { - if (_isInteger(k)) { - // This means that we have to do with a list instead - (current as List).add(value); - } else { - // If we're at the last key in the hierarchy, assign the value. - (current as Map)[k] = value; - } - } else { - // If the key doesn't exist, create a new map or list for it. - final nextKey = keys[i + 1]; - if (_isInteger(nextKey)) { - // This means that we have to do with a list so we create a list instead of a Map - (current as Map)[k] ??= []; - } else { - (current as Map)[k] ??= {}; - } - // Move one level deeper into the map. - current = current[k]; - } - } - }); - - return result; -} - -bool _isInteger(String? value) { - if (value == null) { - return false; - } - return int.tryParse(value) != null; -} +export './src/flatten.dart'; +export './src/unflatten.dart'; diff --git a/lib/src/flatten.dart b/lib/src/flatten.dart new file mode 100644 index 0000000..b7f3665 --- /dev/null +++ b/lib/src/flatten.dart @@ -0,0 +1,50 @@ +/// Flatten a nested Map into a single level map +/// +/// If no [delimiter] is specified, will separate depth levels by `.`. +/// +/// If you don't want to flatten arrays (with 0, 1,... indexes), +/// use [safe] mode. +/// +/// To avoid circular reference issues or huge calculations, +/// you can specify the [maxDepth] the function will traverse. +Map flatten( + Map target, { + String delimiter = ".", + bool safe = false, + int? maxDepth, +}) { + final result = {}; + + void step( + Map obj, [ + String? previousKey, + int currentDepth = 1, + ]) { + obj.forEach((key, value) { + final newKey = previousKey != null ? "$previousKey$delimiter$key" : key; + + if (maxDepth != null && currentDepth >= maxDepth) { + result[newKey] = value; + return; + } + if (value is Map) { + return step(value, newKey, currentDepth + 1); + } + if (value is List && !safe) { + return step( + _listToMap(value), + newKey, + currentDepth + 1, + ); + } + result[newKey] = value; + }); + } + + step(target); + + return result; +} + +Map _listToMap(List list) => + list.asMap().map((key, value) => MapEntry(key.toString(), value)); \ No newline at end of file diff --git a/lib/src/unflatten.dart b/lib/src/unflatten.dart new file mode 100644 index 0000000..30faafd --- /dev/null +++ b/lib/src/unflatten.dart @@ -0,0 +1,48 @@ +Map unflatten( + Map flatMap, { + String delimiter = ".", +}) { + final Map result = {}; + + flatMap.forEach((key, value) { + // Split the flattened key by the specified delimiter. + final keys = key.split( + delimiter, + ); + + dynamic current = result; + + for (int i = 0; i < keys.length; i++) { + final k = keys[i]; + if (i == keys.length - 1) { + if (_isInteger(k)) { + // This means that we have to do with a list instead + (current as List).add(value); + } else { + // If we're at the last key in the hierarchy, assign the value. + (current as Map)[k] = value; + } + } else { + // If the key doesn't exist, create a new map or list for it. + final nextKey = keys[i + 1]; + if (_isInteger(nextKey)) { + // This means that we have to do with a list so we create a list instead of a Map + (current as Map)[k] ??= []; + } else { + (current as Map)[k] ??= {}; + } + // Move one level deeper into the map. + current = current[k]; + } + } + }); + + return result; +} + +bool _isInteger(String? value) { + if (value == null) { + return false; + } + return int.tryParse(value) != null; +} From 355e4c3229f1a2a7f99e8241de5c4ade4172d53f Mon Sep 17 00:00:00 2001 From: Danilo Fuchs Date: Tue, 7 Nov 2023 09:37:22 -0300 Subject: [PATCH 3/9] chore: Configure env --- .fvm/fvm_config.json | 4 ++++ .gitignore | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .fvm/fvm_config.json diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json new file mode 100644 index 0000000..f000541 --- /dev/null +++ b/.fvm/fvm_config.json @@ -0,0 +1,4 @@ +{ + "flutterSdkVersion": "3.13.8", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2e7f80d..f44a5cb 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,9 @@ doc/api/ # project includes source files written in JavaScript. *.js_ *.js.deps -*.js.map \ No newline at end of file +*.js.map + +# FVM config +.fvm/flutter_sdk +.vscode/settings.json + From 3042249f076a7c141d8c81567311bc7bcf6ee317 Mon Sep 17 00:00:00 2001 From: Danilo Fuchs Date: Tue, 7 Nov 2023 09:37:42 -0300 Subject: [PATCH 4/9] feat: Basic test --- test/unflatten_test.dart | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/unflatten_test.dart diff --git a/test/unflatten_test.dart b/test/unflatten_test.dart new file mode 100644 index 0000000..32bdc2f --- /dev/null +++ b/test/unflatten_test.dart @@ -0,0 +1,33 @@ +import 'package:flat/flat.dart'; +import 'package:test/test.dart'; + +void main() { + test('Unflattens map', () { + const obj = { + "a": 1, + "list1.0": "item1", + "list1.1": "item2", + "f.list2.0": "item3", + "f.list2.1": "item4", + "f.list2.2": "item5", + "f.g": 2, + "f.h": true, + "f.j": "text" + }; + + const expected = { + "a": 1, + "list1": ["item1", "item2"], + "f": { + "list2": ["item3", "item4", "item5"], + "g": 2, + "h": true, + "j": "text", + }, + }; + + final result = unflatten(obj); + + expect(result, expected); + }); +} From be591acba75a660a3aa0760079260b354606cd85 Mon Sep 17 00:00:00 2001 From: Danilo Fuchs Date: Tue, 7 Nov 2023 09:50:32 -0300 Subject: [PATCH 5/9] fix: Mixing flattened maps --- lib/src/unflatten.dart | 7 +- test/flat_test.dart | 162 -------------------------------------- test/flatten_test.dart | 165 +++++++++++++++++++++++++++++++++++++++ test/unflatten_test.dart | 91 ++++++++++++++------- 4 files changed, 235 insertions(+), 190 deletions(-) delete mode 100644 test/flat_test.dart create mode 100644 test/flatten_test.dart diff --git a/lib/src/unflatten.dart b/lib/src/unflatten.dart index 30faafd..fbd7df6 100644 --- a/lib/src/unflatten.dart +++ b/lib/src/unflatten.dart @@ -14,7 +14,12 @@ Map unflatten( for (int i = 0; i < keys.length; i++) { final k = keys[i]; - if (i == keys.length - 1) { + if (value is Map) { + for (final entry in value.entries) { + current[k] ??= {}; + current[k][entry.key] = entry.value; + } + } else if (i == keys.length - 1) { if (_isInteger(k)) { // This means that we have to do with a list instead (current as List).add(value); diff --git a/test/flat_test.dart b/test/flat_test.dart deleted file mode 100644 index 021090a..0000000 --- a/test/flat_test.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'dart:convert'; - -import 'package:flat/flat.dart'; -import 'package:test/test.dart'; - -void main() { - test('Flattens nested Map', () { - const obj = { - "a": 1, - "b": {"c": 2} - }; - - const expected = {"a": 1, "b.c": 2}; - - final result = flatten(obj); - - expect(result, expected); - }); - - test('Preserves empty Map', () { - const obj = {"a": {}}; - - const expected = {"a": {}}; - - final result = flatten(obj); - - expect(result, expected); - }); - - test('Preserves null', () { - const obj = {"a": null}; - - const expected = {"a": null}; - - final result = flatten(obj); - - expect(result, expected); - }); - - test('Preserves deep null', () { - const obj = { - "a": {"b": null} - }; - - const expected = {"a.b": null}; - - final result = flatten(obj); - - expect(result, expected); - }); - - test('Flattens List', () { - const obj = { - "a": "item", - "b": [0, 1] - }; - - const expected = {"a": "item", "b.0": 0, "b.1": 1}; - - final result = flatten(obj); - - expect(result, expected); - }); - - test('Flattens complex Map', () { - const obj = { - "a": 1, - "b": { - "c": 2, - "d": "asd", - "a": { - "a": [ - {"where": "a"} - ] - } - } - }; - - const expected = {"a": 1, "b.c": 2, "b.d": "asd", "b.a.a.0.where": "a"}; - - final result = flatten(obj); - - expect(result, expected); - }); - - test('Should delimit with optional delimiter parameter', () { - const obj = { - "a": 1, - "b": {"c": 2} - }; - - const expected = {"a": 1, "b_c": 2}; - - final result = flatten(obj, delimiter: "_"); - - expect(result, expected); - }); - - test('Should not flatten array when in safe mode', () { - const obj = { - "a": 1, - "b": { - "c": [ - {"d": 2} - ] - } - }; - - const expected = { - "a": 1, - "b.c": [ - {"d": 2} - ] - }; - - final result = flatten(obj, safe: true); - - expect(result, expected); - }); - - test('Should limit depth by maxDepth', () { - const obj = { - "a": { - "b": { - "c": { - "d": {"e": 1} - } - } - } - }; - - const maxDepth = 3; - - const expected = { - "a.b.c": { - "d": {"e": 1} - } - }; - - final result = flatten(obj, maxDepth: maxDepth); - - expect(result, expected); - }); - - test('flattens decoded json with lists', () { - // https://github.com/danilofuchs/flat/pull/6 - const obj = { - "mediaData": { - "resources": [ - {"uri": "spotify://", "mimeType": "audio/unknown"} - ] - } - }; - - final result = flatten(jsonDecode(jsonEncode(obj)) as Map); - - expect(result, { - "mediaData.resources.0.uri": "spotify://", - "mediaData.resources.0.mimeType": "audio/unknown" - }); - }); -} diff --git a/test/flatten_test.dart b/test/flatten_test.dart new file mode 100644 index 0000000..5201c8b --- /dev/null +++ b/test/flatten_test.dart @@ -0,0 +1,165 @@ +import 'dart:convert'; + +import 'package:flat/flat.dart'; +import 'package:test/test.dart'; + +void main() { + group('Flatten', () { + test('Flattens nested Map', () { + const obj = { + "a": 1, + "b": {"c": 2} + }; + + const expected = {"a": 1, "b.c": 2}; + + final result = flatten(obj); + + expect(result, expected); + }); + + test('Preserves empty Map', () { + const obj = {"a": {}}; + + const expected = {"a": {}}; + + final result = flatten(obj); + + expect(result, expected); + }); + + test('Preserves null', () { + const obj = {"a": null}; + + const expected = {"a": null}; + + final result = flatten(obj); + + expect(result, expected); + }); + + test('Preserves deep null', () { + const obj = { + "a": {"b": null} + }; + + const expected = {"a.b": null}; + + final result = flatten(obj); + + expect(result, expected); + }); + + test('Flattens List', () { + const obj = { + "a": "item", + "b": [0, 1] + }; + + const expected = {"a": "item", "b.0": 0, "b.1": 1}; + + final result = flatten(obj); + + expect(result, expected); + }); + + test('Flattens complex Map', () { + const obj = { + "a": 1, + "b": { + "c": 2, + "d": "asd", + "a": { + "a": [ + {"where": "a"} + ] + } + } + }; + + const expected = {"a": 1, "b.c": 2, "b.d": "asd", "b.a.a.0.where": "a"}; + + final result = flatten(obj); + + expect(result, expected); + }); + + test('Should delimit with optional delimiter parameter', () { + const obj = { + "a": 1, + "b": {"c": 2} + }; + + const expected = {"a": 1, "b_c": 2}; + + final result = flatten(obj, delimiter: "_"); + + expect(result, expected); + }); + + test('Should not flatten array when in safe mode', () { + const obj = { + "a": 1, + "b": { + "c": [ + {"d": 2} + ] + } + }; + + const expected = { + "a": 1, + "b.c": [ + {"d": 2} + ] + }; + + final result = flatten(obj, safe: true); + + expect(result, expected); + }); + + test('Should limit depth by maxDepth', () { + const obj = { + "a": { + "b": { + "c": { + "d": {"e": 1} + } + } + } + }; + + const maxDepth = 3; + + const expected = { + "a.b.c": { + "d": {"e": 1} + } + }; + + final result = flatten(obj, maxDepth: maxDepth); + + expect(result, expected); + }); + + test('flattens decoded json with lists', () { + // https://github.com/danilofuchs/flat/pull/6 + const obj = { + "mediaData": { + "resources": [ + {"uri": "spotify://", "mimeType": "audio/unknown"} + ] + } + }; + + final result = + flatten(jsonDecode(jsonEncode(obj)) as Map); + + expect(result, { + "mediaData.resources.0.uri": "spotify://", + "mediaData.resources.0.mimeType": "audio/unknown" + }); + }); + }); +} diff --git a/test/unflatten_test.dart b/test/unflatten_test.dart index 32bdc2f..e9e58f4 100644 --- a/test/unflatten_test.dart +++ b/test/unflatten_test.dart @@ -2,32 +2,69 @@ import 'package:flat/flat.dart'; import 'package:test/test.dart'; void main() { - test('Unflattens map', () { - const obj = { - "a": 1, - "list1.0": "item1", - "list1.1": "item2", - "f.list2.0": "item3", - "f.list2.1": "item4", - "f.list2.2": "item5", - "f.g": 2, - "f.h": true, - "f.j": "text" - }; - - const expected = { - "a": 1, - "list1": ["item1", "item2"], - "f": { - "list2": ["item3", "item4", "item5"], - "g": 2, - "h": true, - "j": "text", - }, - }; - - final result = unflatten(obj); - - expect(result, expected); + group('unflatten', () { + test('Nested once', () { + const obj = { + 'hello.world': 'good morning', + }; + + const expected = { + "hello": { + "world": "good morning", + }, + }; + + final result = unflatten(obj); + + expect(result, expected); + }); + + test('Nested twice', () { + const obj = { + 'hello.world.again': 'good morning', + }; + + const expected = { + "hello": { + "world": { + "again": 'good morning', + }, + } + }; + + final result = unflatten(obj); + + expect(result, expected); + }); + + test('Multiple Keys', () { + const obj = { + 'hello.lorem.ipsum': 'again', + 'hello.lorem.dolor': 'sit', + 'world.lorem.ipsum': 'again', + 'world.lorem.dolor': 'sit', + 'world': {'greet': 'hello'}, + }; + + const expected = { + "hello": { + "lorem": { + 'ipsum': 'again', + 'dolor': 'sit', + }, + }, + 'world': { + 'greet': 'hello', + 'lorem': { + 'ipsum': 'again', + 'dolor': 'sit', + }, + }, + }; + + final result = unflatten(obj); + + expect(result, expected); + }); }); } From 242390d3d9a86ea1165eb10bd40b49c80a5571da Mon Sep 17 00:00:00 2001 From: Danilo Fuchs Date: Tue, 7 Nov 2023 10:09:24 -0300 Subject: [PATCH 6/9] feat: Port flat.js tests --- lib/src/unflatten.dart | 5 ++ test/flat_test.dart | 35 ++++++++++ test/flatten_test.dart | 46 ++++++------- test/unflatten_test.dart | 144 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 test/flat_test.dart diff --git a/lib/src/unflatten.dart b/lib/src/unflatten.dart index fbd7df6..a8da8a4 100644 --- a/lib/src/unflatten.dart +++ b/lib/src/unflatten.dart @@ -1,3 +1,8 @@ +/// Unflatten a map with keys such as `a.b.c: value` to a nested Map structure `{a: {b: {c: value}}}`. +/// +/// If no [delimiter] is specified, will separate depth levels by `.`. +/// +/// Unflattens arrays given that the keys are integers. Map unflatten( Map flatMap, { String delimiter = ".", diff --git a/test/flat_test.dart b/test/flat_test.dart new file mode 100644 index 0000000..23a21b3 --- /dev/null +++ b/test/flat_test.dart @@ -0,0 +1,35 @@ +import 'package:flat/src/flatten.dart'; +import 'package:flat/src/unflatten.dart'; +import 'package:test/test.dart'; + +void main() { + group('flatten and unflatten', () { + test( + 'Order of keys should not be changed after round trip flatten/unflatten', + () { + const obj = { + 'b': 1, + 'abc': { + 'c': [ + { + 'd': 1, + 'bca': 1, + 'a': 1, + }, + ], + }, + 'a': 1, + }; + + final result = unflatten(flatten(obj)); + + expect(obj.keys, result.keys); + expect((obj['abc']! as Map).keys, (result['abc']! as Map).keys); + expect( + // ignore: avoid_dynamic_calls + (obj['abc']! as Map)['c'][0].keys, + (result['abc']! as Map).keys, + ); + }); + }); +} diff --git a/test/flatten_test.dart b/test/flatten_test.dart index 5201c8b..487c388 100644 --- a/test/flatten_test.dart +++ b/test/flatten_test.dart @@ -8,7 +8,7 @@ void main() { test('Flattens nested Map', () { const obj = { "a": 1, - "b": {"c": 2} + "b": {"c": 2}, }; const expected = {"a": 1, "b.c": 2}; @@ -40,7 +40,7 @@ void main() { test('Preserves deep null', () { const obj = { - "a": {"b": null} + "a": {"b": null}, }; const expected = {"a.b": null}; @@ -53,7 +53,7 @@ void main() { test('Flattens List', () { const obj = { "a": "item", - "b": [0, 1] + "b": [0, 1], }; const expected = {"a": "item", "b.0": 0, "b.1": 1}; @@ -71,10 +71,10 @@ void main() { "d": "asd", "a": { "a": [ - {"where": "a"} - ] - } - } + {"where": "a"}, + ], + }, + }, }; const expected = {"a": 1, "b.c": 2, "b.d": "asd", "b.a.a.0.where": "a"}; @@ -87,7 +87,7 @@ void main() { test('Should delimit with optional delimiter parameter', () { const obj = { "a": 1, - "b": {"c": 2} + "b": {"c": 2}, }; const expected = {"a": 1, "b_c": 2}; @@ -102,16 +102,16 @@ void main() { "a": 1, "b": { "c": [ - {"d": 2} - ] - } + {"d": 2}, + ], + }, }; const expected = { "a": 1, "b.c": [ - {"d": 2} - ] + {"d": 2}, + ], }; final result = flatten(obj, safe: true); @@ -124,18 +124,18 @@ void main() { "a": { "b": { "c": { - "d": {"e": 1} - } - } - } + "d": {"e": 1}, + }, + }, + }, }; const maxDepth = 3; const expected = { "a.b.c": { - "d": {"e": 1} - } + "d": {"e": 1}, + }, }; final result = flatten(obj, maxDepth: maxDepth); @@ -148,9 +148,9 @@ void main() { const obj = { "mediaData": { "resources": [ - {"uri": "spotify://", "mimeType": "audio/unknown"} - ] - } + {"uri": "spotify://", "mimeType": "audio/unknown"}, + ], + }, }; final result = @@ -158,7 +158,7 @@ void main() { expect(result, { "mediaData.resources.0.uri": "spotify://", - "mediaData.resources.0.mimeType": "audio/unknown" + "mediaData.resources.0.mimeType": "audio/unknown", }); }); }); diff --git a/test/unflatten_test.dart b/test/unflatten_test.dart index e9e58f4..f789a63 100644 --- a/test/unflatten_test.dart +++ b/test/unflatten_test.dart @@ -29,7 +29,27 @@ void main() { "world": { "again": 'good morning', }, - } + }, + }; + + final result = unflatten(obj); + + expect(result, expected); + }); + + test('Object inside array', () { + const obj = { + 'hello.0.again': 'good morning', + }; + + const expected = { + "hello": [ + { + "world": { + "again": 'good morning', + }, + }, + ], }; final result = unflatten(obj); @@ -66,5 +86,127 @@ void main() { expect(result, expected); }); + + test('nested objects do not clobber each other when a.b inserted before a', + () { + final obj = {}; + obj['foo.bar'] = {'t': 123}; + obj['foo'] = {'p': 333}; + + const expected = { + "foo": { + "bar": { + 't': 123, + }, + 'p': 333, + }, + }; + + final result = unflatten(obj); + + expect(result, expected); + }); + + test('Custom Delimiter', () { + const obj = { + 'hello world again': 'good morning', + }; + + const expected = { + "hello": { + "world": { + "again": 'good morning', + }, + }, + }; + + final result = unflatten(obj, delimiter: ' '); + + expect(result, expected); + }); + + test('Messy', () { + const obj = { + 'hello.world': 'again', + 'lorem.ipsum': 'another', + 'good.morning': { + 'hash.key': { + 'nested.deep': { + 'and.even.deeper.still': 'hello', + }, + }, + }, + 'good.morning.again': {'testing.this': 'out'}, + }; + + const expected = { + 'hello': {'world': 'again'}, + 'lorem': {'ipsum': 'another'}, + 'good': { + 'morning': { + 'hash': { + 'key': { + 'nested': { + 'deep': { + 'and': { + 'even': { + 'deeper': {'still': 'hello'}, + }, + }, + }, + }, + }, + }, + 'again': { + 'testing': {'this': 'out'}, + }, + }, + }, + }; + + final result = unflatten(obj); + + expect(result, expected); + }); + + test('Should be unflatten array', () { + const obj = { + 'a.0': 'foo', + 'a.1': 'bar', + }; + + const expected = { + 'a': ['foo', 'bar'], + }; + + final result = unflatten(obj); + + expect(result, expected); + }); + + test('Does not interpret keys with numbers as arrays', () { + const obj = {'1key.2_key': 'ok'}; + + const expected = { + '1key': {'2_key': 'ok'}, + }; + + final result = unflatten(obj); + + expect(result, expected); + }); + + test('Empty objects should not be removed', () { + const obj = {'foo': [], 'bar': {}}; + + const expected = { + 'foo': [], + 'bar': {}, + }; + + final result = unflatten(obj); + + expect(result, expected); + }); }); } From 41264560c19e382d521db4ae3f85e7be25f6fe0f Mon Sep 17 00:00:00 2001 From: Danilo Fuchs Date: Tue, 7 Nov 2023 10:14:25 -0300 Subject: [PATCH 7/9] fix: Remove support for nested maps --- lib/src/unflatten.dart | 9 ++-- test/unflatten_test.dart | 101 ++------------------------------------- 2 files changed, 7 insertions(+), 103 deletions(-) diff --git a/lib/src/unflatten.dart b/lib/src/unflatten.dart index a8da8a4..763d470 100644 --- a/lib/src/unflatten.dart +++ b/lib/src/unflatten.dart @@ -19,11 +19,10 @@ Map unflatten( for (int i = 0; i < keys.length; i++) { final k = keys[i]; - if (value is Map) { - for (final entry in value.entries) { - current[k] ??= {}; - current[k][entry.key] = entry.value; - } + if (value is Map || value is List) { + throw ArgumentError( + 'The value of the key $key is a ${value.runtimeType} which is not supported', + ); } else if (i == keys.length - 1) { if (_isInteger(k)) { // This means that we have to do with a list instead diff --git a/test/unflatten_test.dart b/test/unflatten_test.dart index f789a63..2245bcb 100644 --- a/test/unflatten_test.dart +++ b/test/unflatten_test.dart @@ -57,7 +57,7 @@ void main() { expect(result, expected); }); - test('Multiple Keys', () { + test('Fails on multiple Keys', () { const obj = { 'hello.lorem.ipsum': 'again', 'hello.lorem.dolor': 'sit', @@ -66,45 +66,7 @@ void main() { 'world': {'greet': 'hello'}, }; - const expected = { - "hello": { - "lorem": { - 'ipsum': 'again', - 'dolor': 'sit', - }, - }, - 'world': { - 'greet': 'hello', - 'lorem': { - 'ipsum': 'again', - 'dolor': 'sit', - }, - }, - }; - - final result = unflatten(obj); - - expect(result, expected); - }); - - test('nested objects do not clobber each other when a.b inserted before a', - () { - final obj = {}; - obj['foo.bar'] = {'t': 123}; - obj['foo'] = {'p': 333}; - - const expected = { - "foo": { - "bar": { - 't': 123, - }, - 'p': 333, - }, - }; - - final result = unflatten(obj); - - expect(result, expected); + expect(() => unflatten(obj), throwsArgumentError); }); test('Custom Delimiter', () { @@ -125,51 +87,7 @@ void main() { expect(result, expected); }); - test('Messy', () { - const obj = { - 'hello.world': 'again', - 'lorem.ipsum': 'another', - 'good.morning': { - 'hash.key': { - 'nested.deep': { - 'and.even.deeper.still': 'hello', - }, - }, - }, - 'good.morning.again': {'testing.this': 'out'}, - }; - - const expected = { - 'hello': {'world': 'again'}, - 'lorem': {'ipsum': 'another'}, - 'good': { - 'morning': { - 'hash': { - 'key': { - 'nested': { - 'deep': { - 'and': { - 'even': { - 'deeper': {'still': 'hello'}, - }, - }, - }, - }, - }, - }, - 'again': { - 'testing': {'this': 'out'}, - }, - }, - }, - }; - - final result = unflatten(obj); - - expect(result, expected); - }); - - test('Should be unflatten array', () { + test('Should unflatten array', () { const obj = { 'a.0': 'foo', 'a.1': 'bar', @@ -195,18 +113,5 @@ void main() { expect(result, expected); }); - - test('Empty objects should not be removed', () { - const obj = {'foo': [], 'bar': {}}; - - const expected = { - 'foo': [], - 'bar': {}, - }; - - final result = unflatten(obj); - - expect(result, expected); - }); }); } From c63ccb9d892a437b72607a83543a699eb4dd4cc8 Mon Sep 17 00:00:00 2001 From: Pana g Date: Tue, 7 Nov 2023 16:43:53 +0200 Subject: [PATCH 8/9] Fixed unflatten function to work with nested objects in arrays, fixed some tests --- example/main.dart | 5 ++++ lib/src/unflatten.dart | 51 +++++++++++++++++++++++----------------- test/flat_test.dart | 3 ++- test/unflatten_test.dart | 2 +- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/example/main.dart b/example/main.dart index 48c03c1..f0b5f35 100644 --- a/example/main.dart +++ b/example/main.dart @@ -9,6 +9,11 @@ void main() { "list1": ["item1", "item2"], "f": { "list2": ["item3", "item4", "item5"], + "list3": [ + {"item3": "item47"}, + {"item4": "item48"}, + {"item5": "item49"}, + ], "g": 2, "h": true, "j": "text", diff --git a/lib/src/unflatten.dart b/lib/src/unflatten.dart index 763d470..54ed102 100644 --- a/lib/src/unflatten.dart +++ b/lib/src/unflatten.dart @@ -10,38 +10,45 @@ Map unflatten( final Map result = {}; flatMap.forEach((key, value) { - // Split the flattened key by the specified delimiter. - final keys = key.split( - delimiter, - ); - + final keys = key.split(delimiter); dynamic current = result; for (int i = 0; i < keys.length; i++) { final k = keys[i]; - if (value is Map || value is List) { - throw ArgumentError( - 'The value of the key $key is a ${value.runtimeType} which is not supported', - ); - } else if (i == keys.length - 1) { + if (i == keys.length - 1) { + // Last key, assign the value if (_isInteger(k)) { - // This means that we have to do with a list instead - (current as List).add(value); + final int index = int.parse(k); + while ((current as List).length <= index) { + current.add(null); // Padding the array + } + current[index] = value; } else { - // If we're at the last key in the hierarchy, assign the value. - (current as Map)[k] = value; + if ((current as Map).containsKey(k)) { + throw ArgumentError('Cannot unflatten, key "$k" already exists'); + } + current[k] = value; } } else { - // If the key doesn't exist, create a new map or list for it. - final nextKey = keys[i + 1]; - if (_isInteger(nextKey)) { - // This means that we have to do with a list so we create a list instead of a Map - (current as Map)[k] ??= []; + // Not the last key, we might need to create a map or array + if (_isInteger(k)) { + final int index = int.parse(k); + while ((current as List).length <= index) { + current.add(null); // Padding the array + } + // Ensure that we have a Map at the index if the next key is not an integer + if (!_isInteger(keys[i + 1]) && + (current[index] == null || current[index] is! Map)) { + current[index] = {}; + } + current = current[index]; } else { - (current as Map)[k] ??= {}; + if ((current as Map)[k] == null) { + // Next key will tell us whether to create a list or a map + current[k] = _isInteger(keys[i + 1]) ? [] : {}; + } + current = current[k]; } - // Move one level deeper into the map. - current = current[k]; } } }); diff --git a/test/flat_test.dart b/test/flat_test.dart index 23a21b3..6cc965c 100644 --- a/test/flat_test.dart +++ b/test/flat_test.dart @@ -28,7 +28,8 @@ void main() { expect( // ignore: avoid_dynamic_calls (obj['abc']! as Map)['c'][0].keys, - (result['abc']! as Map).keys, + // ignore: avoid_dynamic_calls + (result['abc']! as Map)['c'][0].keys, ); }); }); diff --git a/test/unflatten_test.dart b/test/unflatten_test.dart index 2245bcb..69c71f5 100644 --- a/test/unflatten_test.dart +++ b/test/unflatten_test.dart @@ -39,7 +39,7 @@ void main() { test('Object inside array', () { const obj = { - 'hello.0.again': 'good morning', + 'hello.0.world.again': 'good morning', }; const expected = { From bbf4838a960f6e2b13ef5730f0772b4116fb3ec3 Mon Sep 17 00:00:00 2001 From: Danilo Fuchs Date: Tue, 7 Nov 2023 12:45:15 -0300 Subject: [PATCH 9/9] fix: Throw ArgumentError if not flat --- lib/src/flatten.dart | 2 +- lib/src/unflatten.dart | 13 +++++++++++++ test/unflatten_test.dart | 20 +++++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/src/flatten.dart b/lib/src/flatten.dart index b7f3665..c9f0277 100644 --- a/lib/src/flatten.dart +++ b/lib/src/flatten.dart @@ -47,4 +47,4 @@ Map flatten( } Map _listToMap(List list) => - list.asMap().map((key, value) => MapEntry(key.toString(), value)); \ No newline at end of file + list.asMap().map((key, value) => MapEntry(key.toString(), value)); diff --git a/lib/src/unflatten.dart b/lib/src/unflatten.dart index 54ed102..4c8b812 100644 --- a/lib/src/unflatten.dart +++ b/lib/src/unflatten.dart @@ -3,12 +3,25 @@ /// If no [delimiter] is specified, will separate depth levels by `.`. /// /// Unflattens arrays given that the keys are integers. +/// +/// Throws [ArgumentError] if any key is already a Map or List. +/// +/// Throws [ArgumentError] if there are conflicting keys. Map unflatten( Map flatMap, { String delimiter = ".", }) { final Map result = {}; + flatMap.forEach((key, value) { + if (value is Map) { + throw ArgumentError('Expected flat map, but key "$key" is a Map'); + } + if (value is List) { + throw ArgumentError('Expected flat map, but key "$key" is a List'); + } + }); + flatMap.forEach((key, value) { final keys = key.split(delimiter); dynamic current = result; diff --git a/test/unflatten_test.dart b/test/unflatten_test.dart index 69c71f5..4abc9dc 100644 --- a/test/unflatten_test.dart +++ b/test/unflatten_test.dart @@ -57,7 +57,7 @@ void main() { expect(result, expected); }); - test('Fails on multiple Keys', () { + test('Fails on repeated key', () { const obj = { 'hello.lorem.ipsum': 'again', 'hello.lorem.dolor': 'sit', @@ -69,6 +69,24 @@ void main() { expect(() => unflatten(obj), throwsArgumentError); }); + test('Fails on nested Map', () { + const obj = { + 'world': {'greet': 'hello'}, + }; + + expect(() => unflatten(obj), throwsArgumentError); + }); + + test('Fails on nested nested Map', () { + const obj = { + 'hello.lorem.ipsum': { + 'world': {'greet': 'hello'}, + }, + }; + + expect(() => unflatten(obj), throwsArgumentError); + }); + test('Custom Delimiter', () { const obj = { 'hello world again': 'good morning',