Skip to content

Commit

Permalink
Version 4.0: copyWith(...) function nullability support (#43)
Browse files Browse the repository at this point in the history
* basic implementation

* minor

* prevent unnecessary copyWithNull methods generation

* naming corrections

* naming + ignore cast_nullable_to_non_nullable

* docs + test fixes

* docs

* docs

* release
  • Loading branch information
numen31337 committed Jan 6, 2022
1 parent 36118fd commit 7907433
Show file tree
Hide file tree
Showing 19 changed files with 207 additions and 141 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
Provides [source_gen](https://pub.dev/packages/source_gen) `Generator` for generating `copyWith` extensions. For more info on this package check out [my blog article](https://www.oleksandrkirichenko.com/blog/dart-extensions/) or the [main documentation file](https://pub.dev/packages/copy_with_extension_gen).
Provides [Dart Build System](https://pub.dev/packages/build) builder for generating `copyWith` extensions for classes annotated with [copy_with_extension](https://pub.dev/packages/copy_with_extension).

For more information on how to use this package, see [copy_with_extension_gen](https://pub.dev/packages/copy_with_extension_gen).

For more information on how this package works, see [my blog article](https://oleksandrkirichenko.com/blog/dart-extensions).

## copy_with_extension
* Package: [copy_with_extension](https://pub.dev/packages/copy_with_extension)
Expand Down
6 changes: 5 additions & 1 deletion copy_with_extension/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
## 4.0.0
* **BREAKING** `copyWith` function now correctly supports nullification of nullable fields like so `copyWith(id: null)`.
* **BREAKING** `CopyWith` annotation for named constructor `namedConstructor` is renamed to `constructor` to be in sync with [json_serializable](https://pub.dev/packages/json_serializable).

## 3.0.0
* Updating `analyzer` to `>=2.0.0 <4.0.0`
* Named constructor support.
* Better error reporting.
* Introduction of the new `copyWith` function with nullability support. The default behaviour of this library assumes that this function is used as follows: `myInstance.copyWith.value("newValue")`. The old functionality is still available.
* Introduction of the new `copyWith` function with nullability support that can be used like so: `myInstance.copyWith.value("newValue")`. The old functionality is still available.
* **BREAKING** `generateCopyWithNull` is renamed to `copyWithNull`.

## 2.0.3 Dependency update
Expand Down
2 changes: 1 addition & 1 deletion copy_with_extension/example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class SimpleObjectImmutableField {
}

/// Allows the use of a private constructor.
@CopyWith(namedConstructor: "_")
@CopyWith(constructor: "_")
class SimpleObjectPrivateConstructor {
@CopyWithField(immutable: true)
final String? id;
Expand Down
20 changes: 12 additions & 8 deletions copy_with_extension/lib/copy_with_extension.dart
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
/// Provides annotation class to use with
/// [copy_with_extension_gen](https://pub.dev/packages/copy_with_extension_gen).
/// Provides `CopyWith` annotation class used by [copy_with_extension_gen](https://pub.dev/packages/copy_with_extension_gen).
library copy_with_extension;

/// Annotation used to indicate that the `copyWith` extension should be generated.
class CopyWith {
const CopyWith({
this.copyWithNull = false,
this.skipFields = false,
this.namedConstructor,
this.constructor,
});

/// Set `copyWithNull` to `true` if you want to use `copyWithNull` function that allows you to nullify the fields. E.g. `myInstance.copyWithNull(id: true, name: true)`. Otherwise it will be still generated for internal use but marked as private.
/// Set `copyWithNull` to `true` if you want to use `copyWithNull` function that allows you to nullify the fields. E.g. `myInstance.copyWithNull(id: true, name: true)`.
final bool copyWithNull;

/// Prevent the library from generating copyWith function for individual filelds. If you want to use only copyWith(...) function.
/// Prevent the library from generating `copyWith` functions for individual filelds e.g. `instance.copyWith.id("123")`. If you want to use only copyWith(...) function.
final bool skipFields;

/// Set `namedConstructor` if you want to use a named constructor instead. The generated fields will be derived from this constructor.
final String? namedConstructor;
/// Set `constructor` if you want to use a named constructor. The generated fields will be derived from this constructor.
final String? constructor;
}

/// Additional field related options for the `CopyWith`.
class CopyWithField {
const CopyWithField({this.immutable = false});

/// Indicates that the field should be hidden in the generated `copyWith` method. By setting this flag to `true` the field will always be copied as it is e.g. `userID` field.
/// Indicates that the field should be hidden in the generated `copyWith` method. By setting this flag to `true` the field will always be copied as it and excluded from `copyWith` interface.
final bool immutable;
}

/// This placeholder object is a default value for nullable fields to handle cases when the user wants to nullify the value.
class $CopyWithPlaceholder {
const $CopyWithPlaceholder();
}
4 changes: 2 additions & 2 deletions copy_with_extension/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: copy_with_extension
version: 3.0.0
version: 4.0.0
description: Annotation for generating `copyWith` extensions code using `copy_with_extension_gen`.
homepage: https://github.com/numen31337/copy_with_extension/tree/master/copy_with_extension
repository: https://github.com/numen31337/copy_with_extension
Expand All @@ -9,4 +9,4 @@ environment:
sdk: ">=2.12.0 <3.0.0"

dev_dependencies:
flutter_lints: ^1.0.0
flutter_lints: ^1.0.0
6 changes: 5 additions & 1 deletion copy_with_extension_gen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
## 4.0.0
* **BREAKING** `copyWith` function now correctly supports nullification of nullable fields like so `copyWith(id: null)`.
* **BREAKING** `CopyWith` annotation for named constructor `namedConstructor` is renamed to `constructor` to be in sync with [json_serializable](https://pub.dev/packages/json_serializable).

## 3.0.0
* Updating `analyzer` to `>=2.0.0 <4.0.0`
* Named constructor support.
* Better error reporting.
* Introduction of the new `copyWith` function with nullability support. The default behaviour of this library assumes that this function is used as follows: `myInstance.copyWith.value("newValue")`. The old functionality is still available.
* Introduction of the new `copyWith` function with nullability support that can be used like so: `myInstance.copyWith.value("newValue")`. The old functionality is still available.
* **BREAKING** `generateCopyWithNull` is renamed to `copyWithNull`.

## 2.0.3 Dependency update
Expand Down
25 changes: 15 additions & 10 deletions copy_with_extension_gen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ Provides [Dart Build System](https://pub.dev/packages/build) builder for generat
This library allows you to copy instances of immutable classes modifying specific fields like so:

```dart
myInstance.copyWith.fieldName("test") // Preferred way with nullability support.
myInstance.copyWith.fieldName("test") // Change a single field.
myInstance.copyWith(fieldName: "test", anotherField: "test") // Change multiple fields at once without nullability support.
myInstance.copyWith(fieldName: "test", anotherField: "test", nullableField: null) // Change multiple fields at once.
myInstance.copyWithNull(fieldName: true, anotherField: true) // Nullify multiple fields at once.
```
Expand All @@ -16,8 +16,8 @@ myInstance.copyWithNull(fieldName: true, anotherField: true) // Nullify multiple
## Usage

#### In your `pubspec.yaml` file:
- Add to `dependencies` section `copy_with_extension: ^3.0.0`
- Add to `dev_dependencies` section `copy_with_extension_gen: ^3.0.0`
- Add to `dependencies` section `copy_with_extension: ^4.0.0`
- Add to `dev_dependencies` section `copy_with_extension_gen: ^4.0.0`
- Add to `dev_dependencies` section `build_runner: ^2.1.7`
- Set `environment` to at least Dart `2.12.0` version like so: `">=2.12.0 <3.0.0"`

Expand All @@ -33,12 +33,12 @@ environment:

dependencies:
...
copy_with_extension: ^3.0.0
copy_with_extension: ^4.0.0

dev_dependencies:
...
build_runner: ^2.1.7
copy_with_extension_gen: ^3.0.0
copy_with_extension_gen: ^4.0.0
```

#### Annotate your class with `CopyWith` annotation:
Expand Down Expand Up @@ -69,18 +69,19 @@ flutter pub run build_runner build

```dart
const result = BasicClass(id: "id")
final copied = result.copyWith.text("test") // Results in BasicClass(id: "id", text: "test");
final copiedOne = result.copyWith.text("test") // Results in BasicClass(id: "id", text: "test");
final copiedTwo = result.copyWith(id: "foo", text: null) // Results in BasicClass(id: "foo", text: null);
```

## Additional features

#### Change several fields at once with copyWith()

You can modify multiple fields at once using `copyWith` as a function like so: `myInstance.copyWith(fieldName: "test", anotherField: "test")`. Be aware that this kind of usage does not support nullification and all passed `null` values will be ignored.
You can modify multiple fields at once using `copyWith` as a function like so: `myInstance.copyWith(fieldName: "test", anotherField: "test")`. Passing the `null` value to `non-nullable` fields will be ignored.

#### Nullifying instance fields:

The `copyWith` method ignores any `null` values that are passed to it. In order to nullify the class fields, an additional `copyWithNull` function can be generated. To achieve this, simply pass an additional parameter to your class annotation `@CopyWith(generateCopyWithNull: true)`.
In order to nullify the class fields, an additional `copyWithNull` function can be generated. To make use of it, pass an additional parameter to your class annotation `@CopyWith(generateCopyWithNull: true)`.

#### Immutable fields

Expand All @@ -91,4 +92,8 @@ If you want to prevent a particular field from modifying with `copyWith` method
final int myImmutableField;
```

By adding this annotation you forcing your generated `copyWith` to always copy this field as it is, without allowing its modification.
By adding this annotation you forcing your generated `copyWith` to always copy this field as it is, without exposing it in the function interface.

## How this library is better than `freezed`?

It isn't. This library is a non-intrusive alternative for those who only need the `copyWith` functionality and do not want to maintain the codebase in the way implied by the framework. This library only requires from your side the annotation of your class with `CopyWith()` and an indication of the `.part` file, everything else is up to you. [`freezed`](https://pub.dev/packages/freezed), on the other hand, offers many more code generation features but requires your models to be written in a framework-specific manner.
6 changes: 5 additions & 1 deletion copy_with_extension_gen/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ include: package:flutter_lints/flutter.yaml
analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
implicit-dynamic: false

linter:
rules:
cast_nullable_to_non_nullable: true
2 changes: 1 addition & 1 deletion copy_with_extension_gen/example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class SimpleObjectImmutableField {
}

/// Allows the use of a private constructor.
@CopyWith(namedConstructor: "_")
@CopyWith(constructor: "_")
class SimpleObjectPrivateConstructor {
@CopyWithField(immutable: true)
final String? id;
Expand Down
104 changes: 55 additions & 49 deletions copy_with_extension_gen/lib/src/copy_with_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,26 @@ class CopyWithGenerator extends GeneratorForAnnotation<CopyWith> {
final classAnnotation = readClassAnnotation(annotation);

final sortedFields =
sortedConstructorFields(classElement, classAnnotation.namedConstructor);
sortedConstructorFields(classElement, classAnnotation.constructor);
final typeParametersAnnotation = typeParametersString(classElement, false);
final typeParametersNames = typeParametersString(classElement, true);
final typeAnnotation = classElement.name + typeParametersNames;

return '''
${_copyWithProxyPart(
classAnnotation.namedConstructor,
classAnnotation.constructor,
classElement.name,
typeParametersAnnotation,
typeParametersNames,
sortedFields,
!classAnnotation.copyWithNull,
classAnnotation.skipFields,
)}
extension ${classElement.name}CopyWith$typeParametersAnnotation on ${classElement.name}$typeParametersNames {
/// CopyWith feature provided by `copy_with_extension_gen` library. Returns a callable class and can be used as follows: `instanceOf$classElement.name.copyWith(...)`. Be aware that this kind of usage does not support nullification and all passed `null` values will be ignored.${classAnnotation.skipFields ? "" : " Prefer to copy the instance with a specific field change that handles nullification of fields correctly, e.g. like this:`instanceOf$classElement.name.copyWith.fieldName(...)`"}
${"_${classElement.name}CWProxy$typeParametersNames get copyWith => _${classElement.name}CWProxy$typeParametersNames(this);"}
extension \$${classElement.name}CopyWith$typeParametersAnnotation on ${classElement.name}$typeParametersNames {
/// Returns a callable class that can be used as follows: `instanceOf$classElement.name.copyWith(...)`${classAnnotation.skipFields ? "" : " or like so:`instanceOf$classElement.name.copyWith.fieldName(...)`"}.
${"_\$${classElement.name}CWProxy$typeParametersNames get copyWith => _\$${classElement.name}CWProxyImpl$typeParametersNames(this);"}
${_copyWithNullPart(typeAnnotation, sortedFields, classAnnotation.namedConstructor, !classAnnotation.copyWithNull, classAnnotation.skipFields)}
${classAnnotation.copyWithNull ? _copyWithNullPart(typeAnnotation, sortedFields, classAnnotation.constructor, classAnnotation.skipFields) : ""}
}
''';
}
Expand All @@ -54,50 +53,57 @@ class CopyWithGenerator extends GeneratorForAnnotation<CopyWith> {
String _copyWithValuesPart(
String typeAnnotation,
List<FieldInfo> sortedFields,
String? namedConstructor,
String? constructor,
bool skipFields,
bool isAbstract,
) {
final constructorInput = sortedFields.fold<String>(
'',
(r, v) {
if (v.immutable) {
return r;
} else {
if (v.immutable) return r; // Skip the field

if (isAbstract) {
final type = v.type.endsWith('?') ? v.type : '${v.type}?';
return '$r $type ${v.name},';
} else {
return '$r Object? ${v.name} = const \$CopyWithPlaceholder(),';
}
},
);
final paramsInput = sortedFields.fold<String>(
'',
(r, v) {
if (v.immutable) {
return '$r ${v.name}: _value.${v.name},';
} else {
return '$r ${v.name}: ${v.name} ?? _value.${v.name},';
}
if (v.immutable) return '$r ${v.name}: _value.${v.name},';

return '''
$r ${v.name}:
${v.name} == const \$CopyWithPlaceholder()
? _value.${v.name}
// ignore: cast_nullable_to_non_nullable
: ${v.name} as ${v.type},''';
},
);

final constructorBody = isAbstract
? ""
: "{ return ${constructorFor(typeAnnotation, constructor)}($paramsInput); }";

return '''
/// This function does not support nullification of optional types, all `null` values passed to this function will be ignored. For nullification, use `$typeAnnotation(...).copyWithNull(...)` to set certain fields to `null`.${skipFields ? "" : " Prefer `$typeAnnotation(...).copyWith.fieldName(...)` to override fields one at a time with nullification support."}
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored.${skipFields ? "" : " You can also use `$typeAnnotation(...).copyWith.fieldName(...)` to override fields one at a time with nullification support."}
///
/// Usage
/// ```dart
/// $typeAnnotation(...).copyWith(id: 12, name: "My name")
/// ````
$typeAnnotation call({$constructorInput}) {
return ${constructorFor(typeAnnotation, namedConstructor)}($paramsInput);
}
$typeAnnotation call({$constructorInput}) $constructorBody
''';
}

/// Generates the complete `copyWithNull` function.
String _copyWithNullPart(
String typeAnnotation,
List<FieldInfo> sortedFields,
String? namedConstructor,
bool private,
String? constructor,
bool skipFields,
) {
/// Return if there is no nullable fields
Expand Down Expand Up @@ -126,60 +132,60 @@ class CopyWithGenerator extends GeneratorForAnnotation<CopyWith> {
},
);

final description = private
? ""
: '''
/// Copies the object with the specific fields set to `null`. If you pass `false` as a parameter, nothing will be done and it will be ignored. Don't do it.${skipFields ? "" : " Prefer `$typeAnnotation(...).copyWith.fieldName(...)` to override fields one at a time with nullification support."}
///
/// Usage
/// ```dart
/// $typeAnnotation(...).copyWithNull(firstField: true, secondField: true)
/// ````''';
final description = '''
/// Copies the object with the specific fields set to `null`. If you pass `false` as a parameter, nothing will be done and it will be ignored. Don't do it. Prefer `copyWith(field: null)`${skipFields ? "" : " or `$typeAnnotation(...).copyWith.fieldName(...)` to override fields one at a time with nullification support"}.
///
/// Usage
/// ```dart
/// $typeAnnotation(...).copyWithNull(firstField: true, secondField: true)
/// ````''';

return '''
$description
$typeAnnotation ${private ? "_" : ""}copyWithNull({$nullConstructorInput}) {
return ${constructorFor(typeAnnotation, namedConstructor)}($nullParamsInput);
$typeAnnotation copyWithNull({$nullConstructorInput}) {
return ${constructorFor(typeAnnotation, constructor)}($nullParamsInput);
}
''';
''';
}

/// Generates a `CopyWithProxy` class.
String _copyWithProxyPart(
String? namedConstructor,
String? constructor,
String type,
String typeParameters,
String typeParameterNames,
List<FieldInfo> sortedFields,
bool privateCopyWithNull,
bool skipFields,
) {
final typeAnnotation = type + typeParameterNames;
final filteredFields = sortedFields.where((e) => !e.immutable);
final nonNullableFields = filteredFields.where((e) => !e.nullable);
final nullableFields =
filteredFields.where((e) => !nonNullableFields.contains(e));

final nonNullableFunctions =
skipFields ? "" : nonNullableFields.map((e) => '''
final nonNullableFunctions = skipFields ? "" : filteredFields.map((e) => '''
@override
$type$typeParameterNames ${e.name}(${e.type} ${e.name}) => this(${e.name}: ${e.name});
''').join("\n");
final nullableFunctions = skipFields ? "" : nullableFields.map((e) => '''
$type$typeParameterNames ${e.name}(${e.type} ${e.name}) => ${e.name} == null ? _value.${privateCopyWithNull ? "_" : ""}copyWithNull(${e.name}: true) : this(${e.name}: ${e.name});
final nonNullableFunctionsInterface =
skipFields ? "" : filteredFields.map((e) => '''
$type$typeParameterNames ${e.name}(${e.type} ${e.name});
''').join("\n");

return '''
/// Proxy class for `CopyWith` functionality. This is a callable class and can be used as follows: `instanceOf$type.copyWith(...)`. Be aware that this kind of usage does not support nullification and all passed `null` values will be ignored.${skipFields ? "" : " Prefer to copy the instance with a specific field change that handles nullification of fields correctly, e.g. like this:`instanceOf$type.copyWith.fieldName(...)`"}
class _${type}CWProxy$typeParameters {
final $type$typeParameterNames _value;
abstract class _\$${type}CWProxy$typeParameters {
$nonNullableFunctionsInterface
const _${type}CWProxy(this._value);
${_copyWithValuesPart(typeAnnotation, sortedFields, constructor, skipFields, true)};
}
${_copyWithValuesPart(typeAnnotation, sortedFields, namedConstructor, skipFields)}
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOf$type.copyWith(...)`.${skipFields ? "" : " Additionally contains functions for specific fields e.g. `instanceOf$type.copyWith.fieldName(...)`"}
class _\$${type}CWProxyImpl$typeParameters implements _\$${type}CWProxy$typeParameterNames {
final $type$typeParameterNames _value;
$nullableFunctions
const _\$${type}CWProxyImpl(this._value);
$nonNullableFunctions
@override
${_copyWithValuesPart(typeAnnotation, sortedFields, constructor, skipFields, false)}
}
''';
}
Expand Down

0 comments on commit 7907433

Please sign in to comment.