safe_result 2.0.0 copy "safe_result: ^2.0.0" to clipboard
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
}
3
likes
150
points
62
downloads

Publisher

verified publishernolann-dev.xyz

Weekly Downloads

A Dart package providing a Result type for handling success and error cases in a type-safe way, with support for custom error types.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

More

Packages that depend on safe_result