enum_assist 0.1.0+1 enum_assist: ^0.1.0+1 copied to clipboard
Seamlessly generate extension methods and json conversion classes for your enums using enum_assist
Enum Assist
Seamlessly generate extension methods and json conversion classes for your enums!
Motivation #
Dart enums can be a bit tedious to work with. Serializing them to/from json, using
switch statements based off their values, or using describeEnum
or split('.')
to get the value's name are a couple of examples where working with enums could be improved.
Writing extensions has been a great way to add extra functionality to classes & enums. Though, you'll find yourself writing the same extensions over and over again. I was getting tired of copying and pasting code and changing a couple of things whenever I created a new enum. So I did what any other sane developer would do, I took a couple of weeks to create an automation tool to save me time. 🤪
So welcome enum_assist into your lifeproject! The fastest way to writing extension methods and json conversion classes for your enums!
Check out the example or the index to see what it can do.
Index #
How to use #
Install #
To use enum_assist, you will need to set up the build_runner (code-generator) for your project.
First, add the following packages to your pubspec.yaml
:
depenedencies:
enum_assist_annotation:
dev_dependencies:
build_runner:
enum_assist:
What are these packages?
- enum_assist_annotation, contains all the annotations that enum_assist references to generate code.
- Why a Dependency: The annotations are a part of your code, so enum_assist_annotation
must be part of the
dependencies
- Why a Dependency: The annotations are a part of your code, so enum_assist_annotation
must be part of the
- enum_assist, the code generator for all the annotations
- Why a Dev Dependency?: The generator only generates the code. So its technically not part of your code which means that enum_assist can be part of the
dev_dependencies
- Why a Dev Dependency?: The generator only generates the code. So its technically not part of your code which means that enum_assist can be part of the
- build_runner, a tool that any code-generator package uses to generate code
- Why a Dev Dependency?: Same reason as enum_assist
Generating the Code #
Build Runner Commands #
// If your package depends on Flutter
flutter pub run build_runner build
// If your package _does not_ depend on Flutter
dart pub run build_runner build
If you're new to build_runner, I suggest taking a look at these commands & options
File Setup #
Each file will need to start with the enum_assist import and the part
keyword.
import 'package:enum_assist_annotation/enum_assist_annotation.dart';
part 'example.ge.dart';
Features #
Default Extension Methods #
The following methods will be generated with every enum annotated with EnumAssist
Name
The name of the enum value.
Greeting.friendly.name; // friendly
Greeting.friendly.description; // A friendly greeting
Greeting.professional.toInt; // 0
Greeting.friendly.toInt; // 1
Greeting.relaxed.toInt; //2
Greeting.friendly.readable; // Friendly
Specific case formatting can be done with serializedFormat (either
EnumAssist
orbuild.yaml
)
Greeting.friendly.serialized; // friendly
map/maybeMap #
The base of all custom extension methods.
Each enum will generate a .map(...)
& .maybeMap(...)
method, which is equivalent to pattern matching.
.map()
provides callbacks for each enum value. All callbacks are required args and can return any value.
var greet = Greeting.friendly;
final whatDoYouSay = greet.map(
professional: 'Hello Sir',
friendly: 'Hello',
relaxed: 'Saaa dude!',
);
whatDoYouSay; // Hello
.maybeMap()
provides callbacks for each enum value, plus an orElse
callback.
orElse
is the only required arg.
var greet = Greeting.friendly;
final whatDoYouSay = greet.maybeMap(
professional: 'Hello Sir',
orElse: '*blank stare*',
);
whatDoYouSay; // *blank stare*
Return Type #
.map<T>()
and .maybeMap<T>()
use generics to provide the return type of the callback.
var greet = Greeting.friendly;
final whatDoYouSay = greet.map<String>(
professional: 'Hello Sir',
friendly: 'Hello',
// relaxed: 123, // compile error: `123` is not a type String
relaxed: 'Saaa dude!',
);
whatDoYouSay.runtimeType; // String
While its not necessary to define the return type, it is recommended to do so.
var greet = Greeting.friendly;
final whatDoYouSay = greet.map(
professional: 'Hello Sir',
friendly: 'Hello',
relaxed: 'Saaa dude!',
);
whatDoYouSay.runtimeType; // String
var greet = Greeting.friendly;
final whatDoYouSay = greet.map(
professional: 'Hello Sir',
friendly: null,
relaxed: 'Saaa dude!',
);
whatDoYouSay.runtimeType; // String?
var greet = Greeting.friendly;
final whatDoYouSay = greet.map(
professional: 'Hello Sir',
friendly: null,
relaxed: 3,
);
whatDoYouSay.runtimeType; // Object?
Custom Extensions #
enum_assist allows you to create your own extension methods for your enum
There are two ways to create your own extension methods.
You start by creating a class that extends MapExtension or MaybeExtension.
IMPORTANT: All properties (other than value
) need to be defined
within the super constructor!
Map Extension #
class SaidByExt extends MapExtension<String> {
const SaidByExt(String value)
: super(
value,
methodName: 'saidBy',
docComment: 'Greeting that is said by...',
allowNulls: false, // default
);
}
Add it to the the extensions
property
@EnumAssist()
enum Greeting {
@EnumValue(extensions: [SaidByExt('Executives')])
professional,
@EnumValue(extensions: [SaidByExt('Co-workers')])
friendly,
@EnumValue(extensions: [SaidByExt('Friends')])
relaxed,
}
Generated code:
/// Greeting that is said by...
String get saidBy {
return map<String>(
professional: 'Executives',
friendly: 'Co-workers',
relaxed: 'Friends',
);
}
Maybe Extension #
Expected return values:
defaultValue
:- When the extension value is not defined
- When the extension value is
null
ANDallowNulls
isfalse
null
- When the extension value is
null
ANDallowNulls
istrue
- When the extension value is
class HowFrequentExt extends MaybeExtension<int?> {
const HowFrequentExt(int? value)
: super(
value,
methodName: 'howFrequent',
docComment: '1-10, How often this greeting is used.\n\n`null` if never used',
defaultValue: 0,
allowNulls: true,
);
}
Add it to the the extensions
property
@EnumAssist()
enum Greeting {
@EnumValue(extensions: [])
professional,
@EnumValue(extensions: [HowFrequentExt(3)])
friendly,
@EnumValue(extensions: [HowFrequentExt(8)])
relaxed,
@EnumValue(extensions: [HowFrequentExt(null)])
rude,
}
Generated Code:
/// 1-10, How often this greeting is used
///
/// `null` if never used
int? get howFrequent {
return maybeMap<int?>(
// returns default value
//? if theres an argument provided, it does nothing.
orElse: HowFrequentExt(3).defaultValue,
professional: HowFrequentExt(3).defaultValue,
friendly: 3,
relaxed: 8,
rude: null,
);
}
Notice This:
For the generated code to access thedefaultValue
, it must create an instance of the extension class. If there is a required argument, the arg must be passed to avoid exceptions. Therefore, the required arg will be provided & ignored to return default value.
Note: If an extension is omitted (like
professional
), the default value will be used.
null
will only be returned if declared with anull
value. (likerude
)
Json Converter #
Serializing enums almost always requires a switch statement.
Mistakes can easily be made when converting from a String
(or other types) to an enum.
The Json converter class is a great way to handle your enums' serialization.
The name of json converter class will be ${NAME_OF_ENUM}Conv
For a detailed example, go to toJson/fromJson
// Generated Json Converter class
final conv = GreetingConv();
conv.toJson(Greeting.professional); // professional
conv.fromJson('professional'); // Greeting.professional
Build Configuration #
Customize the settings for each enum, or for all enums inside your build.yaml
file.
targets:
$default:
builders:
enum_assist:
enabled: true
options:
# possible values:
# - true
# - false
# default: true
create_json_conv: true
create_name: true
create_description: true
create_to_int: true
create_readable: true
create_serialized: true
# possible values:
# - camel
# - capital
# - constant
# - dot
# - header
# - kebab
# - no
# - none
# - pascal
# - path
# - sentence
# - snake
# - swap
# default: none
serialized_format: none
# possible values:
# - true
# - false
# default: true
use_doc_comment_as_description: true
# possible values:
# - true
# - false
# default: false
use_int_value_for_serialization: false
Some Examples!
Examples #
Map Example #
Lets create a .response
method for the enum Greeting
enum.
This method will return a String
with the response to the greeting.
We first need to create our extension class.
class ResponseExt extends MapExtension<String> {
const ResponseExt(String value)
: super(
value,
methodName: 'response',
docComment: 'The response to a greeting',
);
}
Note:
TheMapExtension
class also has anallowNulls
argument, which defaults tofalse
.
This can be set totrue
to change the return type nullable.
Next, we need to add our extension to the enum.
This can be done by annotating the enum's values with EnumValue,
And then adding the extension to the extensions
field.
Note:
Because the.map(...)
requires all args to be defined, we must add theResponseExt
extension to ALL enum fields.
Failure to do so will result in an error when generating the code.
@EnumAssist()
enum Greeting {
@EnumValue(
extensions: [
ResponseExt('Hello, how do you do?'),
],
)
professional,
@EnumValue(
extensions: [
ResponseExt('Hey! Hows it going?'),
],
)
friendly,
@EnumValue(
extensions: [
ResponseExt('Whats up my dude!?'),
],
)
relaxed,
}
After the build_runner has run, you can now access the .response
method on the Greeting
enum.
var greet = Greeting.friendly;
greet.response; // Hey! Hows it going?
Greeting.relaxed.response; // Whats up my dude!?
MaybeMap Example #
Lets create a .isCool
method for the Greeting
enum.
This method will return true
only if the enum value is friendly
or relaxed
. Or else it will return false
.
We first need to create our extension class.
class IsCoolExt extends MaybeExtension<bool> {
const IsCoolExt(bool value)
: super(
value,
methodName: 'isCool',
defaultValue: false,
docComment: 'Is this a cool greeting?',
);
}
Note:
TheMaybeExtension
class also has anallowNulls
argument, which defaults tofalse
.
This can be set totrue
if you want the return type to be nullable.
Note:
The constructor could take a named argument with a default value to reduce the amount of code needed.const IsCoolExt([bool value = true])
Next, we need to add our extension to the enum.
This can be done by adding the EnumValue
annotation to any enum field.
And then adding the extension to the extensions
list.
@EnumAssist()
enum Greeting {
professional,
@EnumValue(
extensions: [
IsCoolExt(true),
],
)
friendly,
@EnumValue(
extensions: [
IsCoolExt(true),
],
)
relaxed,
}
Notice This:
We did not annotateprofessional
withEnumValue
orIsCoolExt
.
This is because.maybeMap(...)
doesn't require all callbacks to be defined.The generator will use the
defaultValue
fromIsCoolExt
as the return value.
After the build_runner has run, you can now access the .isCool
method on the Greeting
enum.
var greet = Greeting.friendly;
greet.isCool; // true
Greeting.professional.isCool; // false
toJson & fromJson #
Serializing enums almost always requires a switch statement.
Mistakes can easily be made when converting from a string to an enum.
Json Converter Classes are a great way to handle this.
Let's create an enum for the two examples below, Using json_serializable & Manually Serializing.
@EnumAssist()
enum SuperHeroes {
@EnumValue(serializedValue: 'Capitan America')
capitanAmerica,
@EnumValue(serializedValue: 'Black Widow')
blackWidow,
@EnumValue(serializedValue: 'Dr. Strange')
drStrange,
}
Using json_serializable #
json_serializable will automatically serialize enums to json by using describeEnum. This is great if your enum's values are exactly the same as the json values. But that is not always the case, just like our SuperHeroes
enum.
Let's use our generated class SuperHeroesConv
fix that problem!
Here is our example class, annotated with JsonSerializable
@JsonSerializable()
class Character {
const Character(
this.secretIdentity,
this.hero,
this.powerLevel,
);
final String secretIdentity;
final SuperHeroes hero;
factory Character.fromJson(Map<String, dynamic> json) =>
_$CharacterFromJson(json);
}
By default, json_serializable will serialize our hero field using the literal enum value as a String
.
final steve = Character('Steve Rogers', SuperHeroes.capitanAmerica);
final json = steve.toJson();
print(json['hero']); //capitanAmerica
To tell json_serializable to convert the hero
field with the values provided by EnumValue.serializedValue
, you'll need to annotated the field in your class
final String secretIdentity;
@SuperHeroesConv()
final SuperHeroes hero;
Note:
If hero
were nullable, you would need to annotate the field with a nullable converter
final String secretIdentity;
@SuperHeroesConv.nullable
final SuperHeroes hero;
After you run the build_runner, the json value for the hero
field will now be
final steve = Character('Steve Rogers', SuperHeroes.capitanAmerica);
final json = steve.toJson();
print(json['hero']); // Capitan America
Manually Serializing #
Here is an example of what your class could look like
class Character {
const Character(
this.secretIdentity,
this.hero,
);
final String secretIdentity;
final SuperHeroes hero;
Map<String, dynamic> toJson() {
return {
'secretIdentity': secretIdentity,
'hero': superHeroToJson(hero),
};
}
factory Character.fromJson(Map<String, dynamic> json) {
return Character(
json['secretIdentity'] as String,
superHeroFromJson(json['hero'] as String),
);
}
}
String superHeroToJson(SuperHeroes hero) {
switch (hero) {
case SuperHeroes.capitanAmerica:
return 'Capitan America';
case SuperHeroes.blackWidow:
return 'Black Widow';
case SuperHeroes.drStrange:
return 'Dr. Strange';
}
}
SuperHeroes superHeroFromJson(String hero) {
switch (hero) {
case 'Capitan America':
return SuperHeroes.capitanAmerica;
case 'Black Widow':
return SuperHeroes.blackWidow;
case 'Dr. Strange':
return SuperHeroes.drStrange;
default:
throw Exception('Could not find superhero for $hero');
}
}
It's a lot of work to just convert an enum to json!
Thankfully, the generated class SuperHeroConv
can do all of the work here
Our toJson
and fromJson
methods will now look like this
class Character {
const Character(
this.secretIdentity,
this.hero,
);
final String secretIdentity;
final SuperHeroes hero;
static const _conv = SuperHeroesConv();
Map<String, dynamic> toJson() {
return {
'secretIdentity': secretIdentity,
'hero': _conv.toJson(hero),
// you could also use the `serialized` method here
// which is the same as _conv.toJson(hero)
//
// 'hero': hero.serialized,
};
}
factory Character.fromJson(Map<String, dynamic> json) {
return Character(
json['secretIdentity'] as String,
_conv.fromJson(json['hero'] as String),
);
}
}
Here's what the hero
's value would look like
final steve = Character('Steve Rogers', SuperHeroes.capitanAmerica);
final json = steve.toJson();
print(json['hero']); // Capitan America