Mapster
Mapster is an object mapping library.
How to use
- Extend one of
Mapperclasses. There are 9 types:OneSourceMapper,TwoSourcesMapper, ... ,NineSourcesMapper. ExtendOneSourceMapper, if you need to map 1 object to another. ExtendTwoSourcesMapper, if you need to map 2 objects to another, and so on up to 9 source objects.
class UserToUserResponseMapper extends OneSourceMapper<User, UserResponse> {
UserToUserResponseMapper(super.input);
@override
UserResponse map() {
return UserResponse(
id: source.id,
fullName: '${source.firstName} ${source.lastName}',
);
}
}
- Create
Mapsterinstance.
void main() {
final mapster = Mapster();
}
- Register all your
Mappers in created instance ofMapster. UseMapperMeta's static methods:one,two, ... ,ninedepend onMappertype.
void main() {
final mapster = Mapster();
mapster.register(MapperMeta.one(UserToUserResponseMapper.new));
}
- And use
Mapster!
void main() {
final mapster = Mapster();
mapster.register(MapperMeta.one(UserToUserResponseMapper.new));
const user = User(
id: 1,
firstName: 'Harry',
lastName: 'Potter',
);
final userResponse = mapster.map1(user, To<UserResponse>());
}
Note that you should pass To<YourResultType>() as last parameter. This way you specify what type
should Mapster return.
Also, you can create private getters for sources. It is more comfortable.
class UserUserPostToLikedPostNotification
extends ThreeSourcesMapper<User, User, Post, LikedPostNotification> {
UserUserPostToLikedPostNotification(super.input);
@override
LikedPostNotification map() {
return LikedPostNotification(
postID: _post.id,
authorID: _user1.id,
likeUserID: _user2.id,
postText: _post.text,
authorName: '${_user1.firstName} ${_user1.lastName}',
likeUserName: '${_user2.firstName} ${_user2.lastName}',
);
}
User get _user1 => source1;
User get _user2 => source2;
Post get _post => source3;
}
Map functions
Mapster has 9 map methods: map1, map2, ... , map9. All of them get source objects and
then To<YourResultType>().
You can pass source objects to Mapster's map methods IN ANY ORDER. You do not need to check
the order of input objects in signature of certain Mapper every time. Mapster is smart enough to
find a proper Mapper.
class UserPostToPostResponse extends TwoSourcesMapper<User, Post, PostResponse> {
UserPostToPostResponse(super.input);
@override
PostResponse map() {
return PostResponse(
id: source2.id,
text: source2.text,
userID: source1.id,
userName: '${source1.firstName} ${source1.lastName}',
);
}
}
void main() {
final mapster = Mapster();
mapster.register(MapperMeta.two(UserPostToPostResponse.new));
const user = User(
id: 1,
firstName: 'Harry',
lastName: 'Potter',
);
const post = Post(
id: 1,
text: "The philosopher's stone",
);
// You can swap source objects, the result will be the same.
final postResponse1 = mapster.map2(user, post, To<PostResponse>());
final postResponse2 = mapster.map2(post, user, To<PostResponse>());
}
Beware of Mappers with multiple source objects of the same type. Under the hood, Mapster
matches all input objects in order they are passed. For example, let's look at this code:
class UserUserPostToLikedPostNotification
extends ThreeSourcesMapper<User, User, Post, LikedPostNotification> {
UserUserPostToLikedPostNotification(super.input);
@override
LikedPostNotification map() {
return LikedPostNotification(
postID: source3.id,
authorID: source1.id,
likeUserID: source2.id,
postText: source3.text,
authorName: '${source1.firstName} ${source1.lastName}',
likeUserName: '${source2.firstName} ${source2.lastName}',
);
}
}
void main() {
final mapster = Mapster();
mapster.register(MapperMeta.three(UserUserPostToLikedPostNotification.new));
const user = User(
id: 1,
firstName: 'Harry',
lastName: 'Potter',
);
const post = Post(
id: 1,
text: "The philosopher's stone",
);
const likeUser = User(
id: 2,
firstName: 'Ronald',
lastName: 'Weasley',
);
// You can swap source objects, but if you swap multiple objects of the same type,
// the result WILL change.
// Mapster does its maximum. But Mapster is not able to define the right order
// for multiple objects of the same type.
// So, you should avoid creating Mappers with multiple objects of the same type.
final notification1 = mapster.map3(
user,
likeUser,
post,
To<LikedPostNotification>(),
);
final notification2 = mapster.map3(
likeUser,
user,
post,
To<LikedPostNotification>(),
);
}
Also you can not create a Mapper with nullable input or output types. Instead you can create
a DTO. For example:
class ToUserInfoResponseDTO {
const ToUserInfoResponseDTO({
required this.id,
required this.firstName,
this.lastName,
this.phone,
});
final int id;
final String firstName;
final String? lastName;
final String? phone;
}
class UserInfoToUserInfoResponseMapper
extends OneSourceMapper<ToUserInfoResponseDTO, UserInfoResponse> {
UserInfoToUserInfoResponseMapper(super.input);
@override
UserInfoResponse map() {
var fullName = source.firstName;
final lastName = source.lastName;
if (lastName != null) {
fullName += ' $lastName';
}
return UserInfoResponse(
id: source.id,
fullName: fullName,
phone: source.phone,
);
}
}
void main() {
final mapster = Mapster();
/// If you need to pass `null` create special DTO for it.
final dto = ToUserInfoResponseDTO(
id: 1,
firstName: 'Harry',
lastName: null,
phone: null,
);
mapster.register(MapperMeta.one(UserInfoToUserInfoResponseMapper.new));
print(mapster.map1(dto, To<UserInfoResponse>()));
}
Pros & Cons
Pros
- Do not need to specify types in
<>during usingregisterandmapfunctions ofMapster - Do not need to worry about the order of parameters
- Analyzer correctly determines a return type of
mapfunctions Mapsterhas O(1) time complexity of searching for a properMapperMapsterhas O(n) time complexity (where n is an amount of parameters) of ordering arguments before passing them to aMapperMapsterhas no dependency- Do not need to inject your classes/functions with large amount of mappers anymore. Just inject
with
Mapster - Do not need to know a specific
Mapperto map - Ability to specify
Mappers in a one place - Ability to redefine
Mappers
Cons
- not found yet🙂
Other features
Redefine Mapper
You can redefine Mapper by calling register again, like that:
void main() {
final mapster = Mapster();
const user = User(
id: 1,
firstName: 'Harry',
lastName: 'Potter',
);
// Register Mapper with input type: User, and output type: UserResponse.
mapster.register(MapperMeta.one(UserToUserResponseMapper.new));
final userResponse1 = mapster.map1(user, To<UserResponse>());
// Register another Mapper with the same types:
// input type: User, and output type: UserResponse.
mapster.register(MapperMeta.one(AnotherUserToUserResponseMapper.new));
final userResponse2 = mapster.map1(user, To<UserResponse>());
}
Mapster stores Mappers based on its' source types and result type. If new Mapper has the same
set of input types (an order of input types does NOT matter) and the same output type as the
old Mapper, then Mapster replaces old one with a new one.
void main() {
final mapster = Mapster();
const user = User(
id: 1,
firstName: 'Harry',
lastName: 'Potter',
);
// Register Mapper with input type: User, and output type: UserResponse.
mapster.register(MapperMeta.one(UserToUserResponseMapper.new));
final userResponse1 = mapster.map1(user, To<UserResponse>());
// Register another Mapper with swapped result and input types:
// input type: UserResponse, and output type: User.
mapster.register(MapperMeta.one(UserResponseToUserMapper.new));
// Because input types set of the 1st Mapper contains
// different types than input types set of the 2nd Mapper,
// these two mappers considered as different.
// Also we can say: because output type of the 1st Mapper not
// equals to output type of the 2nd Mapper, these two
// mappers considered as different.
final user2 = mapster.map1(userResponse1, To<User>());
}
Work with injectable
If you use injectable package, you can register Mapster
and Mappers like that:
@module
abstract class MapsterModule {
@singleton
Mapster get mapster => Mapster();
}
@singleton
class MapsterRegistrar {
const MapsterRegistrar(this._mapster);
final Mapster _mapster;
@postConstruct
void register() {
_mapster..register(
MapperMeta.one(UserToUserResponseMapper.new),
)..register(
MapperMeta.three(UserUserPostToLikedPostNotification.new),
);
}
}
We won't use MapsterRegistrar class. But it's useful to us, because @singletons can
have @postConstruct method. So, this way, we can register Mapster and all our Mappers
in get_it.
If you use feature-first or layer-first approach in you project, you can declare
multiple MapsterRegistrar in multiple places with the same name, but do NOT try to
get MapsterRegister from get_it, if you creates multiple MasterRegistrar with the same name,
because it can cause a problem. Remember, that we do not need to get MapsterRegistrar, we creates
it only to use @postConstruct.