Skip to content

Instantly share code, notes, and snippets.

@vsavkin
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vsavkin/bb2964a9523d40056bf3 to your computer and use it in GitHub Desktop.
Save vsavkin/bb2964a9523d40056bf3 to your computer and use it in GitHub Desktop.
Spike: Hammock.Mapper

Hammock.Mapper (Spike)

A few weeks I released Hammock - an AngularDart service for working with Rest APIs. You can read more about it here. This post is about a small library called Hammock.Mapper I built on my flight from Toronto to SF. It uses conventions and a bit of meta information to generate all the serialization and deserialization functions required by Hammock.

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(constructor: #blank)
class Post {
  int id;
  String title;

  Post(this.id, this.title);
  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(constructor: #blank)
class Post {
  int id;
  String title;
  List<Comment> comments;

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

@Mappable(constructor: #blank)
class Comment {
  int id;
  String text;

  Comment(this.id, this.text);
  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.

Other 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(constructor: #blank)
class Post {
  int id;
  String title;
  DateTime publishedAt;

  Post(this.id, this.title, this.publishedAt);
  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);

Spike

This is just a spike, and it is only 200 lines long. So there are quite a few things missing:

  1. Mappers should be configurable:
@Mappable(constructor: #blank)
class Post {
  @Field(name: "some-field") int id;
  String title;

  @Field(mapper: CustomDateTimeMapper) DateTime createdAt;
  @Field(skip: true) DateTime createdAt;

  Post(this.id, this.title);
}
  1. There should be more control over Mammock configuration:
config.set(
    mappers()
      .resource("posts", Post, resourceToData: someFunc, dataToResource: anotherFunc)
      .createHammockConfig()
);
  1. Provide a way to generate mappers, so you don't have to use mirrors. Similar to how AngularDart handles it.

Source Code

You can look at the source code here.

It's a spike. Feedback welcomed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment