diff --git a/mobile/lib/utils/option.dart b/mobile/lib/utils/option.dart new file mode 100644 index 0000000000..79e26a15c4 --- /dev/null +++ b/mobile/lib/utils/option.dart @@ -0,0 +1,86 @@ +sealed class Option { + const Option(); + + bool get isSome => this is Some; + bool get isNone => this is None; + + factory Option.some(T value) { + return Some(value); + } + + factory Option.none() { + return const None(); + } + + factory Option.from(T? value) { + if (value == null) { + return const None(); + } + return Some(value); + } + + T? unwrapOrNull() { + if (this is Some) { + return (this as Some).value; + } + return null; + } + + T unwrap() { + if (this is Some) { + return (this as Some).value; + } + throw StateError('Cannot unwrap None'); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! Option) return false; + if (this is None && other is None) { + return true; + } + return this is Some && + other is Some && + this.unwrap() == other.unwrap(); + } + + @override + int get hashCode { + if (this is Some) { + return (this.unwrap()).hashCode; + } + return 0; + } + + @override + String toString() { + if (this is Some) { + return 'Some(${this.unwrap()})'; + } + return 'None'; + } +} + +class Some extends Option { + final T value; + + const Some(this.value); +} + +class None extends Option { + const None(); +} + +// Implemented as an extension rather than adding to the Option class because +// the type argument for None is not known at compile time, and fallback to Never +// As such, when the method is called on Option, with a default value of T, +// a runtime error +extension OptionExtensions on Option { + T unwrapOr(T defaultValue) { + if (this is Some) { + return (this as Some).value; + } + return defaultValue; + } +} diff --git a/mobile/test/utils/option_test.dart b/mobile/test/utils/option_test.dart new file mode 100644 index 0000000000..13d6fcf2b0 --- /dev/null +++ b/mobile/test/utils/option_test.dart @@ -0,0 +1,93 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/utils/option.dart'; + +void main() { + group('Option', () { + test('should create a Some instance', () { + const value = 42; + final option = Option.some(value); + expect(option, isA>()); + expect(option.isSome, isTrue); + expect(option.isNone, isFalse); + }); + + test('Some instance should hold the correct value', () { + const value = 'immich'; + final option = Option.some(value); + expect(option.unwrapOrNull(), equals(value)); + }); + }); + + test('should create a None instance', () { + final option = Option.none(); + expect(option, isA()); + expect(option.isSome, isFalse); + expect(option.isNone, isTrue); + }); + + test('None instance are equal', () { + final option1 = Option.none(); + final option2 = Option.none(); + expect(option1, equals(option2)); + }); + + group('Option.from', () { + test('should create a Some instance for a non-null value', () { + const value = 100.5; + final option = Option.from(value); + expect(option, isA>()); + expect(option.isSome, isTrue); + expect(option.isNone, isFalse); + expect(option.unwrapOrNull(), equals(value)); + }); + + test('should create a None instance for a null value', () { + final String? value = null; + final option = Option.from(value); + expect(option, isA()); + expect(option.isSome, isFalse); + expect(option.isNone, isTrue); + }); + }); + + group('unwrap()', () { + test('should return the value for Some', () { + const value = 'immich'; + final option = Option.some(value); + expect(option.unwrap(), equals(value)); + }); + + test('should throw StateError for None', () { + final option = Option.none(); + expect(() => option.unwrap(), throwsStateError); + }); + }); + + group('unwrapOrNull()', () { + test('should return the value for Some', () { + const value = 'test'; + final option = Option.some(value); + expect(option.unwrapOrNull(), equals(value)); + }); + + test('should return null for None', () { + final option = Option.none(); + expect(option.unwrapOrNull(), isNull); + }); + }); + + group('unwrapOr()', () { + test('should return the value for Some', () { + const value = true; + const defaultValue = false; + final option = Option.some(value); + expect(option.unwrapOr(defaultValue), equals(value)); + }); + + test('should return the default value for None', () { + const defaultValue = 'immich'; + final option = Option.none(); + expect(option.unwrapOr(defaultValue), equals(defaultValue)); + }); + }); +}