smartstruct

Creator: coderz1093

Last updated:

Add to Cart

Description:

smartstruct

Smartstruct - Dart bean mappings - the easy nullsafe way! #
Code generator for generating type-safe mappers in dart, inspired by https://mapstruct.org/

Installation
Usage
Examples
Roadmap

Overview #

Add smartstruct as a dependency, and smartstruct_generator as a dev_dependency
Create a Mapper class
Annotate the class with @mapper
Run the build_runner
Use the generated Mapper!

Installation #
Add smartstruct as a dependency, and the generator as a dev_dependency.
https://pub.dev/packages/smartstruct
dependencies:
smartstruct: [version]

dev_dependencies:
smartstruct_generator: [version]
# add build runner if not already added
build_runner:
copied to clipboard
Run the generator
dart run build_runner build
flutter packages pub run build_runner build
// or watch
flutter packages pub run build_runner watch
copied to clipboard
Usage #
Create your beans.
class Dog {
final String breed;
final int age;
final String name;
Dog(this.breed, this.age, this.name);
}
copied to clipboard
class DogModel {
final String breed;
final int age;
final String name;
DogModel(this.breed, this.age, this.name);
}
copied to clipboard
To generate a mapper for these two beans, you need to create a mapper interface.
// dogmapper.dart
part 'dogmapper.mapper.g.dart';

@Mapper()
abstract class DogMapper {
Dog fromModel(DogModel model);
}
copied to clipboard
Once you ran the generator, next to your dog.mapper.dart a dog.mapper.g.dart will be generated.
dart run build_runner build
copied to clipboard
// dogmapper.mapper.g.dart
class DogMapperImpl extends DogMapper {
@override
Dog fromModel(DogModel model) {
Dog dog = Dog(model.breed, model.age, model.name);
return dog;
}
}
copied to clipboard
The Mapper supports positional arguments, named arguments and property access via implicit and explicit setters.
Case sensitivity #
By default mapper generator works in case insensitivity manner.
class Source {
final String userName;

Source(this.userName);
}

class Target {
final String username;

Target({required this.username});
}

@Mapper()
abstract class ExampleMapper {
Target fromSource(Source source);
}
copied to clipboard
As you can see, classes above got different field's names (case) for username. Because mappers are case insensitive by default, those classes are correctly mapped.

class ExampleMapperImpl extends ExampleMapper {
@override
Target fromSource(Source source) {
final target = Target(username: source.userName);
return target;
}
}
copied to clipboard
To create case sensitive mapper, you can add param caseSensitiveFields to @Mapper annotation. Case sensitive mapper is checking field's names in case sensitive manner.

@Mapper(caseSensitiveFields: true)
abstract class ExampleMapper {
Target fromSource(Source source);
}
copied to clipboard
Explicit Field Mapping #
If some fields do not match each other, you can add a Mapping Annotation on the method level, to change the behaviour of certain mappings.
class Dog {
final String name;
Dog(this.name);
}
class DogModel {
final String dogName;
DogModel(this.dogName);
}
copied to clipboard
@Mapper()
class DogMapper {
@Mapping(source: 'dogName', target: 'name')
Dog fromModel(DogModel model);
}
copied to clipboard
In this case, the field dogName of DogModel will be mapped to the field name of the resulting Dog
class DogMapperImpl extends DogMapper {
@override
Dog fromModel(DogModel model) {
Dog dog = Dog(model.dogName);
return dog;
}
}
copied to clipboard
Function Mapping #
The source attribute can also be a Function. This Function will then be called with the Source Parameter of the mapper method as a parameter.
class Dog {
final String name;
final String breed;
Dog(this.name, this.breed);
}
class DogModel {
final String name;
DogModel(this.name);
}
copied to clipboard
@Mapper()
class DogMapper {
@IgnoreMapping()
static String randomBreed(DogModel model) => 'some random breed';

@Mapping(source: randomBreed, target: 'breed')
Dog fromModel(DogModel model);
}
copied to clipboard
Note the @IgnoreMapping Annotation, to make sure that no static implementation for this mapper method is created.
See Static Mapping
Will generate the following Mapper.
class DogMapperImpl extends DogMapper {
@override
Dog fromModel(DogModel model) {
Dog dog = Dog(model.dogName, DogMapper.randomBreed(model));
return dog;
}
}
copied to clipboard
Ignore Fields #
Fields can be ignored, by specififying the ignore attribute on the Mapping `Annotation``
class Dog {
final String name;
String? breed;
Dog(this.name);
}
class DogModel {
final String name;
final String breed;
DogModel(this.name, this.breed);
}
copied to clipboard
@Mapper()
class DogMapper {
@Mapping(target: 'breed', ignore: true)
Dog fromModel(DogModel model);
}
copied to clipboard
Will generate the following Mapper.
class DogMapperImpl extends DogMapper {
@override
Dog fromModel(DogModel model) {
Dog dog = Dog(model.name);
return dog;
}
}
copied to clipboard
Nested Bean Mapping #
Nested beans can be mapped, by defining an additional mapper method for the nested bean.
// nestedmapper.dart
class NestedTarget {
final SubNestedTarget subNested;
NestedTarget(this.subNested);
}
class SubNestedTarget {
final String myProperty;
SubNestedTarget(this.myProperty);
}

class NestedSource {
final SubNestedSource subNested;
NestedSource(this.subNested);
}

class SubNestedSource {
final String myProperty;
SubNestedSource(this.myProperty);
}

@Mapper()
abstract class NestedMapper {
NestedTarget fromModel(NestedSource model);

SubNestedTarget fromSubClassModel(SubNestedSource model);
}
copied to clipboard
Will generate the mapper
// nestedmapper.mapper.g.dart
class NestedMapperImpl extends NestedMapper {
@override
NestedTarget fromModel(NestedSource model) {
final nestedtarget = NestedTarget(fromSubClassModel(model.subNested));
return nestedtarget;
}

@override
SubNestedTarget fromSubClassModel(SubNestedSource model) {
final subnestedtarget = SubNestedTarget(model.myProperty);
return subnestedtarget;
}
}

copied to clipboard
Alternatively you can directly define the nested mapping in the source attribute.
class User {
final String username;
final String zipcode;
final String street;

User(this.username, this.zipcode, this.street);
}

class UserResponse {
final String username;
final AddressResponse address;

UserResponse(this.username, this.address);
}

class AddressResponse {
final String zipcode;
final StreetResponse street;

AddressResponse(this.zipcode, this.street);
}

class StreetResponse {
final num streetNumber;
final String streetName;

StreetResponse(this.streetNumber, this.streetName);
}
copied to clipboard
With this, you can define the mappings directly in the Mapping Annotation
@Mapper()
abstract class UserMapper {
@Mapping(target: 'zipcode', source: 'response.address.zipcode')
@Mapping(target: 'street', source: 'response.address.street.streetName')
User fromResponse(UserResponse response);
}
copied to clipboard
Would generate the following mapper.
class UserMapperImpl extends UserMapper {
UserMapperImpl() : super();

@override
User fromResponse(UserResponse response) {
final user = User(response.username, response.address.zipcode,
response.address.street.streetName);
return user;
}
}
copied to clipboard
List Support #
Lists will be mapped as new instances of a list, with help of the map method.
class Source {
final List<int> intList;
final List<SourceEntry> entryList;

Source(this.intList, this.entryList);
}

class SourceEntry {
final String prop;

SourceEntry(this.prop);
}

class Target {
final List<int> intList;
final List<TargetEntry> entryList;

Target(this.intList, this.entryList);
}

class TargetEntry {
final String prop;

TargetEntry(this.prop);
}

@Mapper()
abstract class ListMapper {
Target fromSource(Source source);
TargetEntry fromSourceEntry(SourceEntry source);
}
copied to clipboard
Will generate the Mapper
class ListMapperImpl extends ListMapper {
@override
Target fromSource(Source source) {
final target = Target(
source.intList.map((e) => e).toList(),
source.entryList.map(fromSourceEntry).toList());
return target;
}

@override
TargetEntry fromSourceEntry(SourceEntry source) {
final targetentry = TargetEntry(source.prop);
return targetentry;
}
}
copied to clipboard
Injectable #
The Mapper can be made a lazy injectable singleton, by setting the argument useInjection to true, in the Mapper Interface.
In this case you also need to add the injectable dependency, as described here. https://pub.dev/packages/injectable
Make sure, that in the Mapper File, you import the injectable dependency, before running the build_runner!
// dogmapper.dart

import 'package:injectable/injectable.dart';

@Mapper(useInjectable = true)
abstract class DogMapper {
Dog fromModel(DogModel model);
}
copied to clipboard
// dogmapper.mapper.g.dart
@LazySingleton(as: DogMapper)
class DogMapperImpl extends DogMapper {...}
copied to clipboard
Freezed #
Generally you can use smartstruct with freezed.
One problem you will have to manually workaround is ignoring the freezed generated copyWith method in the generated mapper.
The copyWith field is a normal field in the model / entity, and smartstruct does not have a way of knowing on when to filter it out, and when not.
Imagine having the following freezed models.
@freezed
class Dog with _$Dog {
Dog._();
factory Dog(String name) = _Dog;
}

@freezed
class DogModel with _$DogModel {
factory DogModel(String name) = _DogModel;
}
copied to clipboard
Freezed will generate a copyWith field for your Dog and DogModel.
When generating the mapper, you explicitly have to ignore this field.
@Mapper()
abstract class DogMapper {
@Mapping(target: 'copyWith', ignore: true)
Dog fromModel(DogModel model);
}
copied to clipboard
Will generate the mapper, using the factory constructor.
class DogMapperImpl extends DogMapper {
DogMapperImpl() : super();

@override
Dog fromModel(DogModel model) {
final dog = Dog(model.name);
return freezedtarget;
}
}
copied to clipboard
Static Mapping #
Static Methods in a Mapper Class will automatically be mapped with a static pendant in the generated mapper file.
class Dog {
final String name;
Dog(this.name);
}
class DogModel {
final String name;
DogModel(this.name);
}
copied to clipboard
@Mapper()
class DogMapper {
static Dog fromModel(DogModel model) => _$fromModel(model);
}
copied to clipboard
Will generate a mapper file providing the following static methods.
Dog _$fromModel(DogModel model) {
final dog = Dog(model.name);
return dog;
}
copied to clipboard
Static Mapping with a proxy #
Alternatively you can set generateStaticProxy to truein the Mapping Annotation, to generate a Mapper Proxy implementation for your static methods.
class Dog {
final String name;
Dog(this.name);
}
class DogModel {
final String name;
DogModel(this.name);
}
copied to clipboard
@Mapper(generateStaticProxy = true)
class DogMapper {
Dog fromModel(DogModel model);
}
copied to clipboard
Will generate the following mapper.
class DogMapperImpl extends DogMapper {
DogMapperImpl() : super();

@override
Dog fromModel(DogModel model) {
final dog = Dog(model.name);
return dog;
}
}

class DogMapper$ {
static final DogMapper mapper = DogMapperImpl();

static Dog fromModel(DogModel model) =>
mapper.fromModel(model);
}
copied to clipboard
Examples #
Please refer to the example package, for a list of examples and how to use the Mapper Annotation.
You can always run the examples by navigating to the examples package and executing the generator.
$ dart pub get
...
$ dart run build_runner build
copied to clipboard
Roadmap #
Feel free to open a Pull Request, if you'd like to contribute.
Or just open an issue, and i do my level best to deliver.

License

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Customer Reviews

There are no reviews.