/hammock_mapper

Hammock Mapper

Primary LanguageDart

Hammock.Mapper

Hammock.Mapper uses conventions and a bit of meta information to generate all the serialization and deserialization functions required by Hammock. Read more about Hammock here.

Hammock

First, let's look at a small example of using Hammock.

Suppose we have the following model defined:

class Post {
  int id;
  String title;
  Post(this.id, this.title);
}

You can configure Hammock to work with this model:

config.set({
    "posts" : {
        "type" : Post,
        "serializer" : serializePost,
        "deserializer" : {
            "query" : deserializerPost,
            "command" : {
              "success" : updatePost,
              "error" : parseErrors
            }
        }
    }
});

Resource serializePost(Post post) =>
      resource("posts", post.id, {"id" : post.id, "title" : post.title});

Post updatePost(Post post, CommandResponse resp) {
  post.id = resp.content["id"];
  post.title = resp.content["title"];
  return post;
}

deserializePost(Resource r) => new Post(r.id, r.content["title"]);

parseErrors(obj, CommandResponse resp) => resp.content["errors"];

Having this configuration in place, we can use ObjectStore to load and save post objects.

ObjectStore store; //get injected

Future<Post> p = store.one(Post, 123);
p.title = 'new title';
store.update(p);

Hammock does not assume anything about the objects it works with, and as a result, you have to provide all the serialization and deserialization functions. This flexibility can be useful, especially when dealing with legacy APIs, or APIs that you do not control.

Implementing all these functions, however, can be quite tedious, especially when dealing with nested objects (e.g., a post with many comments). That's where Hammock.Mapper comes into play. With Hammock.Mapper we can annotate our classes with a little bit of extra information and the library will take care of generating all the required serialization and deserialization functions.

Hammock.Mapper

Let's look at our example again, but this time using Hammock.Mapper.

@Mappable()
class Post {
  int id;
  String title;

  Post(this.id, this.title);
  @Constructor() Post.blank();
}

First, you need to annotate your models with Mappable. The model has to have a constructor without arguments. We can choose the construtor we want to use.

config.set(
    mappers()
      .resource("posts", Post)
      .createHammockConfig()
);

That's all the configuration that you have to provide.

Nested Objects and Lists

Let's look at a more complicated example:

@Mappable()
class Post {
  int id;
  String title;
  List<Comment> comments;

  Post(this.id, this.title, this.comments);
  @Constructor() Post.blank();
}

@Mappable()
class Comment {
  int id;
  String text;

  Comment(this.id, this.text);
  @Constructor() Comment.blank();
}

config.set(
    mappers()
      .resource("posts", Post)
      .resource("comments", Comment)
      .createHammockConfig()
);

With this configuration, you can load and save comments and posts. Note, that the list property will be handled properly.

Configuring Mappers

@Mappable()
class Post {
  @Field(name: 'custom-id')   int id;
  @Field(skip: true)          String skip;
  @Field(readOnly: true)      String readOnly;
  @Field(mapper: someMapper)  String customMapper;
}

Global Mappers

There are types, however, you cannot add meta information to (e.g., DateTime) using annotations. For those types mappers have to be registered explicitly.

@Mappable()
class Post {
  int id;
  String title;
  DateTime publishedAt;

  Post(this.id, this.title, this.publishedAt);
  @Constructor() Post.blank();
}

class DateTimeMapper implements Mapper<DateTime> {
  String toData(DateTime d) => d.toString();
  DateTime fromData(String s) => DateTime.parse(s);
}

config.set(
    mappers()
      .resource("posts", Post)
      .mapper(DateTime, new DateTimeMapper())
      .createHammockConfig()
);

Identity Map

There is another problem that the code at the beginning of this document has.

//WITHOUT Hammock.Mapper

//somewhere in Component1
final p1 = store.one(Post, 123);
//display p1

//somewhere in Component2
final p2 = store.one(Post, 123);
p2.title = 'new title';  
//p1 still has the old title because p1 != p2

The Component1 will not see the changed title because it has a different instance of Post(123). Hammock.Mapper solves this problem by using an identity map. So when using Hammock.Mapper both of the components will get the same object.

  //WITH Hammock.Mapper

  //somewhere in Component1
  final p1 = store.one(Post, 123);
  //display p1

  //somewhere in Component2
  final p2 = store.one(Post, 123);
  p2.title = 'new title';  
  //p1 has the new title because p1 == p2

You can disable it:

config.set(
    mappers()
      .resource("posts", Post)
      .instantiator(simpleInstantiator)
      .createHammockConfig()
);

You Can Always Go Low-Level

Note, that Hammock.Mapper just creates a configuration map for Hammock. You can always change the configuration or add new items to it.

Map m = mappers()
      .resource("posts", Post)
      .mapper(DateTime, new DateTimeMapper())
      .createHammockConfig();

m["special-resource"] = ...;

config.set(m);

Use Mappers Directly

You can use the mapper piece of library without Hammock:

final mappers = new Mappers();
Map data = mappers.toData(post);
Post restoredPost = mappers.fromData(Post, data);