safe_result 2.0.0
safe_result: ^2.0.0 copied to clipboard
A Dart package providing a Result type for handling success and error cases in a type-safe way, with support for custom error types.
example/safe_result_example.dart
import 'package:safe_result/safe_result.dart';
// Custom error types
sealed class AppError {
final String message;
const AppError(this.message);
@override
String toString() => message;
}
class ValidationError extends AppError {
const ValidationError(super.message);
}
class NetworkError extends AppError {
final int statusCode;
const NetworkError(super.message, this.statusCode);
@override
String toString() => '$message (Status: $statusCode)';
}
class DatabaseError extends AppError {
const DatabaseError(super.message);
}
void main() {
// Basic usage examples
basicExample();
// Pattern matching examples
patternMatchingExample();
// Transformation examples
transformationExample();
// Validation example
validationExample();
// Async example
asyncExample();
// Error handling examples
errorHandlingExample();
// Nullable Result examples
nullableExample();
}
void basicExample() {
// Creating Results with specific error types
final success = Result<int, ValidationError>.ok(42);
final failure = Result<int, ValidationError>.error(
ValidationError('Value must be positive'));
// Checking result type
print('Is success? ${success.isOk}'); // true
print('Is failure? ${failure.isError}'); // true
// Getting values
final value = success.getOrElse(0);
print('Value or default: $value'); // 42
final fallback = failure.getOrElse(0);
print('Fallback value: $fallback'); // 0
}
void patternMatchingExample() {
// Pattern matching with switch expressions
Result<String, ValidationError> processInput(String input) => switch (input) {
'' => Result.error(ValidationError('Input cannot be empty')),
var str when str.length < 3 =>
Result.error(ValidationError('Input too short')),
var str => Result.ok(str.toUpperCase())
};
// Using pattern matching in switch statements
void handleResult(Result<String, ValidationError> result) {
switch (result) {
case Ok(value: final v):
print('Success: $v');
case Error(error: final e):
print('Error: $e');
}
}
handleResult(processInput('')); // Error: Input cannot be empty
handleResult(processInput('hi')); // Error: Input too short
handleResult(processInput('hello')); // Success: HELLO
}
void transformationExample() {
final result = Result<int, ValidationError>.ok(42);
// Using map to transform success values
final doubled = result.map((value) => value * 2);
print(doubled); // Ok(84)
// Using fold to handle both success and error cases
final stringified = result.fold(
onOk: (value) => 'Value is: $value',
onError: (error) => 'Error: $error',
);
print(stringified); // Value is: 42
// Chaining transformations
final complex = result.map((value) => value * 2).fold(
onOk: (value) => value > 50 ? 'Large' : 'Small',
onError: (error) => 'Error: $error',
);
print(complex); // Large
}
// Data class for validation
class User {
final String name;
final int age;
final String email;
User(this.name, this.age, this.email);
@override
String toString() => 'User(name: $name, age: $age, email: $email)';
}
void validationExample() {
// Validation functions returning Result with ValidationError
Result<String, ValidationError> validateName(String name) {
if (name.isEmpty) {
return Result.error(ValidationError('Name cannot be empty'));
}
if (name.length < 2) {
return Result.error(ValidationError('Name too short'));
}
return Result.ok(name);
}
Result<int, ValidationError> validateAge(int age) {
if (age < 0 || age > 120) {
return Result.error(ValidationError('Invalid age'));
}
return Result.ok(age);
}
Result<String, ValidationError> validateEmail(String email) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(email)) {
return Result.error(ValidationError('Invalid email format'));
}
return Result.ok(email);
}
// Combining multiple validations
Result<User, ValidationError> createUser(String name, int age, String email) {
final nameResult = validateName(name);
final ageResult = validateAge(age);
final emailResult = validateEmail(email);
return switch ((nameResult, ageResult, emailResult)) {
(Ok(value: final n), Ok(value: final a), Ok(value: final e)) =>
Result.ok(User(n, a, e)),
(Error(error: final e), _, _) => Result.error(e),
(_, Error(error: final e), _) => Result.error(e),
(_, _, Error(error: final e)) => Result.error(e),
};
}
// Example usage
final validUser = createUser('John', 30, 'john@example.com');
final invalidUser = createUser('', -5, 'invalid-email');
print(validUser); // Ok(User(name: John, age: 30, email: john@example.com))
print(invalidUser); // Error: Name cannot be empty
}
Future<void> asyncExample() async {
// Simulated async operations returning Result with NetworkError
Future<Result<String, NetworkError>> fetchUserName() async {
try {
// Simulate API call
await Future.delayed(Duration(seconds: 1));
return Result.ok('John Doe');
} catch (e) {
return Result.error(NetworkError('Failed to fetch user', 500));
}
}
Future<Result<int, NetworkError>> fetchUserAge(String userName) async {
try {
// Simulate API call
await Future.delayed(Duration(seconds: 1));
return Result.ok(30);
} catch (e) {
return Result.error(NetworkError('Failed to fetch age', 404));
}
}
// Using andThen for chaining async operations
final result = await fetchUserName().andThen((name) => fetchUserAge(name));
// Handle the final result
switch (result) {
case Ok(value: final age):
print('User age: $age');
case Error(error: final e):
print('Error: $e');
}
}
void errorHandlingExample() {
// Function that might throw
int riskyOperation(int value) {
if (value < 0) {
throw DatabaseError('Value cannot be negative');
}
return value * 2;
}
// Using Result to handle exceptions
Result<int, DatabaseError> safeOperation(int value) {
try {
return Result.ok(riskyOperation(value));
} on DatabaseError catch (e) {
return Result.error(e);
}
}
// Example usage
final success = safeOperation(5);
final failure = safeOperation(-5);
// Using fold for unified error handling
void processResult(Result<int, DatabaseError> result) {
final message = result.fold(
onOk: (value) => 'Operation succeeded: $value',
onError: (error) => 'Operation failed: $error',
);
print(message);
}
processResult(success); // Operation succeeded: 10
processResult(failure); // Operation failed: Value cannot be negative
}
void nullableExample() {
String? nullableName;
String? validName = 'John';
// Converting nullable to Result with specific error type
final nullResult = Result<String?, ValidationError>.ok(nullableName)
.requireNotNull(ValidationError('Name is required'));
final validResult = Result<String?, ValidationError>.ok(validName)
.requireNotNull(ValidationError('Name is required'));
print(nullResult); // Error: Name is required
print(validResult); // Ok: John
// Transforming nullable values
final transformed = Result<String?, ValidationError>.ok(validName)
.requireNotNull(ValidationError('Name is required'))
.map((name) => name.toUpperCase());
print(transformed); // Ok: JOHN
}