LCOV - code coverage report
Current view: top level - src/repository - remote_adapter.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 138 147 93.9 %
Date: 2020-07-30 22:52:57 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.watchOne]
      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, this.title, this.completed = false});
      26             : /// }
      27             : /// ```
      28             : class RemoteAdapter<T extends DataModel<T>> = _RemoteAdapter<T>
      29             :     with _RemoteAdapterSerialization<T>, _RemoteAdapterWatch<T>;
      30             : 
      31             : abstract class _RemoteAdapter<T extends DataModel<T>>
      32             :     with _Lifecycle<_RemoteAdapter<T>> {
      33           1 :   @protected
      34             :   _RemoteAdapter(this.localAdapter);
      35             : 
      36             :   @protected
      37             :   @visibleForTesting
      38             :   final LocalAdapter<T> localAdapter;
      39             : 
      40             :   /// A [GraphNotifier] instance also available to adapters
      41           1 :   @protected
      42           2 :   GraphNotifier get graph => localAdapter.graph;
      43             : 
      44             :   /// All adapters for the relationship subgraph of [T] and their relationships.
      45             :   ///
      46             :   /// This [Map] is typically required when initializing new models, and passed as-is.
      47             :   @protected
      48             :   Map<String, RemoteAdapter> adapters;
      49             : 
      50             :   // late finals
      51             :   bool _remote;
      52             :   bool _verbose;
      53             : 
      54             :   /// Give adapter subclasses access to the dependency injection system
      55             :   @nonVirtual
      56             :   @protected
      57             :   ProviderReference ref;
      58             : 
      59             :   /// The pluralized and downcased [DataHelpers.getType<T>] version of type [T]
      60             :   ///
      61             :   /// Example: [T] as `Post` has a [type] of `posts`.
      62             :   @nonVirtual
      63             :   @protected
      64             :   final type = DataHelpers.getType<T>();
      65             : 
      66             :   /// Returns the base URL for this type [T].
      67             :   ///
      68             :   /// Typically used in a generic adapter (i.e. one shared by all types)
      69             :   /// so it should be e.g. `http://jsonplaceholder.typicode.com/`
      70             :   ///
      71             :   /// For specific paths to this type [T], see [urlForFindAll], [urlForFindOne], etc
      72           1 :   @protected
      73           1 :   String get baseUrl => throw UnsupportedError('Please override baseUrl');
      74             : 
      75             :   /// Returns URL for [findAll]. Defaults to [type].
      76           1 :   @protected
      77           2 :   String urlForFindAll(Map<String, dynamic> params) => '$type';
      78             : 
      79             :   /// Returns HTTP method for [findAll]. Defaults to `GET`.
      80           1 :   @protected
      81             :   DataRequestMethod methodForFindAll(Map<String, dynamic> params) =>
      82             :       DataRequestMethod.GET;
      83             : 
      84             :   /// Returns URL for [findOne]. Defaults to [type]/[id].
      85           1 :   @protected
      86           2 :   String urlForFindOne(id, Map<String, dynamic> params) => '$type/$id';
      87             : 
      88             :   /// Returns HTTP method for [findOne]. Defaults to `GET`.
      89           1 :   @protected
      90             :   DataRequestMethod methodForFindOne(id, Map<String, dynamic> params) =>
      91             :       DataRequestMethod.GET;
      92             : 
      93             :   /// Returns URL for [save]. Defaults to [type]/[id] (if [id] is present).
      94           1 :   @protected
      95             :   String urlForSave(id, Map<String, dynamic> params) =>
      96           3 :       id != null ? '$type/$id' : type;
      97             : 
      98             :   /// Returns HTTP method for [save]. Defaults to `PATCH` if [id] is present,
      99             :   /// or `POST` otherwise.
     100           1 :   @protected
     101             :   DataRequestMethod methodForSave(id, Map<String, dynamic> params) =>
     102             :       id != null ? DataRequestMethod.PATCH : DataRequestMethod.POST;
     103             : 
     104             :   /// Returns URL for [delete]. Defaults to [type]/[id].
     105           1 :   @protected
     106           2 :   String urlForDelete(id, Map<String, dynamic> params) => '$type/$id';
     107             : 
     108             :   /// Returns HTTP method for [delete]. Defaults to `DELETE`.
     109           1 :   @protected
     110             :   DataRequestMethod methodForDelete(id, Map<String, dynamic> params) =>
     111             :       DataRequestMethod.DELETE;
     112             : 
     113             :   /// A [Map] representing default HTTP query parameters. Defaults to empty.
     114             :   ///
     115             :   /// It can return a [Future], so that adapters overriding this method
     116             :   /// have a chance to call async methods.
     117             :   ///
     118             :   /// Example:
     119             :   /// ```
     120             :   /// @override
     121             :   /// FutureOr<Map<String, dynamic>> get defaultParams async {
     122             :   ///   final token = await _localStorage.get('token');
     123             :   ///   return await super.defaultParams..addAll({'token': token});
     124             :   /// }
     125             :   /// ```
     126           1 :   @protected
     127           1 :   FutureOr<Map<String, dynamic>> get defaultParams => {};
     128             : 
     129             :   /// A [Map] representing default HTTP headers.
     130             :   ///
     131             :   /// Initial default is: `{'Content-Type': 'application/json'}`.
     132             :   ///
     133             :   /// It can return a [Future], so that adapters overriding this method
     134             :   /// have a chance to call async methods.
     135             :   ///
     136             :   /// Example:
     137             :   /// ```
     138             :   /// @override
     139             :   /// FutureOr<Map<String, String>> get defaultHeaders async {
     140             :   ///   final token = await _localStorage.get('token');
     141             :   ///   return await super.defaultHeaders..addAll({'Authorization': token});
     142             :   /// }
     143             :   /// ```
     144           1 :   @protected
     145             :   FutureOr<Map<String, String>> get defaultHeaders =>
     146           1 :       {'Content-Type': 'application/json'};
     147             : 
     148             :   // lifecycle methods
     149             : 
     150             :   @override
     151             :   @mustCallSuper
     152           1 :   Future<RemoteAdapter<T>> initialize(
     153             :       {final bool remote,
     154             :       final bool verbose,
     155             :       final Map<String, RemoteAdapter> adapters,
     156             :       ProviderReference ref}) async {
     157           1 :     if (isInitialized) return this as RemoteAdapter<T>;
     158           1 :     _remote = remote ?? true;
     159           1 :     _verbose = verbose ?? true;
     160           1 :     this.adapters = adapters;
     161           1 :     this.ref = ref;
     162             : 
     163           3 :     await localAdapter.initialize();
     164             : 
     165           2 :     await super.initialize();
     166             :     return this as RemoteAdapter<T>;
     167             :   }
     168             : 
     169             :   @override
     170           1 :   Future<void> dispose() async {
     171           2 :     await super.dispose();
     172           3 :     await localAdapter.dispose();
     173             :   }
     174             : 
     175           1 :   void _assertInit() {
     176           1 :     assert(isInitialized, true);
     177             :   }
     178             : 
     179             :   // serialization interface
     180             : 
     181             :   /// Returns a [DeserializedData] object when deserializing a given [data].
     182             :   ///
     183             :   /// If [init] is `true`, ALL models in deserialization (including `included`)
     184             :   /// will be initialized.
     185             :   ///
     186             :   /// [key] can be used to supply a specific `key` when deserializing ONE model.
     187             :   @protected
     188             :   @visibleForTesting
     189             :   DeserializedData<T, DataModel<dynamic>> deserialize(dynamic data,
     190             :       {String key, bool init});
     191             : 
     192             :   /// Returns a serialized version of a model of [T],
     193             :   /// as a [Map<String, dynamic>] ready to be JSON-encoded.
     194             :   @protected
     195             :   @visibleForTesting
     196             :   Map<String, dynamic> serialize(T model);
     197             : 
     198             :   // caching
     199             : 
     200             :   /// Returns whether calling [findAll] should trigger a remote call.
     201             :   ///
     202             :   /// Meant to be overriden. Defaults to [remote].
     203           1 :   @protected
     204             :   bool shouldLoadRemoteAll(
     205             :     bool remote,
     206             :     Map<String, dynamic> params,
     207             :     Map<String, String> headers,
     208             :   ) =>
     209             :       remote;
     210             : 
     211             :   /// Returns whether calling [findOne] should initiate an HTTP call.
     212             :   ///
     213             :   /// Meant to be overriden. Defaults to [remote].
     214           1 :   @protected
     215             :   bool shouldLoadRemoteOne(
     216             :     dynamic id,
     217             :     bool remote,
     218             :     Map<String, dynamic> params,
     219             :     Map<String, String> headers,
     220             :   ) =>
     221             :       remote;
     222             : 
     223             :   // remote implementation
     224             : 
     225             :   @protected
     226             :   @visibleForTesting
     227           1 :   Future<List<T>> findAll(
     228             :       {bool remote,
     229             :       Map<String, dynamic> params,
     230             :       Map<String, String> headers,
     231             :       bool init}) async {
     232           1 :     _assertInit();
     233           1 :     remote ??= _remote;
     234           3 :     params = await defaultParams & params;
     235           3 :     headers = await defaultHeaders & headers;
     236             :     init ??= false;
     237             : 
     238           1 :     if (!shouldLoadRemoteAll(remote, params, headers)) {
     239           2 :       final models = localAdapter.findAll();
     240             :       if (init) {
     241           1 :         models.map((m) => m._initialize(adapters, save: true));
     242             :       }
     243             :       return models;
     244             :     }
     245             : 
     246           2 :     return await sendRequest<List<T>>(
     247           5 :       baseUrl.asUri / urlForFindAll(params) & params,
     248           1 :       method: methodForFindAll(params),
     249             :       headers: headers,
     250           1 :       onSuccess: (data) {
     251           2 :         return deserialize(data, init: init).models;
     252             :       },
     253             :     );
     254             :   }
     255             : 
     256             :   @protected
     257             :   @visibleForTesting
     258           1 :   Future<T> findOne(final dynamic model,
     259             :       {bool remote,
     260             :       Map<String, dynamic> params,
     261             :       Map<String, String> headers,
     262             :       bool init}) async {
     263           1 :     _assertInit();
     264           1 :     assert(model != null);
     265           1 :     remote ??= _remote;
     266           3 :     params = await defaultParams & params;
     267           3 :     headers = await defaultHeaders & headers;
     268             :     init ??= false;
     269             : 
     270           2 :     final id = model is T ? model.id : model;
     271             : 
     272           1 :     if (!shouldLoadRemoteOne(id, remote, params, headers)) {
     273             :       final key =
     274           5 :           graph.getKeyForId(type, id) ?? (model is T ? model._key : null);
     275             :       if (key == null) {
     276             :         return null;
     277             :       }
     278           2 :       final newModel = localAdapter.findOne(key);
     279             :       if (init) {
     280           2 :         newModel._initialize(adapters, save: true);
     281             :       }
     282             :       return newModel;
     283             :     }
     284             : 
     285           2 :     return await sendRequest<T>(
     286           5 :       baseUrl.asUri / urlForFindOne(id, params) & params,
     287           1 :       method: methodForFindOne(id, params),
     288             :       headers: headers,
     289           1 :       onSuccess: (data) {
     290           2 :         return deserialize(data as Map<String, dynamic>, init: init).model;
     291             :       },
     292             :     );
     293             :   }
     294             : 
     295             :   @protected
     296             :   @visibleForTesting
     297           1 :   Future<T> save(final T model,
     298             :       {bool remote,
     299             :       Map<String, dynamic> params,
     300             :       Map<String, String> headers,
     301             :       bool init}) async {
     302           1 :     _assertInit();
     303           1 :     remote ??= _remote;
     304           3 :     params = await defaultParams & params;
     305           3 :     headers = await defaultHeaders & headers;
     306             :     init ??= false;
     307             : 
     308           1 :     if (remote == false) {
     309             :       // we ignore `init` as saving locally requires initializing
     310           2 :       return model._initialize(adapters, save: true);
     311             :     }
     312             : 
     313           2 :     final body = json.encode(serialize(model));
     314             : 
     315           2 :     return await sendRequest<T>(
     316           6 :       baseUrl.asUri / urlForSave(model.id, params) & params,
     317           2 :       method: methodForSave(model.id, params),
     318             :       headers: headers,
     319             :       body: body,
     320           1 :       onSuccess: (data) {
     321             :         if (data == null) {
     322             :           // return "old" model if response was empty
     323             :           if (init) {
     324           2 :             model._initialize(adapters, save: true);
     325             :           }
     326             :           return model;
     327             :         }
     328             :         // deserialize already inits models
     329             :         // if model had a key already, reuse it
     330           1 :         final newModel = deserialize(data as Map<String, dynamic>,
     331           1 :                 key: model._key, init: init)
     332           1 :             .model;
     333             : 
     334             :         // in the unlikely case where supplied key couldn't be used
     335             :         // ensure "old" copy of model carries the updated key
     336           4 :         if (init && model._key != null && model._key != newModel._key) {
     337           3 :           graph.removeKey(model._key);
     338           2 :           model._key = newModel._key;
     339             :         }
     340             :         return newModel;
     341             :       },
     342             :     );
     343             :   }
     344             : 
     345             :   @protected
     346             :   @visibleForTesting
     347           1 :   Future<void> delete(final dynamic model,
     348             :       {bool remote,
     349             :       Map<String, dynamic> params,
     350             :       Map<String, String> headers}) async {
     351           1 :     _assertInit();
     352           1 :     remote ??= _remote;
     353           3 :     params = await defaultParams & params;
     354           3 :     headers = await defaultHeaders & headers;
     355             : 
     356           2 :     final id = model is T ? model.id : model;
     357           5 :     final key = graph.getKeyForId(type, id) ?? (model is T ? model._key : null);
     358             : 
     359             :     if (key == null) {
     360             :       return;
     361             :     }
     362             : 
     363           3 :     await localAdapter.delete(key);
     364             : 
     365             :     if (remote && id != null) {
     366           3 :       graph.removeId(type, id);
     367           2 :       return await sendRequest<void>(
     368           5 :         baseUrl.asUri / urlForDelete(id, params) & params,
     369           1 :         method: methodForDelete(id, params),
     370             :         headers: headers,
     371             :       );
     372             :     }
     373             :   }
     374             : 
     375           1 :   @protected
     376             :   @visibleForTesting
     377           2 :   Future<void> clear() => localAdapter.clear();
     378             : 
     379             :   // http
     380             : 
     381             :   /// An [http.Client] used to make an HTTP request.
     382             :   ///
     383             :   /// This getter returns a new client every time
     384             :   /// as by default they are used once and then closed.
     385           0 :   @protected
     386             :   @visibleForTesting
     387           0 :   http.Client get httpClient => http.Client();
     388             : 
     389             :   /// The function used to perform an HTTP request and return an [R].
     390             :   ///
     391             :   /// **IMPORTANT**:
     392             :   ///  - [uri] takes the FULL `Uri` including query parameters
     393             :   ///  - [headers] do NOT include ANY defaults such as [defaultHeaders]
     394             :   ///
     395             :   /// Example:
     396             :   ///
     397             :   /// ```
     398             :   /// await sendRequest(
     399             :   ///   baseUrl.asUri + 'token' & await defaultParams & {'a': 1},
     400             :   ///   headers: await defaultHeaders & {'a': 'b'},
     401             :   ///   onSuccess: (data) => data['token'] as String,
     402             :   /// );
     403             :   /// ```
     404             :   ///
     405             :   ///ignore: comment_references
     406             :   /// To build the URI you can use [String.asUri], [Uri.+] and [Uri.&].
     407             :   ///
     408             :   /// To merge headers and params with their defaults you can use the helper
     409             :   /// [Map<String, dynamic>.&].
     410             :   ///
     411             :   /// In addition, [onSuccess] is supplied to post-process the
     412             :   /// data in JSON format. Deserialization and initialization
     413             :   /// typically occur in this function.
     414             :   ///
     415             :   /// [onError] can also be supplied to override [_RemoteAdapter.onError].
     416             :   @protected
     417             :   @visibleForTesting
     418           1 :   FutureOr<R> sendRequest<R>(
     419             :     final Uri uri, {
     420             :     DataRequestMethod method = DataRequestMethod.GET,
     421             :     Map<String, String> headers,
     422             :     String body,
     423             :     OnData<R> onSuccess,
     424             :     OnData<R> onError,
     425             :   }) async {
     426             :     // callbacks
     427           1 :     onSuccess ??= (_) async => null;
     428           2 :     onError ??= (e) async => this.onError(e) as R;
     429             : 
     430             :     http.Response response;
     431             :     dynamic data;
     432             :     dynamic error;
     433             :     StackTrace stackTrace;
     434             : 
     435             :     try {
     436             :       switch (method) {
     437           1 :         case DataRequestMethod.HEAD:
     438           0 :           response = await httpClient.head(uri, headers: headers);
     439             :           break;
     440           1 :         case DataRequestMethod.GET:
     441           3 :           response = await httpClient.get(uri, headers: headers);
     442             :           break;
     443           1 :         case DataRequestMethod.PUT:
     444           0 :           response = await httpClient.put(uri, headers: headers, body: body);
     445             :           break;
     446           1 :         case DataRequestMethod.POST:
     447           3 :           response = await httpClient.post(uri, headers: headers, body: body);
     448             :           break;
     449           1 :         case DataRequestMethod.PATCH:
     450           3 :           response = await httpClient.patch(uri, headers: headers, body: body);
     451             :           break;
     452           1 :         case DataRequestMethod.DELETE:
     453           3 :           response = await httpClient.delete(uri, headers: headers);
     454             :           break;
     455             :         default:
     456             :           break;
     457             :       }
     458             :     } catch (err, stack) {
     459             :       error = err;
     460             :       stackTrace = stack;
     461             :     } finally {
     462           2 :       httpClient.close();
     463             :     }
     464             : 
     465             :     // response handling
     466             : 
     467           1 :     if (response?.body == null) {
     468           3 :       return await onError(DataException(error, stackTrace: stackTrace));
     469             :     }
     470             : 
     471             :     try {
     472           4 :       data = response.body.isEmpty ? null : json.decode(response.body);
     473           1 :     } on FormatException catch (e) {
     474             :       error = e;
     475             :     }
     476             : 
     477           1 :     final code = response.statusCode;
     478             : 
     479           1 :     if (_verbose) {
     480           1 :       print(
     481           2 :           '[flutter_data] $T: ${method.toShortString()} $uri [HTTP $code]${body != null ? '\n -> body: $body' : ''}');
     482             :     }
     483             : 
     484           2 :     if (error == null && code >= 200 && code < 300) {
     485           2 :       return await onSuccess(data);
     486             :     } else {
     487           1 :       final e = DataException(error ?? data,
     488             :           stackTrace: stackTrace, statusCode: code);
     489           1 :       if (_verbose) {
     490           2 :         print('[flutter_data] $T: $e');
     491             :       }
     492           2 :       return await onError(e);
     493             :     }
     494             :   }
     495             : 
     496             :   /// Describes how to handle errors arising in [sendRequest].
     497             :   ///
     498             :   /// NOTE: [sendRequest] has an `onError` argument used to override
     499             :   /// this default behavior.
     500           1 :   @protected
     501             :   @visibleForTesting
     502             :   OnData<R> onError<R>(e) => throw e;
     503             : 
     504             :   /// Initializes [model] making it ready to use with [DataModel] extensions.
     505             :   ///
     506             :   /// Optionally provide [key]. Use [save] to persist in local storage.
     507           1 :   @protected
     508             :   @visibleForTesting
     509             :   T initializeModel(T model, {String key, bool save}) {
     510           2 :     return model?._initialize(adapters, key: key, save: save);
     511             :   }
     512             : }
     513             : 
     514             : /// A utility class used to return deserialized main [models] AND [included] models.
     515             : class DeserializedData<T, I> {
     516           1 :   const DeserializedData(this.models, {this.included});
     517             :   final List<T> models;
     518             :   final List<I> included;
     519           3 :   T get model => models.single;
     520             : }
     521             : 
     522             : /// A standard [Exception] used throughout Flutter Data.
     523             : ///
     524             : /// Usually thrown from [_RemoteAdapter.onError] in [_RemoteAdapter.sendRequest].
     525             : class DataException implements Exception {
     526             :   final Object error;
     527             :   final int statusCode;
     528             :   final StackTrace stackTrace;
     529           1 :   const DataException(this.error, {this.stackTrace, this.statusCode});
     530             : 
     531           1 :   @override
     532             :   bool operator ==(dynamic other) =>
     533           3 :       identical(this, other) || toString() == other.toString();
     534             : 
     535           0 :   @override
     536             :   int get hashCode =>
     537           0 :       runtimeType.hashCode ^
     538           0 :       statusCode.hashCode ^
     539           0 :       error.hashCode ^
     540           0 :       stackTrace.hashCode;
     541             : 
     542           1 :   @override
     543             :   String toString() {
     544           6 :     return 'DataException: $error ${statusCode != null ? " [HTTP $statusCode]" : ""}\n$stackTrace';
     545             :   }
     546             : }
     547             : 
     548             : // ignore: constant_identifier_names
     549          10 : enum DataRequestMethod { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE }
     550             : 
     551             : extension _ToStringX on DataRequestMethod {
     552           4 :   String toShortString() => toString().split('.').last;
     553             : }

Generated by: LCOV version 1.14