blob: 448de19253d4685443200be9c61afa2afa672d45 [file] [log] [blame]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Utility for indexing json data structures.
///
/// Each element of [path] should be a `String`, `int` or `(String, String)`.
///
/// For each element `key` of [path], recurse into [json].
///
/// If the `key` is a String, the next json structure should be a Map, and have
/// `key` as a property. Recurse into that property.
///
/// If `key` is an `int`, the next json structure must be a List, with that
/// index. Recurse into that index.
///
/// If `key` is a `(String k, String v)` the next json structure must be a List
/// of maps, one of them having the property `k` with value `v`, recurse into
/// that map.
///
/// If at some point the types don't match throw a [FormatException].
///
/// Returns the result as a [T].
T dig<T>(dynamic json, List<Object> path) {
var i = 0;
String currentElementType() => switch (json) {
Null _ => 'null',
num _ => 'a number',
String _ => 'a string',
List _ => 'a list',
Map _ => 'a map',
_ => throw ArgumentError('Bad json'),
};
String currentPath() =>
i == 0 ? 'root' : path.sublist(0, i).map((i) => '[$i]').join('');
for (; i < path.length; i++) {
outer:
switch (path[i]) {
case final String key:
if (json is! Map) {
throw FormatException(
'Expected a map at ${currentPath()}. '
'Found ${currentElementType()}.',
);
}
json = json[key];
case final int key:
if (json is! List) {
throw FormatException(
'Expected a list at ${currentPath()}. '
'Found ${currentElementType()}.',
);
}
if (key >= json.length) {
throw FormatException(
'Expected at least ${key + 1} element(s) at ${currentPath()}. '
'Found only ${json.length} element(s)',
);
}
json = json[key];
case (final String key, final String value):
if (json is! List) {
throw FormatException(
'Expected a list at ${currentPath()}. '
'Found ${currentElementType()}.',
);
}
final t = json;
for (var j = 0; j < t.length; j++) {
final element = t[j];
if (element is! Map) {
json = element;
throw FormatException(
'Expected a map at ${currentPath()}[$j]. '
'Found ${currentElementType()}.',
);
}
if (element[key] == value) {
json = element;
break outer;
}
}
throw FormatException(
'No element with $key=$value at ${currentPath()}',
);
case final key:
throw ArgumentError('Bad key $key in', 'path');
}
}
if (json is! T) {
final targetTypeName = switch (T) {
const (int) => 'an int',
const (double) => 'a number',
const (num) => 'a number',
const (String) => 'a string',
const (List) => 'a list',
const (Map) => 'a map',
const (List<Object?>) => 'a list',
const (Map<String, Object?>) => 'a map',
const (Map<String, dynamic>) => 'a map',
const (Null) => 'null',
_ => throw ArgumentError('$T is not a json type'),
};
throw FormatException(
'Expected $targetTypeName at ${currentPath()}. '
'Found ${currentElementType()}.',
);
}
return json;
}