| // Copyright 2014 The Flutter 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:meta/meta.dart' show immutable; |
| |
| /// A subtree of a JSON object as well as its path. |
| @immutable |
| class JsonContext<T> { |
| /// Create a [JsonContext] that represents a subtree. |
| const JsonContext(this.current, this.path); |
| |
| /// The content of the subtree. |
| final T current; |
| /// The path from the root. |
| final List<String> path; |
| |
| /// Create a [JsonContext] that represents the root of a tree. |
| static JsonContext<Map<String, dynamic>> root(Map<String, dynamic> root) { |
| return JsonContext<Map<String, dynamic>>(root, const <String>[]); |
| } |
| } |
| |
| /// A JSON object. |
| typedef JsonObject = Map<String, dynamic>; |
| |
| /// A JSON array. |
| typedef JsonArray = List<dynamic>; |
| |
| String _jsonTypeErrorMessage(List<String> currentPath, String nextKey, Type expectedType, Type actualType) { |
| return 'Unexpected value at path ${currentPath.join('.')}.$nextKey: ' |
| 'Expects $expectedType but got $actualType.'; |
| } |
| |
| /// Returns a JSON object's specified key. |
| /// |
| /// If the result is not of type `T`, throws an `ArgumentError`. |
| JsonContext<T> jsonGetKey<T>(JsonContext<JsonObject> context, String key) { |
| final dynamic result = context.current[key]; |
| if (result is! T) { |
| throw ArgumentError(_jsonTypeErrorMessage(context.path, key, T, result.runtimeType)); |
| } |
| return JsonContext<T>(result, <String>[...context.path, key]); |
| } |
| |
| /// Returns a JSON array's specified index. |
| /// |
| /// If the subtree is not of type `T`, throws an `ArgumentError`. |
| JsonContext<T> jsonGetIndex<T>(JsonContext<JsonArray> context, int index) { |
| final dynamic result = context.current[index]; |
| if (result is! T) { |
| throw ArgumentError(_jsonTypeErrorMessage(context.path, '$index', T, result.runtimeType)); |
| } |
| return JsonContext<T>(result, <String>[...context.path, '$index']); |
| } |
| |
| List<dynamic> _jsonPathSplit(String path) { |
| return path.split('.').map((String key) { |
| final int? index = int.tryParse(key); |
| if (index != null) { |
| return index; |
| } else { |
| return key; |
| } |
| }).toList(); |
| } |
| |
| /// Returns the value at `path` of a JSON tree. |
| /// |
| /// The path is split using `.`. Integral elements are considered as array |
| /// indexes, while others are considered as map indexes. |
| /// |
| /// If the final result is not of type `T`, throws an `ArgumentError`. |
| JsonContext<T> jsonGetPath<T>(JsonContext<dynamic> context, String path) { |
| JsonContext<dynamic> current = context; |
| void jsonGetKeyOrIndex<M>(dynamic key, int depth) { |
| assert(key is String || key is int, 'Key at $depth is a ${key.runtimeType}.'); |
| if (key is String) { |
| current = jsonGetKey<M>(current as JsonContext<JsonObject>, key); |
| } else if (key is int) { |
| current = jsonGetIndex<M>(current as JsonContext<JsonArray>, key); |
| } else { |
| assert(false); |
| } |
| } |
| void jsonGetKeyOrIndexForNext(dynamic key, dynamic nextKey, int depth) { |
| assert(nextKey is String || nextKey is int, 'Key at ${depth + 1} is a ${key.runtimeType}.'); |
| if (nextKey is String) { |
| jsonGetKeyOrIndex<JsonObject>(key, depth); |
| } else if (nextKey is int) { |
| jsonGetKeyOrIndex<JsonArray>(key, depth); |
| } else { |
| assert(false); |
| } |
| } |
| |
| final List<dynamic> pathSegments = _jsonPathSplit(path); |
| for (int depth = 0; depth < pathSegments.length; depth += 1) { |
| if (depth != pathSegments.length - 1) { |
| jsonGetKeyOrIndexForNext(pathSegments[depth], pathSegments[depth + 1], depth); |
| } else { |
| jsonGetKeyOrIndex<T>(pathSegments[depth], depth); |
| } |
| } |
| return current as JsonContext<T>; |
| } |