LCOV - code coverage report
Current view: top level - repository - remote_adapter.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 154 157 98.1 %
Date: 2021-12-09 18:46:36 Functions: 0 0 -

          Line data    Source code
       1             : part of flutter_data;
       2             : 
       3             : /// An adapter base class for all remote operations for type [T].
       4             : ///
       5             : /// Includes:
       6             : ///
       7             : ///  - Remote methods such as [_RemoteAdapter.findAll] or [_RemoteAdapter.save]
       8             : ///  - Configuration methods and getters like [_RemoteAdapter.baseUrl] or [_RemoteAdapter.urlForFindAll]
       9             : ///  - Serialization methods like [_RemoteAdapterSerialization.serialize]
      10             : ///  - Watch methods such as [_RemoteAdapterWatch.watchOneNotifier]
      11             : ///  - Access to the [_RemoteAdapter.graph] for subclasses or mixins
      12             : ///
      13             : /// This class is meant to be extended via mixing in new adapters.
      14             : /// This can be done with the [DataRepository] annotation on a [DataModel] class:
      15             : ///
      16             : /// ```
      17             : /// @JsonSerializable()
      18             : /// @DataRepository([MyAppAdapter])
      19             : /// class Todo with DataModel<Todo> {
      20             : ///   @override
      21             : ///   final int? id;
      22             : ///   final String title;
      23             : ///   final bool completed;
      24             : ///
      25             : ///   Todo({this.id, required this.title, this.completed = false});
      26             : /// }
      27             : /// ```
      28             : class RemoteAdapter<T extends DataModel<T>> = _RemoteAdapter<T>
      29             :     with
      30             :         _RemoteAdapterSerialization<T>,
      31             :         _RemoteAdapterOffline<T>,
      32             :         _RemoteAdapterWatch<T>;
      33             : 
      34             : abstract class _RemoteAdapter<T extends DataModel<T>> with _Lifecycle {
      35           1 :   @protected
      36             :   _RemoteAdapter(this.localAdapter, [this._oneProvider, this._allProvider]);
      37             : 
      38             :   @protected
      39             :   @visibleForTesting
      40             :   @nonVirtual
      41             :   final LocalAdapter<T> localAdapter;
      42             : 
      43             :   /// A [GraphNotifier] instance also available to adapters
      44           1 :   @protected
      45             :   @nonVirtual
      46           2 :   GraphNotifier get graph => localAdapter.graph;
      47             : 
      48             :   // None of these fields below can be late finals as they might be re-initialized
      49             :   Map<String, RemoteAdapter>? _adapters;
      50             :   bool? _remote;
      51             :   bool? _verbose;
      52             :   Reader? _read;
      53             : 
      54             :   /// All adapters for the relationship subgraph of [T] and their relationships.
      55             :   ///
      56             :   /// This [Map] is typically required when initializing new models, and passed as-is.
      57           1 :   @protected
      58             :   @nonVirtual
      59           1 :   Map<String, RemoteAdapter> get adapters => _adapters!;
      60             : 
      61             :   /// Give adapter subclasses access to the dependency injection system
      62           1 :   @protected
      63             :   @nonVirtual
      64           1 :   Reader get read => _read!;
      65             : 
      66             :   /// INTERNAL: DO NOT USE
      67           1 :   @visibleForTesting
      68             :   @protected
      69             :   @nonVirtual
      70           1 :   String get internalType => DataHelpers.getType<T>();
      71             : 
      72             :   /// The pluralized and downcased [DataHelpers.getType<T>] version of type [T]
      73             :   /// by default.
      74             :   ///
      75             :   /// Example: [T] as `Post` has a [type] of `posts`.
      76           1 :   @visibleForTesting
      77             :   @protected
      78           1 :   String get type => internalType;
      79             : 
      80             :   /// ONLY FOR FLUTTER DATA INTERNAL USE
      81             :   Watcher? internalWatch;
      82             : 
      83             :   final OneProvider<T>? _oneProvider;
      84             :   final AllProvider<T>? _allProvider;
      85             : 
      86             :   /// Returns the base URL for this type [T].
      87             :   ///
      88             :   /// Typically used in a generic adapter (i.e. one shared by all types)
      89             :   /// so it should be e.g. `http://jsonplaceholder.typicode.com/`
      90             :   ///
      91             :   /// For specific paths to this type [T], see [urlForFindAll], [urlForFindOne], etc
      92           1 :   @protected
      93           1 :   String get baseUrl => throw UnsupportedError('Please override baseUrl');
      94             : 
      95             :   /// Returns URL for [findAll]. Defaults to [type].
      96           1 :   @protected
      97           2 :   String urlForFindAll(Map<String, dynamic> params) => '$type';
      98             : 
      99             :   /// Returns HTTP method for [findAll]. Defaults to `GET`.
     100           1 :   @protected
     101             :   DataRequestMethod methodForFindAll(Map<String, dynamic> params) =>
     102             :       DataRequestMethod.GET;
     103             : 
     104             :   /// Returns URL for [findOne]. Defaults to [type]/[id].
     105           1 :   @protected
     106           2 :   String urlForFindOne(id, Map<String, dynamic> params) => '$type/$id';
     107             : 
     108             :   /// Returns HTTP method for [findOne]. Defaults to `GET`.
     109           1 :   @protected
     110             :   DataRequestMethod methodForFindOne(id, Map<String, dynamic> params) =>
     111             :       DataRequestMethod.GET;
     112             : 
     113             :   /// Returns URL for [save]. Defaults to [type]/[id] (if [id] is present).
     114           1 :   @protected
     115             :   String urlForSave(id, Map<String, dynamic> params) =>
     116           3 :       id != null ? '$type/$id' : type;
     117             : 
     118             :   /// Returns HTTP method for [save]. Defaults to `PATCH` if [id] is present,
     119             :   /// or `POST` otherwise.
     120           1 :   @protected
     121             :   DataRequestMethod methodForSave(id, Map<String, dynamic> params) =>
     122             :       id != null ? DataRequestMethod.PATCH : DataRequestMethod.POST;
     123             : 
     124             :   /// Returns URL for [delete]. Defaults to [type]/[id].
     125           1 :   @protected
     126           2 :   String urlForDelete(id, Map<String, dynamic> params) => '$type/$id';
     127             : 
     128             :   /// Returns HTTP method for [delete]. Defaults to `DELETE`.
     129           1 :   @protected
     130             :   DataRequestMethod methodForDelete(id, Map<String, dynamic> params) =>
     131             :       DataRequestMethod.DELETE;
     132             : 
     133             :   /// A [Map] representing default HTTP query parameters. Defaults to empty.
     134             :   ///
     135             :   /// It can return a [Future], so that adapters overriding this method
     136             :   /// have a chance to call async methods.
     137             :   ///
     138             :   /// Example:
     139             :   /// ```
     140             :   /// @override
     141             :   /// FutureOr<Map<String, dynamic>> get defaultParams async {
     142             :   ///   final token = await _localStorage.get('token');
     143             :   ///   return await super.defaultParams..addAll({'token': token});
     144             :   /// }
     145             :   /// ```
     146           1 :   @protected
     147           1 :   FutureOr<Map<String, dynamic>> get defaultParams => {};
     148             : 
     149             :   /// A [Map] representing default HTTP headers.
     150             :   ///
     151             :   /// Initial default is: `{'Content-Type': 'application/json'}`.
     152             :   ///
     153             :   /// It can return a [Future], so that adapters overriding this method
     154             :   /// have a chance to call async methods.
     155             :   ///
     156             :   /// Example:
     157             :   /// ```
     158             :   /// @override
     159             :   /// FutureOr<Map<String, String>> get defaultHeaders async {
     160             :   ///   final token = await _localStorage.get('token');
     161             :   ///   return await super.defaultHeaders..addAll({'Authorization': token});
     162             :   /// }
     163             :   /// ```
     164           1 :   @protected
     165             :   FutureOr<Map<String, String>> get defaultHeaders =>
     166           1 :       {'Content-Type': 'application/json'};
     167             : 
     168             :   // lifecycle methods
     169             : 
     170             :   @mustCallSuper
     171           1 :   Future<void> onInitialized() async {}
     172             : 
     173             :   @mustCallSuper
     174             :   @nonVirtual
     175           1 :   Future<RemoteAdapter<T>> initialize(
     176             :       {bool? remote,
     177             :       bool? verbose,
     178             :       required Map<String, RemoteAdapter> adapters,
     179             :       required Reader read}) async {
     180           1 :     if (isInitialized) return this as RemoteAdapter<T>;
     181             : 
     182             :     // initialize attributes
     183           1 :     _adapters = adapters;
     184           1 :     _remote = remote ?? true;
     185           1 :     _verbose = verbose ?? true;
     186           1 :     _read = read;
     187             : 
     188           3 :     await localAdapter.initialize();
     189             : 
     190             :     // hook for clients
     191           2 :     await onInitialized();
     192             : 
     193             :     return this as RemoteAdapter<T>;
     194             :   }
     195             : 
     196           1 :   @override
     197           2 :   bool get isInitialized => localAdapter.isInitialized;
     198             : 
     199           1 :   @override
     200             :   void dispose() {
     201           2 :     localAdapter.dispose();
     202             :   }
     203             : 
     204           1 :   void _assertInit() {
     205             :     assert(isInitialized, true);
     206             :   }
     207             : 
     208             :   // serialization interface
     209             : 
     210             :   /// Returns a [DeserializedData] object when deserializing a given [data].
     211             :   ///
     212             :   /// [key] can be used to supply a specific `key` when deserializing ONE model.
     213             :   @protected
     214             :   @visibleForTesting
     215             :   DeserializedData<T, DataModel> deserialize(Object data, {String key});
     216             : 
     217             :   /// Returns a serialized version of a model of [T],
     218             :   /// as a [Map<String, dynamic>] ready to be JSON-encoded.
     219             :   @protected
     220             :   @visibleForTesting
     221             :   Map<String, dynamic> serialize(T model);
     222             : 
     223             :   // caching
     224             : 
     225             :   /// Returns whether calling [findAll] should trigger a remote call.
     226             :   ///
     227             :   /// Meant to be overriden. Defaults to [remote].
     228           1 :   @protected
     229             :   bool shouldLoadRemoteAll(
     230             :     bool remote,
     231             :     Map<String, dynamic> params,
     232             :     Map<String, String> headers,
     233             :   ) =>
     234             :       remote;
     235             : 
     236             :   /// Returns whether calling [findOne] should initiate an HTTP call.
     237             :   ///
     238             :   /// Meant to be overriden. Defaults to [remote].
     239           1 :   @protected
     240             :   bool shouldLoadRemoteOne(
     241             :     dynamic id,
     242             :     bool remote,
     243             :     Map<String, dynamic> params,
     244             :     Map<String, String> headers,
     245             :   ) =>
     246             :       remote;
     247             : 
     248             :   // remote implementation
     249             : 
     250             :   @protected
     251             :   @visibleForTesting
     252           1 :   Future<List<T>> findAll({
     253             :     bool? remote,
     254             :     Map<String, dynamic>? params,
     255             :     Map<String, String>? headers,
     256             :     bool? syncLocal,
     257             :     OnData<List<T>>? onSuccess,
     258             :     OnDataError<List<T>>? onError,
     259             :   }) async {
     260           1 :     _assertInit();
     261           1 :     remote ??= _remote;
     262             :     syncLocal ??= false;
     263           3 :     params = await defaultParams & params;
     264           3 :     headers = await defaultHeaders & headers;
     265             : 
     266           1 :     if (!shouldLoadRemoteAll(remote!, params, headers)) {
     267           3 :       final models = localAdapter.findAll().toImmutableList();
     268           1 :       models.map((m) => m._initialize(adapters, save: true));
     269             :       return models;
     270             :     }
     271             : 
     272           2 :     final result = await sendRequest(
     273           5 :       baseUrl.asUri / urlForFindAll(params) & params,
     274           1 :       method: methodForFindAll(params),
     275             :       headers: headers,
     276             :       requestType: DataRequestType.findAll,
     277           1 :       key: internalType,
     278           1 :       onSuccess: (data) async {
     279             :         if (syncLocal!) {
     280           3 :           await localAdapter.clear();
     281             :         }
     282             :         final models = data != null
     283           3 :             ? deserialize(data as Object).models.toImmutableList()
     284           1 :             : <T>[];
     285             :         return onSuccess?.call(models) ?? models;
     286             :       },
     287             :       onError: onError,
     288             :     );
     289           1 :     return result ?? <T>[];
     290             :   }
     291             : 
     292             :   @protected
     293             :   @visibleForTesting
     294           1 :   Future<T?> findOne(
     295             :     final dynamic model, {
     296             :     bool? remote,
     297             :     Map<String, dynamic>? params,
     298             :     Map<String, String>? headers,
     299             :     OnData<T?>? onSuccess,
     300             :     OnDataError<T?>? onError,
     301             :   }) async {
     302           1 :     _assertInit();
     303             :     if (model == null) {
     304           1 :       throw AssertionError('Model must be not null');
     305             :     }
     306           1 :     remote ??= _remote;
     307             : 
     308           3 :     params = await defaultParams & params;
     309           3 :     headers = await defaultHeaders & headers;
     310             : 
     311           1 :     final id = _resolveId(model);
     312             : 
     313           1 :     if (!shouldLoadRemoteOne(id, remote!, params, headers)) {
     314           3 :       final key = graph.getKeyForId(internalType, id,
     315           2 :           keyIfAbsent: model is T ? model._key : null);
     316             :       if (key == null) {
     317             :         return null;
     318             :       }
     319           2 :       final newModel = localAdapter.findOne(key);
     320           2 :       newModel?._initialize(adapters, save: true);
     321             :       return newModel;
     322             :     }
     323             : 
     324           2 :     return await sendRequest(
     325           5 :       baseUrl.asUri / urlForFindOne(id, params) & params,
     326           1 :       method: methodForFindOne(id, params),
     327             :       headers: headers,
     328             :       requestType: DataRequestType.findOne,
     329           2 :       key: StringUtils.typify(internalType, id),
     330           1 :       onSuccess: (data) {
     331             :         final model = data != null
     332           2 :             ? deserialize(data as Map<String, dynamic>).model
     333             :             : null;
     334             :         return onSuccess?.call(model) ?? model;
     335             :       },
     336             :       onError: onError,
     337             :     );
     338             :   }
     339             : 
     340             :   @protected
     341             :   @visibleForTesting
     342           1 :   Future<T> save(
     343             :     T model, {
     344             :     bool? remote,
     345             :     Map<String, dynamic>? params,
     346             :     Map<String, String>? headers,
     347             :     OnData<T>? onSuccess,
     348             :     OnDataError<T>? onError,
     349             :   }) async {
     350           1 :     _assertInit();
     351           1 :     remote ??= _remote;
     352             : 
     353           3 :     params = await defaultParams & params;
     354           3 :     headers = await defaultHeaders & headers;
     355             : 
     356             :     // we ignore the `init` argument here as
     357             :     // saving locally requires initializing
     358           2 :     model._initialize(adapters, save: true);
     359             : 
     360           1 :     if (remote == false) {
     361             :       return model;
     362             :     }
     363             : 
     364           2 :     final body = json.encode(serialize(model));
     365             : 
     366           2 :     final result = await sendRequest(
     367           6 :       baseUrl.asUri / urlForSave(model.id, params) & params,
     368           2 :       method: methodForSave(model.id, params),
     369             :       headers: headers,
     370             :       body: body,
     371             :       requestType: DataRequestType.save,
     372           1 :       key: model._key,
     373           1 :       onSuccess: (data) {
     374             :         T _model;
     375             :         if (data == null) {
     376             :           // return "old" model if response was empty
     377           2 :           _model = model._initialize(adapters, save: true);
     378             :         } else {
     379             :           // deserialize already inits models
     380             :           // if model had a key already, reuse it
     381             :           final _newModel =
     382           2 :               deserialize(data as Map<String, dynamic>, key: model._key!)
     383           1 :                   .model!;
     384             : 
     385             :           // in the unlikely case where supplied key couldn't be used
     386             :           // ensure "old" copy of model carries the updated key
     387           4 :           if (model._key != null && model._key != _newModel._key) {
     388           3 :             graph.removeKey(model._key!);
     389           2 :             model._key = _newModel._key;
     390             :           }
     391             :           _model = _newModel;
     392             :         }
     393             :         return onSuccess?.call(_model) ?? _model;
     394             :       },
     395             :       onError: onError,
     396             :     );
     397             :     return result ?? model;
     398             :   }
     399             : 
     400             :   @protected
     401             :   @visibleForTesting
     402           1 :   Future<void> delete(
     403             :     dynamic model, {
     404             :     bool? remote,
     405             :     Map<String, dynamic>? params,
     406             :     Map<String, String>? headers,
     407             :     OnData<void>? onSuccess,
     408             :     OnDataError<void>? onError,
     409             :   }) async {
     410           1 :     _assertInit();
     411           1 :     remote ??= _remote;
     412             : 
     413           3 :     params = await defaultParams & params;
     414           3 :     headers = await defaultHeaders & headers;
     415             : 
     416           1 :     final id = _resolveId(model);
     417           1 :     final key = _keyForModel(model);
     418             : 
     419             :     if (key != null) {
     420           3 :       await localAdapter.delete(key);
     421             :     }
     422             : 
     423             :     if (remote!) {
     424           2 :       return await sendRequest(
     425           5 :         baseUrl.asUri / urlForDelete(id, params) & params,
     426           1 :         method: methodForDelete(id, params),
     427             :         headers: headers,
     428             :         requestType: DataRequestType.delete,
     429           2 :         key: StringUtils.typify(internalType, id),
     430             :         onSuccess: onSuccess,
     431             :         onError: onError,
     432             :       );
     433             :     }
     434             :   }
     435             : 
     436           1 :   @protected
     437             :   @visibleForTesting
     438           2 :   Future<void> clear() => localAdapter.clear();
     439             : 
     440             :   // http
     441             : 
     442             :   /// An [http.Client] used to make an HTTP request.
     443             :   ///
     444             :   /// This getter returns a new client every time
     445             :   /// as by default they are used once and then closed.
     446           0 :   @protected
     447             :   @visibleForTesting
     448           0 :   http.Client get httpClient => http.Client();
     449             : 
     450             :   /// The function used to perform an HTTP request and return an [R].
     451             :   ///
     452             :   /// **IMPORTANT**:
     453             :   ///  - [uri] takes the FULL `Uri` including query parameters
     454             :   ///  - [headers] does NOT include ANY defaults such as [defaultHeaders]
     455             :   ///  (unless you omit the argument, in which case defaults will be included)
     456             :   ///
     457             :   /// Example:
     458             :   ///
     459             :   /// ```
     460             :   /// await sendRequest(
     461             :   ///   baseUrl.asUri + 'token' & await defaultParams & {'a': 1},
     462             :   ///   headers: await defaultHeaders & {'a': 'b'},
     463             :   ///   onSuccess: (data) => data['token'] as String,
     464             :   /// );
     465             :   /// ```
     466             :   ///
     467             :   ///ignore: comment_references
     468             :   /// To build the URI you can use [String.asUri], [Uri.+] and [Uri.&].
     469             :   ///
     470             :   /// To merge headers and params with their defaults you can use the helper
     471             :   /// [Map<String, dynamic>.&].
     472             :   ///
     473             :   /// In addition, [onSuccess] is supplied to post-process the
     474             :   /// data in JSON format. Deserialization and initialization
     475             :   /// typically occur in this function.
     476             :   ///
     477             :   /// [onError] can also be supplied to override [_RemoteAdapter.onError].
     478             :   @protected
     479             :   @visibleForTesting
     480           1 :   FutureOr<R?> sendRequest<R>(
     481             :     final Uri uri, {
     482             :     DataRequestMethod method = DataRequestMethod.GET,
     483             :     Map<String, String>? headers,
     484             :     String? body,
     485             :     String? key,
     486             :     OnRawData<R>? onSuccess,
     487             :     OnDataError<R>? onError,
     488             :     DataRequestType requestType = DataRequestType.adhoc,
     489             :     bool omitDefaultParams = false,
     490             :   }) async {
     491             :     // callbacks
     492           0 :     onError ??= this.onError as OnDataError<R>;
     493             : 
     494           2 :     headers ??= await defaultHeaders;
     495             :     final _params =
     496           3 :         omitDefaultParams ? <String, dynamic>{} : await defaultParams;
     497             : 
     498             :     http.Response? response;
     499             :     Object? data;
     500             :     Object? error;
     501             :     StackTrace? stackTrace;
     502             : 
     503             :     try {
     504           3 :       final request = http.Request(method.toShortString(), uri & _params);
     505           2 :       request.headers.addAll(headers);
     506             :       if (body != null) {
     507           1 :         request.body = body;
     508             :       }
     509           3 :       final stream = await httpClient.send(request);
     510           2 :       response = await http.Response.fromStream(stream);
     511             :     } catch (err, stack) {
     512             :       error = err;
     513             :       stackTrace = stack;
     514             :     } finally {
     515           2 :       httpClient.close();
     516             :     }
     517             : 
     518             :     // response handling
     519             : 
     520             :     try {
     521           2 :       if (response?.body.isNotEmpty ?? false) {
     522           2 :         data = json.decode(response!.body);
     523             :       }
     524           1 :     } on FormatException catch (e) {
     525             :       error = e;
     526             :     }
     527             : 
     528           1 :     final code = response?.statusCode;
     529             : 
     530           1 :     if (_verbose!) {
     531           1 :       print(
     532           3 :           '[flutter_data] [$internalType] ${method.toShortString()} $uri [HTTP ${code ?? ''}]${body != null ? '\n -> body:\n $body' : ''}');
     533             :     }
     534             : 
     535           2 :     if (error == null && code != null && code >= 200 && code < 300) {
     536           1 :       return await onSuccess?.call(data);
     537             :     } else {
     538           1 :       final e = DataException(error ?? data!,
     539             :           stackTrace: stackTrace, statusCode: code);
     540             : 
     541           1 :       if (_verbose!) {
     542           3 :         print('[flutter_data] [$internalType] Error: $e');
     543             :       }
     544           1 :       return await onError(e);
     545             :     }
     546             :   }
     547             : 
     548             :   /// Implements global request error handling.
     549             :   ///
     550             :   /// Defaults to throw [e] unless it is an HTTP 404
     551             :   /// or an `OfflineException`.
     552             :   ///
     553             :   /// NOTE: `onError` arguments throughout the API are used
     554             :   /// to override this default behavior.
     555           1 :   @protected
     556             :   @visibleForTesting
     557             :   FutureOr<R?> onError<R>(DataException e) {
     558           3 :     if (e.statusCode == 404 || e is OfflineException) {
     559             :       return null;
     560             :     }
     561             :     throw e;
     562             :   }
     563             : 
     564             :   /// Initializes [model] making it ready to use with [DataModel] extensions.
     565             :   ///
     566             :   /// Optionally provide [key]. Use [save] to persist in local storage.
     567           1 :   @protected
     568             :   @visibleForTesting
     569             :   @nonVirtual
     570             :   T initializeModel(T model, {String? key, bool save = false}) {
     571           2 :     return model._initialize(adapters, key: key, save: save);
     572             :   }
     573             : 
     574           1 :   String? _resolveId(dynamic model) {
     575           2 :     final id = model is T ? model.id : model;
     576           1 :     return id?.toString();
     577             :   }
     578             : 
     579           1 :   String? _keyForModel(dynamic model) {
     580           1 :     final id = _resolveId(model);
     581           3 :     return graph.getKeyForId(internalType, id,
     582           2 :         keyIfAbsent: model is T ? model._key : null);
     583             :   }
     584             : }
     585             : 
     586             : /// A utility class used to return deserialized main [models] AND [included] models.
     587             : class DeserializedData<T, I> {
     588           1 :   const DeserializedData(this.models, {this.included = const []});
     589             :   final List<T> models;
     590             :   final List<I> included;
     591           3 :   T? get model => models.singleOrNull;
     592             : }
     593             : 
     594             : // ignore: constant_identifier_names
     595           2 : enum DataRequestMethod { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE }
     596             : 
     597             : extension _ToStringX on DataRequestMethod {
     598           4 :   String toShortString() => toString().split('.').last;
     599             : }
     600             : 
     601             : typedef OnData<R> = FutureOr<R> Function(R);
     602             : typedef OnRawData<R> = FutureOr<R?> Function(dynamic);
     603             : typedef OnDataError<R> = FutureOr<R?> Function(DataException);
     604             : 
     605             : // ignore: constant_identifier_names
     606           2 : enum DataRequestType {
     607             :   findAll,
     608             :   findOne,
     609             :   save,
     610             :   delete,
     611             :   adhoc,
     612             : }
     613             : 
     614             : extension _DataRequestTypeX on DataRequestType {
     615           4 :   String toShortString() => toString().split('.').last;
     616             : }
     617             : 
     618           1 : DataRequestType _getDataRequestType(String type) =>
     619           4 :     DataRequestType.values.singleWhere((_) => _.toShortString() == type);

Generated by: LCOV version 1.15