Skip to content

Commit

Permalink
feat: Null Value Support (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
getBoolean authored Nov 4, 2023
1 parent e9024dd commit 0d08946
Show file tree
Hide file tree
Showing 9 changed files with 460 additions and 15 deletions.
10 changes: 10 additions & 0 deletions examples/envied_example/lib/nullable_env.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// lib/env/env.dart
import 'package:envied/envied.dart';

part 'nullable_env.g.dart';

@Envied(path: '.env', allowOptionalFields: false)
final class NullableEnv {
@EnviedField(optional: true)
static const String? key6 = _NullableEnv.key6;
}
11 changes: 11 additions & 0 deletions examples/envied_example/lib/nullable_env.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions examples/envied_example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,14 @@ packages:
path: "../../packages/envied"
relative: true
source: path
version: "0.4.0"
version: "0.5.0"
envied_generator:
dependency: "direct dev"
description:
path: "../../packages/envied_generator"
relative: true
source: path
version: "0.4.0"
version: "0.5.0"
file:
dependency: transitive
description:
Expand Down
31 changes: 31 additions & 0 deletions packages/envied/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,42 @@ print(Env.KEY2); // "VALUE2"
```

### Obfuscation

Add the ofuscate flag to EnviedField

```dart
@EnviedField(obfuscate: true)
```

### **Optional Environment Variables**

Enable `allowOptionalFields` to allow nullable types. When a default
value is not provided and the type is nullable, the generator will
assign the value to null instead of throwing an exception.

By default, optional fields are not enabled because it could be
confusing while debugging. If a field is nullable and a default
value is not provided, it will not throw an exception if it is
missing an environment variable.

For example, this could be useful if you are using an analytics service
for an open-source app, but you don't want to require users or contributors
to provide an API key if they build the app themselves.

```dart
@Envied(allowOptionalFields: true)
abstract class Env {
@EnviedField()
static const String? optionalServiceApiKey = _Env.optionalServiceApiKey;
}
```

Optional fields can also be enabled on a per-field basis by setting

```dart
@EnviedField(optional: true)
```

<br>

## License
Expand Down
31 changes: 27 additions & 4 deletions packages/envied/lib/src/envied_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,20 @@ final class Envied {
/// **Can be overridden by the per-field obfuscate option!**
final bool obfuscate;

const Envied(
{String? path, bool? requireEnvFile, this.name, this.obfuscate = false})
: path = path ?? '.env',
/// Allows all the values to be optional when the type is nullable.
///
/// With this enabled, the generator will not throw an exception
/// if the environment variable is missing and a default value was
/// not set.
final bool allowOptionalFields;

const Envied({
String? path,
bool? requireEnvFile,
this.name,
this.obfuscate = false,
this.allowOptionalFields = false,
}) : path = path ?? '.env',
requireEnvFile = requireEnvFile ?? false;
}

Expand All @@ -63,5 +74,17 @@ final class EnviedField {
/// The default value must be a [String], [bool] or a [num].
final Object? defaultValue;

const EnviedField({this.varName, this.obfuscate, this.defaultValue});
/// Allows this field to be optional when the type is nullable.
///
/// With this enabled, the generator will not throw an exception
/// if the environment variable is missing and a default value was
/// not set.
final bool? optional;

const EnviedField({
this.varName,
this.obfuscate,
this.defaultValue,
this.optional,
});
}
44 changes: 42 additions & 2 deletions packages/envied_generator/lib/src/generate_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,51 @@ import 'package:source_gen/source_gen.dart';
/// Since this function also does the type casting,
/// an [InvalidGenerationSourceError] will also be thrown if
/// the type can't be casted, or is not supported.
Iterable<Field> generateFields(FieldElement field, String value) {
Iterable<Field> generateFields(
FieldElement field,
String? value, {
bool allowOptional = false,
}) {
final String type = field.type.getDisplayString(withNullability: false);

late final Expression result;

if (value == null) {
if (!allowOptional) {
throw InvalidGenerationSourceError(
'Environment variable not found for field `${field.name}`.',
element: field,
);
}

// Early return if null, so need to check for allowed types
if (!field.type.isDartCoreInt &&
!field.type.isDartCoreDouble &&
!field.type.isDartCoreNum &&
!field.type.isDartCoreBool &&
!field.type.isDartCoreString &&
field.type is! DynamicType) {
throw InvalidGenerationSourceError(
'Envied can only handle types such as `int`, `double`, `num`, `bool` and'
' `String`. Type `$type` is not one of them.',
element: field,
);
}

return [
Field(
(FieldBuilder fieldBuilder) => fieldBuilder
..static = true
..modifier = FieldModifier.constant
..type = refer(field.type is DynamicType
? ''
: field.type.getDisplayString(withNullability: true))
..name = field.name
..assignment = literalNull.code,
),
];
}

if (field.type.isDartCoreInt ||
field.type.isDartCoreDouble ||
field.type.isDartCoreNum) {
Expand Down Expand Up @@ -57,7 +97,7 @@ Iterable<Field> generateFields(FieldElement field, String value) {
..type = refer(
field.type is DynamicType
? ''
: field.type.getDisplayString(withNullability: false),
: field.type.getDisplayString(withNullability: allowOptional),
)
..name = field.name
..assignment = result.code,
Expand Down
63 changes: 59 additions & 4 deletions packages/envied_generator/lib/src/generate_field_encrypted.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:math' show Random;

import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:code_builder/code_builder.dart';
import 'package:source_gen/source_gen.dart';
Expand All @@ -12,10 +13,50 @@ import 'package:source_gen/source_gen.dart';
/// Since this function also does the type casting,
/// an [InvalidGenerationSourceError] will also be thrown if
/// the type can't be casted, or is not supported.
Iterable<Field> generateFieldsEncrypted(FieldElement field, String value) {
Iterable<Field> generateFieldsEncrypted(
FieldElement field,
String? value, {
bool allowOptional = false,
}) {
final Random rand = Random.secure();
final String type = field.type.getDisplayString(withNullability: false);
final String keyName = '_enviedkey${field.name}';
final bool isNullable = allowOptional &&
field.type.nullabilitySuffix == NullabilitySuffix.question;

if (value == null) {
if (!allowOptional) {
throw InvalidGenerationSourceError(
'Environment variable not found for field `${field.name}`.',
element: field,
);
}

// Early return if null, so need to check for allowed types
if (!field.type.isDartCoreInt &&
!field.type.isDartCoreBool &&
!field.type.isDartCoreString &&
field.type is! DynamicType) {
throw InvalidGenerationSourceError(
'Obfuscated envied can only handle types such as `int`, `bool` and `String`. '
'Type `$type` is not one of them.',
element: field,
);
}

return [
Field(
(FieldBuilder fieldBuilder) => fieldBuilder
..static = true
..modifier = FieldModifier.final$
..type = refer(field.type is DynamicType
? ''
: field.type.getDisplayString(withNullability: true))
..name = field.name
..assignment = literalNull.code,
),
];
}

if (field.type.isDartCoreInt) {
final int? parsed = int.tryParse(value);
Expand Down Expand Up @@ -43,7 +84,11 @@ Iterable<Field> generateFieldsEncrypted(FieldElement field, String value) {
(FieldBuilder fieldBuilder) => fieldBuilder
..static = true
..modifier = FieldModifier.final$
..type = refer('int')
..type = TypeReference(
(b) => b
..symbol = 'int'
..isNullable = isNullable,
)
..name = field.name
// TODO(@techouse): replace with `Expression.operatorBitwiseXor` once https://github.com/dart-lang/code_builder/pull/427 gets merged
..assignment = Block.of([
Expand Down Expand Up @@ -81,7 +126,11 @@ Iterable<Field> generateFieldsEncrypted(FieldElement field, String value) {
(FieldBuilder fieldBuilder) => fieldBuilder
..static = true
..modifier = FieldModifier.final$
..type = refer('bool')
..type = TypeReference(
(b) => b
..symbol = 'bool'
..isNullable = isNullable,
)
..name = field.name
// TODO(@techouse): replace with `Expression.operatorBitwiseXor` once https://github.com/dart-lang/code_builder/pull/427 gets merged
..assignment = Block.of([
Expand Down Expand Up @@ -124,7 +173,13 @@ Iterable<Field> generateFieldsEncrypted(FieldElement field, String value) {
(FieldBuilder fieldBuilder) => fieldBuilder
..static = true
..modifier = FieldModifier.final$
..type = refer(field.type is DynamicType ? '' : 'String')
..type = field.type is DynamicType
? null
: TypeReference(
(b) => b
..symbol = 'String'
..isNullable = isNullable,
)
..name = field.name
..assignment = refer('String').type.newInstanceNamed(
'fromCharCodes',
Expand Down
23 changes: 20 additions & 3 deletions packages/envied_generator/lib/src/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'dart:io';

import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
Expand Down Expand Up @@ -38,6 +40,8 @@ final class EnviedGenerator extends GeneratorForAnnotation<Envied> {
annotation.read('requireEnvFile').literalValue as bool? ?? false,
name: annotation.read('name').literalValue as String?,
obfuscate: annotation.read('obfuscate').literalValue as bool,
allowOptionalFields:
annotation.read('allowOptionalFields').literalValue as bool? ?? false,
);

final Map<String, String> envs = await loadEnvs(
Expand Down Expand Up @@ -99,15 +103,28 @@ final class EnviedGenerator extends GeneratorForAnnotation<Envied> {
varValue = defaultValue?.toString();
}

if (varValue == null) {
if (field.type is InvalidType) {
throw InvalidGenerationSourceError(
'Envied requires types to be explicitly declared. `${field.name}` does not declare a type.',
element: field,
);
}

final bool optional = reader.read('optional').literalValue as bool? ??
config.allowOptionalFields;

// Throw if value is null but the field is not nullable
bool isNullable = field.type is DynamicType ||
field.type.nullabilitySuffix == NullabilitySuffix.question;
if (varValue == null && !(optional && isNullable)) {
throw InvalidGenerationSourceError(
'Environment variable not found for field `${field.name}`.',
element: field,
);
}

return reader.read('obfuscate').literalValue as bool? ?? config.obfuscate
? generateFieldsEncrypted(field, varValue)
: generateFields(field, varValue);
? generateFieldsEncrypted(field, varValue, allowOptional: optional)
: generateFields(field, varValue, allowOptional: optional);
}
}
Loading

0 comments on commit 0d08946

Please sign in to comment.