blob: e09a59bc2ab3600959b44f39f796e16e728be3a3 [file] [log] [blame]
// 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',
);
}