Skip to content

Instantly share code, notes, and snippets.

@devkabiir
Last active October 1, 2019 18:24
Show Gist options
  • Save devkabiir/a1fd1ef2366fc27035dd7f4efc38764a to your computer and use it in GitHub Desktop.
Save devkabiir/a1fd1ef2366fc27035dd7f4efc38764a to your computer and use it in GitHub Desktop.
Built Value, Allowing `nullable` fields with default value to be explicitly set to `null`
class BuiltRef<T> {
/// Holds the ref, can also be null
final T value;
/// Whether this was constructed using [BuiltRef.undefined]
final bool isUndefined;
/// Create a new reference to [value]
const BuiltRef(this.value) : isUndefined = false;
/// Used as the default value in parameter definition
const BuiltRef.undefined()
: value = null,
isUndefined = true;
@override
int get hashCode => value.hashCode;
@override
operator ==(Object other) => other is BuiltRef
? value == other.value && isUndefined == other.isUndefined
: false;
@override
String toString() => value.toString();
}
abstract class MyValue {
/// String that can be null but also has a default
String get nullableStringWithDefault => 'default value';
/// Must have an int value
int get mustHaveInt;
MyValue._() {
if ((nullableStringWithDefault?.length ?? 0) > 50) {
throw Exception('String is too big');
}
print('validation checks pass!');
}
factory MyValue([Function(MyValueBuilder) updates]) = MyValueImpl;
}
class MyValueImpl extends MyValue {
BuiltRef<String> _nullableStringWithDefault;
String get nullableStringWithDefault =>
(_nullableStringWithDefault ??= BuiltRef(super.nullableStringWithDefault))
.value;
BuiltRef<int> _mustHaveInt;
int get mustHaveInt => _mustHaveInt.value;
MyValueImpl._withBuiltRef({
BuiltRef<String> nullableStringWithDefault = const BuiltRef.undefined(),
BuiltRef<int> mustHaveInt,
}) : _mustHaveInt = mustHaveInt,
_nullableStringWithDefault =
nullableStringWithDefault?.isUndefined ?? false
? null
: nullableStringWithDefault,
super._() {
/// Null checks
if (mustHaveInt?.value == null) {
throw ArgumentError.notNull('mustHaveInt');
}
print('null checks pass!');
}
/// Existing code that uses this constructor should work as expected
/// Here nullableStringWithDefault can be set to `null` explicitly
MyValueImpl._({
String nullableStringWithDefault,
int mustHaveInt,
}) : this._withBuiltRef(
nullableStringWithDefault: BuiltRef(nullableStringWithDefault),
mustHaveInt: BuiltRef(mustHaveInt),
);
/// Existing code using this factory will allow default values as well as setting to `null` explicitly
factory MyValueImpl([Function(MyValueBuilder) updates]) =>
(MyValueBuilder()..update(updates)).build();
}
class MyValueBuilder {
MyValueImpl _$v;
/// Initially, the builder doesn't initialize any fields so this will be null
BuiltRef<String> _nullableStringWithDefault;
String get nullableStringWithDefault =>
_$this._nullableStringWithDefault.value;
set nullableStringWithDefault(String nullableStringWithDefault) {
/// This is a nullable field, we don't care what the value is
_$this._nullableStringWithDefault = BuiltRef(nullableStringWithDefault);
}
BuiltRef<int> _mustHaveInt;
int get mustHaveInt => _$this._mustHaveInt.value;
set mustHaveInt(int mustHaveInt) {
/// For non-nullable fields, throw optionally to provide a better stack trace
_$this._mustHaveInt =
BuiltRef(mustHaveInt ?? (throw ArgumentError.notNull('mustHaveInt')));
/// Or just set the value and let the constructor of MyValueImpl handle null values
// _$this._mustHaveInt = BuiltRef(mustHaveInt);
}
MyValueBuilder get _$this {
if (_$v != null) {
_nullableStringWithDefault =
BuiltRef(_$v.nullableStringWithDefault) ?? BuiltRef.undefined();
_mustHaveInt = BuiltRef(_$v.mustHaveInt);
_$v = null;
}
return this;
}
void replace(MyValue value) => _$v = value;
void update([Function(MyValueBuilder) updates]) => updates?.call(_$this);
MyValue build() {
return _$v ??
MyValueImpl._withBuiltRef(
nullableStringWithDefault:
_nullableStringWithDefault ?? const BuiltRef.undefined(),
mustHaveInt: _mustHaveInt,
);
}
}
void main([List<String> args]) {
useCase("When default values are used", () {
final defaultValue = MyValue((b) => b.mustHaveInt = 1);
print(defaultValue.nullableStringWithDefault);
});
useCase("When default values are ignored (custom value is used)", () {
final customValue = MyValue((b) => b
..nullableStringWithDefault = "custom value"
..mustHaveInt = 2);
print(customValue.nullableStringWithDefault);
});
useCase("When default values are ignored and explicitly set to null", () {
final explicitNullValue = MyValue((b) => b
..nullableStringWithDefault = null
..mustHaveInt = 3);
print(explicitNullValue.nullableStringWithDefault);
});
useCase("When default values are ignored and validation fails", () {
try {
MyValue((b) => b
..nullableStringWithDefault =
"custom value tooooooooooooooooooooooooooooooooooooooooooooooooooooo big"
..mustHaveInt = 4);
} catch (e) {
print('validation checks failed!');
}
});
useCase("When default values are used but null checks fail", () {
try {
MyValue();
} catch (e) {
print('null checks failed');
}
});
useCase("When non-nullables are explicitly set to null", () {
try {
MyValueBuilder()
..update((b) => b.mustHaveInt = null)
..build();
} catch (e) {
print('Builder fails to build');
}
});
}
void useCase(String name, Function function) {
print("");
print("".padRight(40, "#"));
print(name);
function();
print("".padRight(40, "#"));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment