enum_assist 0.0.1 enum_assist: ^0.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';
Here's a full example of example.dart
:
import 'package:enum_assist_annotation/enum_assist_annotation.dart';
part 'example.ge.dart';
@EnumAssist()
enum SuperHero {
superman,
spiderman,
ironman,
}
Features #
Default Extension Methods #
The following methods will be generated with every enum annotated with EnumAssist
Name #
Returns the name of the enum value.
var greet = Greeting.friendly;
greet.name; // friendly
Description #
Returns the EnumValue.description of the enum value in a human readable format.
var greet = Greeting.friendly;
greet.description; // A friendly greeting
To Int #
Returns the EnumValue.intValue of the enum value.
Greeting.professional.toInt; // 0
Greeting.friendly.toInt; // 1
Greeting.relaxed.toInt; //2
Readable #
Returns the EnumValue.readable of the enum value in a human readable format.
var greet = Greeting.friendly;
greet.readable; // Friendly
Serialized #
Returns the EnumValue.serializedValue of the enum value.
Specific case formatting can be done with serializedFormat (either EnumAssist
or build.yaml
)
var greet = Greeting.friendly;
greet.serialized; // friendly
map/maybeMap #
Each enum will generate a .map(...)
& .maybeMap(...)
method, which is equivalent to pattern matching.
The base of all custom extension methods.
This enum will be used as reference:
@EnumAssist()
enum Greeting {
professional,
friendly,
relaxed,
}
Map #
.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
Maybe Map #
.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(
orElse: '*blank stare*',
professional: 'Hello Sir',
);
whatDoYouSay; // *blank stare*
Return Type #
.map<T>()
and .maybeMap<T>()
use generics to provide the return type of the callback.
While its not necessary to define the return type, it is recommended to do so.
Defining the return type protects you by forcing a specific return type.
-
Defined Return Type:
String
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
Here are a couple of examples of inferred return types:
-
Return type:
String
var greet = Greeting.friendly; final whatDoYouSay = greet.map( professional: 'Hello Sir', friendly: 'Hello', relaxed: 'Saaa dude!', ); whatDoYouSay.runtimeType; // String
-
Return type:
String?
var greet = Greeting.friendly; final whatDoYouSay = greet.map( professional: 'Hello Sir', friendly: null, relaxed: 'Saaa dude!', ); whatDoYouSay.runtimeType; // String?
-
Return type:
Object?
var greet = Greeting.friendly; final whatDoYouSay = greet.map( professional: 'Hello Sir', friendly: null, relaxed: 3, ); whatDoYouSay.runtimeType; // Object?
Note: Just because you can, doesn't mean you should...
With great power, comes great responsibility
Custom Extensions #
enum_assist allows you to create your own extension methods for your enum, with minimal code!
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 #
There are 4 arguments:
value
(required), The value for the enum field.- Can be any
const
value - Type is inferred from the class's
type
arguments
- Can be any
methodName
(required), The name of the method to be created- This must be unique and should not be use by any other extension classes
docComment
(optional), The doc comment for the method- multi-line strings are supported.
///
will be prepended to each new line.
allowNulls
(optional), Whether or not to returnnull
values- default:
false
- For the return type to be nullable, this must be set to
true
- default:
An example:
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 #
There are 5 arguments, 4 are the same as MapExtension
The 5th argument is:
defaultValue
(required), used as theorElse
callback value.- Can be any
const
value - Type is inferred from the class's type argument
- Can be any
Expected return values:
defaultValue
:- If the extension value is not defined
- If the extension value is
null
ANDallowNulls
isfalse
null
- If the extension value is
null
ANDallowNulls
istrue
- If the extension value is
- Defined value
- If the extension value is defined
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
Here's a quick example:
// Generated Json Converter class
final conv = GreetingConv();
final greet = Greeting.professional;
conv.toJson(greet); // professional
final greetJson = 'professional';
conv.fromJson(greetJson); // Greeting.professional
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),
);
}
}
And that's it!
And here is what the hero
's value would look like
final steve = Character('Steve Rogers', SuperHeroes.capitanAmerica);
final json = steve.toJson();
print(json['hero']); // Capitan America
Build Configuration #
If you want to customize the settings for each enum, you can specify it inside your build.yaml
file.
For example:
targets:
$default:
builders:
enum_assist:
enabled: true
options:
# possible values:
# - true
# - false
# default: true
create_json_conv: 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
Annotations #
Enum Assist #
Used to specify the enum you want to generate code for
Create Json Conversion #
field: createJsonConv
Whether or not to create a json converter class (non-nullable & nullable) for your enum.
enum_assist_annotation depends on json_annotation to generate JsonConverter
classes.
Even if you do not use json_serializable or json_annotation in your project, you can still use the generated conversion classes in your code.
Go to Json Converter Classes for an example
Serialized Format #
field: serializedFormat
Used By:
Sets the format you want the values of the enum to be serialized to.
enum_assist depends on change_case to format the serialized value.
The possible values for the build.yaml
file is any value from the SerializedFormat enum
Here's an example:
-
SerializedFormat.none (default)
static const _professionalName = 'professional';
-
SerializedFormat.snake
static const _veryProfessionalName = 'very_professional';
Use Doc Comment As Description #
Whether or not to use the enum value's doc comment as the description of the enum value.
If set to false
, the description will return null
, unless defined via EnumValue.description.
Enum:
@EnumAssist(
useDocCommentAsDescription: true,
)
enum Greeting {
/// A professional greeting
professional,
/// A friendly greeting
friendly,
/// A relaxed greeting
///
/// Which is my favorite!
relaxed,
}
final greet = Greeting.friendly;
greet.description; // A friendly greeting
// "A relaxed greeting
//
// Which is my favorite!"
Greeting.relaxed.description;
Use Int Value For Serialization #
Whether or not to use the enum's int value for serialization.
Setting this to true
, the serializedValue
field will be ignored.
@EnumAssist(
useIntValueForSerialization: true,
)
enum Greeting {
professional,
friendly,
relaxed,
}
final greet = Greeting.friendly;
greet.serialized; // 1
Greeting.relaxed.serialized; // 2
Notice This: Instead of returning
friendly
, it will return theintValue
of thefriendly
field, which is1
Enum Value #
Used to customize the generator for a specific enum value.
Readable #
Provides the name for readable of the enum value. The name should be a human readable format.
@EnumAssist()
enum Greeting {
@EnumValue(readable: 'Formal')
professional,
friendly,
relaxed,
}
final greet = Greeting.friendly;
greet.readable; // Friendly
Greeting.professional.readable; // Formal
Description #
Provides the description for the description of the enum value.
The description should be a human readable format.
For Example: for Example.isReallyCool
, the description could be The example is really cool!
Expected Return Value:
- Doc Comment of the enum value
null
if the EnumValue, EnumAssist, or build.yamluseDocCommentAsDescription
fields are set to false- Value of EnumValue.description (regardless of
useDocCommentAsDescription
's value)
@EnumAssist()
enum Greeting {
/// A professional greeting
@EnumValue(description: 'Recommended to use in the work place')
professional,
/// A friendly greeting
friendly,
relaxed,
}
final greet = Greeting.friendly;
greet.description; // A friendly greeting
Greeting.professional.description; // Recommended to use in the work place
Int Value #
Provides the int value for toInt of the enum value.
0 indexed integers used to represent the enum value.
When a value is assigned to EnumValue.intValue, the value will be passed on to the next enum field, incrementing by 1.
For example:
@EnumAssist()
enum ResponseCodes {
@EnumValue(intValue: 200)
ok,
created,
accepted,
@EnumValue(intValue: 400)
badRequest,
unauthorized,
@EnumValue(intValue: 403)
forbidden,
notFound,
@EnumValue(intValue: 500)
internalServerError,
notImplemented,
badGateway,
serviceUnavailable,
}
Their return values will be:
ResponseCode.ok.toInt; // 200
ResponseCode.created.toInt; // 201
ResponseCode.accepted.toInt; // 202
ResponseCode.badRequest.toInt; // 400
ResponseCode.unauthorized.toInt; // 401
ResponseCode.forbidden.toInt; // 403
ResponseCode.notFound.toInt; // 404
ResponseCode.internalServerError.toInt; // 500
ResponseCode.notImplemented.toInt; // 501
ResponseCode.badGateway.toInt; // 502
ResponseCode.serviceUnavailable.toInt; // 503
Serialized Value #
Provides the serialized representation of the enum value for serialized and json converter classes.
Specific case formatting can be done with serializedFormat (either EnumAssist
or build.yaml
)
Note: While the type for
serializedValue
isObject?
, the only accepted types areString
, andint
.
@EnumAssist()
enum Greeting {
@EnumValue(serializedValue: 'formal')
professional,
friendly,
@EnumValue(serializedValue: 3) // serializedValue accepts type Object?
relaxed,
}
final greet = Greeting.friendly;
greet.serialized; // friendly
Greeting.professional.serialized; // formal
Gretting.relaxed.serialized; // 3
Use Doc Comment As Description #
Whether or not to use the enum value's doc comment as the description of the enum value.
If set to false
, the description will return null
, unless defined via EnumValue.description.
@EnumAssist()
enum Greeting {
/// A professional greeting
@EnumValue(useDocCommentAsDescription: false)
professional,
/// A friendly greeting
friendly,
relaxed,
}
final greet = Greeting.friendly;
greet.description; // A friendly greeting
Greeting.professional.description; // null
Extensions #
Custom Extension Methods to be created for the enum, specified with the value of the enum field.
Extension classes must extend
MapExtension
, representing the.map(...)
methodMaybeExtension
, representing the.maybeMap(...)
method
Go to Examples for an example of how to create custom extensions.