| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:fxtest/fxtest.dart'; |
| |
| /// Drills into a [Map] through a given sequence of keys. |
| /// These keys should be [String] values for [Map] keys, or [int] values |
| /// for array indices. |
| /// |
| /// If provided keys do not exist in a map, [fallback] will be returned. |
| /// |
| /// If bad indices are provided when encountering a map, a [BadMapPathException] |
| /// will be thrown. |
| /// |
| /// And lastly, if *extra* keys are found (a literal is encountered before all |
| /// keys are used), a [BadMapPathException] will be thrown. |
| /// |
| /// Usage: |
| /// ```dart |
| /// var data = <String, dynamic>{ |
| /// 'key': { |
| /// 'nested': 'value', |
| /// }, |
| /// 'list': ['of', 'data'], |
| /// 'some_numbers': [1, 3, 5], |
| /// }; |
| /// |
| /// >>> getMapPath<String>(data, ['key', 'nested']) |
| /// 'value' |
| /// |
| /// >>> getMapPath<String>(data, ['list', 1]) |
| /// 'data' |
| /// |
| /// >>> getMapPath<String>(data, ['missing', 'path']) |
| /// null |
| /// |
| /// >>> getMapPath<String>(data, ['missing', 'path'], fallback: 'A safe value') |
| /// 'A safe value' |
| /// |
| /// >>> getMapPath<int>(data, ['some_numbers', 1]) |
| /// 3 |
| /// |
| /// >>> getMapPath<String>(data, ['list', 7]) |
| /// // throws BadMapPathException() |
| /// ``` |
| T? getMapPath<T>( |
| Map<String, dynamic>? map, |
| List<dynamic> keys, { |
| // Optional default value in case the requisite entries are missing |
| T? fallback, |
| // If supplied, casts non-null values into their correct type |
| T? Function(dynamic val)? caster, |
| }) { |
| if (map == null) { |
| return fallback; |
| } |
| |
| final dynamic key = keys[0]; |
| final dynamic value = map[key]; |
| |
| if (key is! String || value == null || (value is String && value.isEmpty)) { |
| return fallback; |
| } |
| // We made it to the end! |
| if (keys.length == 1) { |
| if (caster == null) return value; |
| if (value is T) return value; |
| return caster(value); |
| } else if (value is Map<String, dynamic>) { |
| return getMapPath( |
| value, |
| keys.sublist(1, keys.length), |
| fallback: fallback, |
| caster: caster, |
| ); |
| } else if (value is List) { |
| return _getListPath( |
| value, |
| keys.sublist(1, keys.length), |
| fallback: fallback, |
| caster: caster, |
| ); |
| } |
| if (fallback != null) return fallback; |
| throw BadMapPathException( |
| 'Reached literal value before exhausting all keys', |
| ); |
| } |
| |
| /// [List]-based helper for [getMapPath]. |
| /// |
| /// Note: This function used to be woven into [getMapPath]'s own logic, but |
| /// along with leading to unwieldy code, this also created complications when |
| /// needing to handle lists-within-lists. |
| T? _getListPath<T>( |
| List<dynamic> data, |
| List<dynamic> keys, { |
| // Optional default value in case the requisite entries are missing |
| T? fallback, |
| // If supplied, casts non-null values into their correct type |
| T? Function(dynamic val)? caster, |
| }) { |
| final key = keys.first; |
| |
| // Length and type checks first, since unlike Maps which return `null` for bad |
| // keys, Lists throw exceptions. |
| if (key is! int) { |
| if (fallback != null) return fallback; |
| throw BadMapPathException('Reached list value without integer key'); |
| } |
| if (key >= data.length) { |
| return fallback; |
| } |
| |
| final value = data[key]; |
| |
| // We made it to the end! |
| if (keys.length == 1) { |
| if (caster == null) return value; |
| if (value is T) return value; |
| return caster(value); |
| } else if (value is Map<String, dynamic>) { |
| return getMapPath<T>( |
| value, |
| keys.sublist(1), |
| fallback: fallback, |
| caster: caster, |
| ); |
| } else if (value is List) { |
| return _getListPath<T>( |
| value, |
| keys.sublist(1), |
| fallback: fallback, |
| caster: caster, |
| ); |
| } |
| if (fallback != null) return fallback; |
| throw BadMapPathException( |
| 'Reached literal value before exhausting all keys', |
| ); |
| } |