brick_rest 0.0.7+1

  • Readme
  • Changelog
  • Example
  • Installing
  • 81

REST Provider #

Connecting Brick with a RESTful API.

Supported Query Configuration #

providerArgs: #

  • 'headers' (Map<String, String>) set HTTP headers
  • 'request' (String) specifies HTTP method. Only available to #upsert. Defaults to POST
  • 'topLevelKey' (String) the payload is sent or received beneath a JSON key (For example, {"user": {"id"...}})
  • 'supplementalTopLevelData' (Map<String, dynamic>) this map is merged alongside the topLevelKey in the payload. For example, given 'supplementalTopLevelData': {'other_key': true} {"topLevelKey": ..., "other_key": true}. It is strongly recommended to avoid using this property. Your data should be managed at the model level, not the query level.

where: #

RestProvider does not support any Query#where arguments. These should be configured on a model-by-model base by the RestSerializable#endpoint argument.

Models #

@Rest(endpoint:) #

Every REST API is built differently, and with a fair amount of technical debt. Brick provides flexibility for inconsistent endpoints within any system. Endpoints can also change based on the query. The model adapter will query endpoint for upsert or get or delete.

Since Dart requires annotations to be constants, functions cannot be used. This is a headache. Instead, the function must be stringified. The annotation only expects the function body: query will always be available, and instance will be available to methods handling an instance argument like upsert or delete. The function body must return a string.

@ConnectOfflineFirstWithRest(
  restConfig: RestSerializable(
    endpoint: '=> "/users";';
  )
)
class User extends OfflineFirstModel {}

When managing an instance, say in delete, the endpoint will have to be expanded:

@ConnectOfflineFirstWithRest(
  restConfig: RestSerializable(
    endpoint: r'''{
      if (query?.action == QueryAction.delete) return "/users/${instance.id}";

      return "/users";
    }''';
  )
)
class User extends OfflineFirstModel {}

⚠️ If an endpoint's function returns null, it is skipped by the provider.

With Query#providerArgs

@ConnectOfflineFirstWithRest(
  restConfig: RestSerializable(
    endpoint: r'''{
      if (query?.action == QueryAction.delete) return "/users/${instance.id}";

      if (query?.action == QueryAction.get &&
          query?.providerArgs.isNotEmpty &&
          query?.providerArgs['limit'] != null) {
            return "/users?limit=${query.providerArgs['limit']}";
      }

      return "/users";
    }''';
  )
)
class User extends OfflineFirstModel {}

With Query#where

@ConnectOfflineFirstWithRest(
  restConfig: RestSerializable(
    endpoint: r'''{
      if (query?.action == QueryAction.delete) return "/users/${instance.id}";

      if (query?.action == QueryAction.get && query?.where != null) {
        final id = Where.firstByField('id', query.where)?.value;
        if (id != null) return "/users/$id";
      }

      return "/users";
    }''';
  )
)
class User extends OfflineFirstModel {}

DRY Endpoints

As this can become repetitive across models that share a similar interface with a remote provider, a helper class can be employed. Brick imports the same files that a model file imports into brick.g.dart, which in turn is shared across all adapters.

// Plainly:
import 'package:my_flutter_app/endpoint_helpers.dart';
...
class User extends OfflineFirstModel {}

// is accessible for
class UserAdapter ... {
  endpoint() {}
}

A complete example:

// endpoint_helpers.dart
class EndpointHelpers {
  static indexOrMemberEndpoint(String path) {
    if (query?.action == QueryAction.delete) return "/$path/${instance.id}";

    if (query?.action == QueryAction.get && query?.where != null) {
      final id = Where.firstByField('id', query.where)?.value;
      if (id != null) return "/$path/$id";
    }

    return "/$path";
  }
}

// user.dart
import 'package:my_flutter_app/endpoint_helpers.dart';
@ConnectOfflineFirstWithRest(
  restConfig: RestSerializable(
    endpoint: '=> EndpointHelpers.indexOrMemberEndpoint("users")';
  )
)
class User extends OfflineFirstModel {}

// hat.dart
// Brick has already discovered and imported endpoint_helpers.dart, so while it
// can be imported again in this file for consistency, it's not necessary
@ConnectOfflineFirstWithRest(
  restConfig: RestSerializable(
    endpoint: '=> EndpointHelpers.indexOrMemberEndpoint("hats")';
  )
)
class Hat extends OfflineFirstModel {}

@RestSerializable(fromKey:) and @RestSerializable(toKey:) #

Data will be nested beneath a top-level key in a JSON response. The key is determined by the following priority:

  1. A topLevelKey in Query#providerArgs with a non-empty value
  2. fromKey if invoked from provider#get or toKey if invoked from provider#upsert
  3. The first discovered key. As a map is effectively an unordered list, relying on this fall through is not recommended.

fromKey and toKey are defined in the model's annotation:

@ConnectOfflineFirstWithRest(
  restConfig: RestSerializable(
    toKey: 'user',
    fromKey: 'users',
  )
)
class User extends OfflineFirstModel {}

⚠️ If the response from REST is not a map, the full response is returned instead.

@RestSerializable(fieldRename:) #

Brick reduces the need to map REST keys to model field names by assuming a standard naming convention. For example:

RestSerializable(fieldRename: FieldRename.snake_case)
// on from rest (get)
 "last_name" => final String lastName
// on to rest (upsert)
final String lastName => "last_name"

Fields #

@Rest(enumAsString:) #

Brick by default assumes enums from a REST API will be delivered as integers matching the index in the Flutter app. However, if your API delivers strings instead, the field can be easily annotated without writing a custom generator.

Given the API:

{ "user": { "hats": [ "bowler", "birthday" ] } }

Simply convert hats into a Dart enum:

enum Hat { baseball, bowler, birthday }

...

@Rest(enumAsString: true)
final List<Hat> hats;

@Rest(name:) #

REST keys can be renamed per field. This will override the default set by RestSerializable#fieldRename.

@Rest(
  name: "full_name"  // "full_name" is used in from and to requests to REST instead of "last_name"
)
final String lastName;

@Rest(ignoreFrom:) and @Rest(ignoreTo:) #

When true, the field will be ignored by the (de)serializing function in the adapter.

Unsupported Field Types #

The following are not serialized to REST. However, unsupported types can still be accessed in the model as non-final fields.

  • Nested List<> e.g. <List<List<int>>>
  • Many-to-many associations

Unreleased #

0.0.7+1 #

  • Change _convertJson to protected method convertJsonFromGet (#57)

0.0.7 #

  • When url is null in RestProvider#upsert and RestProvider#delete, return null and do not attempt to perform the action
  • On upsert invocations, specify supplementalTopLevelData to include other information outside the topLevelKey/toKey. This inserts a map alongside the data generated by the adapter.

0.0.5 #

  • Carry rename from Query#params to Query#providerArgs from brick_core

0.0.4 #

  • Rest#defaultValue updated to reflect FieldSerializable#defaultValue change

0.0.2 #

  • Fix linter hints

example/example.dart

import 'package:brick_core/core.dart';
import '../lib/rest.dart';

/// This class and code is always generated.
/// It is included here as an illustration.
/// Rest adapters are generated by domains that utilize the brick_rest_generators package,
/// such as brick_offline_first_with_rest_build
class UserAdapter extends RestAdapter<User> {
  final fromKey = 'users';
  final toKey = 'user';

  fromRest(data, {provider, repository}) async {
    return User(
      name: data['name'],
    );
  }

  toRest(instance, {provider, repository}) async {
    return {
      'name': instance.name,
    };
  }

  restEndpoint({query, instance}) => "/users";
}

/// This value is always generated.
/// It is included here as an illustration.
/// Import it from `lib/app/brick.g.dart` in your application.
final dictionary = RestModelDictionary({
  User: UserAdapter(),
});

/// A model is unique to the end implementation (e.g. a Flutter app)
class User extends RestModel {
  final String name;

  User({
    this.name,
  });
}

class MyRepository extends SingleProviderRepository<RestModel> {
  MyRepository(String baseApiUrl)
      : super(
          RestProvider(
            baseApiUrl,
            modelDictionary: dictionary,
          ),
        );
}

void main() async {
  final repository = MyRepository('http://localhost:8080');

  final users = await repository.get<User>();
  print(users);
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  brick_rest: ^0.0.7+1

2. Install it

You can install packages from the command line:

with pub:


$ pub get

with Flutter:


$ flutter pub get

Alternatively, your editor might support pub get or flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:brick_rest/rest.dart';
import 'package:brick_rest/rest_exception.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
66
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
90
Overall:
Weighted score of the above. [more]
81
Learn more about scoring.

We analyzed this package on Jul 10, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.15

Health suggestions

Fix lib/rest_exception.dart. (-0.50 points)

Analysis of lib/rest_exception.dart reported 1 hint:

line 19 col 19: Avoid empty catch blocks.

Format lib/rest.dart.

Run dartfmt to format lib/rest.dart.

Maintenance suggestions

Package is pre-v0.1 release. (-10 points)

While nothing is inherently wrong with versions of 0.0.*, it might mean that the author is still experimenting with the general direction of the API.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.4.0 <3.0.0
brick_core ^0.0.6 0.0.6
http ^0.12.0 0.12.1
logging ^0.11.3+2 0.11.4
meta ^1.1.6 1.2.2 1.3.0-nullsafety
Transitive dependencies
charcode 1.1.3
collection 1.14.13 1.15.0-nullsafety
http_parser 3.1.4
path 1.7.0
pedantic 1.9.2
source_span 1.7.0
string_scanner 1.0.5
term_glyph 1.1.0
typed_data 1.2.0 1.3.0-nullsafety
Dev dependencies
mockito ^4.0.0
test >=1.9.4 <2.0.0