Skip to content

Catchable — A Swift macro that generates decorators for protocols to inject customizable error processors, enabling reusable error handling and transformation for throwing functions.

License

Notifications You must be signed in to change notification settings

pawel-sp/Catchable

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Catchable

Catchable is a Swift macro that can be applied to any protocol to automatically generate a decorator that enables the injection of a custom error processor, simplifying the handling and transformation of errors thrown by conforming types.

Motivation

Consider the following example with a protocol and its conforming class:

protocol FooProtocol {
    func foo() throws
    func bar() throws
}

final class FooService {
    enum Error: Swift.Error {
        case critical
    }

    func foo(path: String) throws {
        throw Error.critical
    }
}

final class Foo: FooProtocol {
    private let service: FooService

    init(service: FooService) {
        self.service = service
    }

    func foo() throws -> [String] {
        try service.foo(path: "foo")
    }

    func bar() throws {
        try service.foo(path: "foo")
    }
}

The Foo class can throw a variety of errors originating from multiple dependencies. Since not all of these errors are user-friendly or localized, you might want to map them to a custom application-specific error type to improve user experience:

enum AppError: LocalizedError {
    case somethingWentWrong

    var errorDescription: String? {
        switch self {
        case .somethingWentWrong: "Oops!"
        }
    }
}

final class Foo: FooProtocol {
    private let service: FooService

    init(service: FooService) {
        self.service = service
    }

    func foo() throws {
        do {
            try service.foo(path: "foo")
        } catch {
            throw AppError.somethingWentWrong
        }
    }

    func bar() throws {
        do {
            try service.foo(path: "bar")
        } catch {
            throw AppError.somethingWentWrong
        }
    }
}

In this version, all errors are mapped to AppError, making them more suitable for display to the user. However, this manual approach introduces repetitive boilerplate code, especially when applied across multiple methods or classes.

Catchable eliminates this boilerplate by generating a decorator that wraps your implementation and applies a reusable error processor, streamlining error handling with minimal effort.

Usage

Here is how the same example looks using the @Catchable macro:

@Catchable
protocol FooProtocol {
    func foo() throws
    func bar() throws
}

// `ErrorProcessor` is a protocol provided by the `Catchable` library.
// It allows custom logic for handling or transforming errors.
struct AppErrorProcessor: ErrorProcessor {
    func callAsFunction(_ error: Error) -> Error {
        switch error {
        case FooService.Error.criticalError: AppError.somethingWentWrong
        default: error
        }
    }
}

final class Foo: FooProtocol {
    private let service: FooService

    init(service: FooService) {
        self.service = service
    }

    func foo() throws {
        try service.foo(path: "foo")
    }

    func bar() throws {
        try service.foo(path: "bar")
    }
}

// `catchable(errorProcessor:)` is automatically generated by the macro.
let userFriendlyFoo = Foo(service: FooService()).catchable(errorProcessor: AppErrorProcessor())

In this setup, userFriendlyFoo wraps all method calls in error processing logic. Any error thrown by userFriendlyFoo methods is passed through AppErrorProcessor, which transforms the FooService.Error.critical error into AppError.somethingWentWrong.

You can use the Catchable macro not only to map errors but also to process them in any way you need. For example, to log errors to a remote service:

struct AppErrorProcessor: ErrorProcessor {
    func callAsFunction(_ error: Error) -> Error {
    	RemoteService.log(error)
        return error
    }
}

This makes error-handling logic highly reusable, whether it's for mapping, logging, analytics, or custom error reporting.

Limitations

The Catchable macro works by generating a private decorator class named CatchableDecorator, which conforms to the annotated protocol. Due to current macro system constraints, only one @Catchable macro can be used per file. To apply Catchable to multiple protocols, place each annotated protocol in a separate source file.

License

Catchable is released under the MIT license. See the LICENSE file for more info.

About

Catchable — A Swift macro that generates decorators for protocols to inject customizable error processors, enabling reusable error handling and transformation for throwing functions.

Resources

License

Stars

Watchers

Forks

Languages