Skip to content

britjair/smartstruct

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

71 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Smartstruct - Dart bean mappings - the easy nullsafe way!

Code generator for generating type-safe mappers in dart, inspired by https://mapstruct.org/

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:

Run the generator

dart run build_runner build
flutter packages pub run build_runner build
// or watch
flutter packages pub run build_runner watch

Usage

Create your beans.

class Dog {
    final String breed;
    final int age;
    final String name;
    Dog(this.breed, this.age, this.name);
}
class DogModel {
    final String breed;
    final int age;
    final String name;
    DogModel(this.breed, this.age, this.name);
}

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);
}

Once you ran the generator, next to your dog.mapper.dart a dog.mapper.g.dart will be generated.

dart run build_runner build
// dogmapper.mapper.g.dart
class DogMapperImpl extends DogMapper {
    @override
    Dog fromModel(DogModel model) {
        Dog dog = Dog(model.breed, model.age, model.name);
        return dog;
    }
}

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);
}

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;
  }
}

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);
}

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);
}
@Mapper()
class DogMapper {
    @Mapping(source: 'dogName', target: 'name')
    Dog fromModel(DogModel model);
}

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;
    }
}

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);
}
@Mapper()
class DogMapper {
    static String randomBreed(DogModel model) => 'some random breed';

    @Mapping(source: randomBreed, target: 'breed')
    Dog fromModel(DogModel model);
}

Will generate the following Mapper.

class DogMapperImpl extends DogMapper {
    @override
    Dog fromModel(DogModel model) {
        Dog dog = Dog(model.dogName, DogMapper.randomBreed(model));
        return dog;
    }
}

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);
}

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;
  }
}

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);
}

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;
  }
}

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);
}
// dogmapper.mapper.g.dart
@LazySingleton(as: DogMapper)
class DogMapperImpl extends DogMapper {...}

Optional to Required

If target class property is not optional but source property is mapping will fail in case of null value.

You can avoid this by specifying defaultValue property in @Mapping annotation.

class NullablePropertiesTarget {
  final String text;
  final num number;

  NullablePropertiesTarget(this.text, this.number);
}

class NullablePropertiesSource {
  final String? text;
  final num? number;

  NullablePropertiesSource(this.text, this.number);
}

abstract class NullablePropertiesMapper {
  @Mapping(source: 'number', target: 'number', defaultValue: '0')
  NullablePropertiesTarget fromSource(NullablePropertiesSource source);
}

Will produce:

class NullablePropertiesMapperImpl extends NullablePropertiesMapper {
  NullablePropertiesMapperImpl() : super();

  @override
  NullablePropertiesTarget fromSource(NullablePropertiesSource source) {
    assert(source.text != null, 'text cannot be blank');
    final nullablepropertiestarget =
        NullablePropertiesTarget(source.text!, source.number ?? 0);
    return nullablepropertiestarget;
  }
}

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

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.

About

Dart Code Generator for generating mapper classes with default

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Dart 100.0%