Section 7/91 menit

7. Real Use Cases

7. Real Use Cases

Use Case 1: Validation Pipeline

swift
// Validation layer yang ketat — setiap validator hanya bisa gagal dengan satu jenis error

enum ValidationError: Error, LocalizedError {
    case tooShort(field: String, minimum: Int, actual: Int)
    case tooLong(field: String, maximum: Int, actual: Int)
    case invalidCharacters(field: String, allowed: CharacterSet)
    case alreadyTaken(field: String, value: String)
    case requiredField(String)
    
    var errorDescription: String? {
        switch self {
        case .tooShort(let field, let min, let actual):
            return "\(field) minimal \(min) karakter (sekarang \(actual))"
        case .tooLong(let field, let max, let actual):
            return "\(field) maksimal \(max) karakter (sekarang \(actual))"
        case .invalidCharacters(let field, _):
            return "\(field) mengandung karakter tidak valid"
        case .alreadyTaken(let field, let value):
            return "\(field) '\(value)' sudah digunakan"
        case .requiredField(let field):
            return "\(field) wajib diisi"
        }
    }
}

struct UserRegistrationForm {
    let username: String
    let email: String
    let password: String
}

// Setiap validator hanya throws ValidationError — tidak lebih
func validateUsername(_ username: String) throws(ValidationError) {
    guard !username.isEmpty else { throw ValidationError.requiredField("username") }
    guard username.count >= 3 else {
        throw ValidationError.tooShort(field: "username", minimum: 3, actual: username.count)
    }
    guard username.count <= 30 else {
        throw ValidationError.tooLong(field: "username", maximum: 30, actual: username.count)
    }
    
    let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_-"))
    guard username.unicodeScalars.allSatisfy({ allowed.contains($0) }) else {
        throw ValidationError.invalidCharacters(field: "username", allowed: allowed)
    }
}

func validateEmail(_ email: String) throws(ValidationError) {
    guard !email.isEmpty else { throw ValidationError.requiredField("email") }
    guard email.contains("@") && email.contains(".") else {
        throw ValidationError.invalidCharacters(field: "email", allowed: .init())
    }
}

func validatePassword(_ password: String) throws(ValidationError) {
    guard !password.isEmpty else { throw ValidationError.requiredField("password") }
    guard password.count >= 8 else {
        throw ValidationError.tooShort(field: "password", minimum: 8, actual: password.count)
    }
}

// Aggregator — mengumpulkan semua validation errors
func validateForm(_ form: UserRegistrationForm) -> [ValidationError] {
    var errors: [ValidationError] = []
    
    if let error = try? { try validateUsername(form.username) }() {
        // Tip: closure trick untuk catch typed error ke optional
    }
    
    // Pattern yang lebih bersih:
    do { try validateUsername(form.username) } catch { errors.append(error) }
    do { try validateEmail(form.email) } catch { errors.append(error) }
    do { try validatePassword(form.password) } catch { errors.append(error) }
    
    return errors
}

// Penggunaan di ViewModel
@MainActor
class RegistrationViewModel: ObservableObject {
    @Published var validationErrors: [ValidationError] = []
    
    func submit(form: UserRegistrationForm) async {
        let errors = validateForm(form)
        validationErrors = errors
        
        guard errors.isEmpty else { return }
        
        do {
            try await registerUser(form)
        } catch {
            // error di sini adalah RegistrationError — typed
            handleRegistrationError(error)
        }
    }
}

Use Case 2: File Parser dengan Typed Error

swift
// CSV Parser — contoh typed throws di parsing pipeline

enum CSVError: Error {
    case emptyInput
    case invalidEncoding
    case malformedRow(line: Int, content: String)
    case missingColumn(name: String, availableColumns: [String])
    case typeMismatch(column: String, expected: String, got: String)
}

struct CSVParser {
    let separator: Character
    
    init(separator: Character = ",") {
        self.separator = separator
    }
    
    func parse(_ content: String) throws(CSVError) -> [[String: String]] {
        guard !content.isEmpty else { throw CSVError.emptyInput }
        
        var lines = content.components(separatedBy: "\n")
            .map { $0.trimmingCharacters(in: .whitespaces) }
            .filter { !$0.isEmpty }
        
        guard !lines.isEmpty else { throw CSVError.emptyInput }
        
        let headers = lines.removeFirst()
            .split(separator: separator)
            .map(String.init)
        
        return try lines.enumerated().map { lineIndex, line throws(CSVError) in
            let values = line.split(separator: separator, omittingEmptySubsequences: false)
                .map(String.init)
            
            guard values.count == headers.count else {
                throw CSVError.malformedRow(line: lineIndex + 2, content: line)
            }
            
            return Dictionary(uniqueKeysWithValues: zip(headers, values))
        }
    }
    
    // Typed conversion — generic, propagates error
    func column<T, E: Error>(
        named name: String,
        from row: [String: String],
        as type: T.Type,
        converter: (String) throws(E) -> T
    ) throws(CSVError) -> T {
        guard let raw = row[name] else {
            throw CSVError.missingColumn(name: name, availableColumns: Array(row.keys))
        }
        
        do {
            return try converter(raw)
        } catch {
            throw CSVError.typeMismatch(column: name, expected: "\(T.self)", got: raw)
        }
    }
}

// Penggunaan:
let parser = CSVParser()

do {
    let csvContent = """
    name,age,email
    Ari,28,ari@example.com
    Budi,30,budi@example.com
    """
    
    let rows = try parser.parse(csvContent)
    
    for row in rows {
        let name = row["name"] ?? ""
        // Kalau ada error, langsung tahu itu CSVError — tidak perlu cast
        print("User: \(name)")
    }
} catch {
    // exhaustive switch — compiler pastikan semua case ter-cover
    switch error {
    case .emptyInput:
        print("File CSV kosong")
    case .invalidEncoding:
        print("Encoding tidak valid")
    case .malformedRow(let line, let content):
        print("Baris \(line) tidak valid: \(content)")
    case .missingColumn(let name, let available):
        print("Kolom '\(name)' tidak ada. Tersedia: \(available.joined(separator: ", "))")
    case .typeMismatch(let col, let expected, let got):
        print("Kolom '\(col)': expected \(expected), got '\(got)'")
    }
}

Use Case 3: Repository Pattern dengan Domain Errors

swift
// Repository dengan typed errors yang jelas di setiap operation

enum UserRepositoryError: Error {
    case notFound(id: String)
    case duplicateEmail(email: String)
    case constraintViolation(field: String, reason: String)
    case storageFailure(underlying: String)
}

enum QueryError: Error {
    case invalidFilter(String)
    case resultTooLarge(count: Int, limit: Int)
    case timeout(after: Duration)
}

protocol UserRepository {
    func find(by id: String) async throws(UserRepositoryError) -> User
    func save(_ user: User) async throws(UserRepositoryError)
    func delete(id: String) async throws(UserRepositoryError)
    func query(_ filter: UserFilter) async throws(QueryError) -> [User]
}

// Interactor yang tahu persis error dari setiap operation
struct UserInteractor {
    let repository: any UserRepository
    
    func getUser(id: String) async -> Result<User, UserRepositoryError> {
        do {
            let user = try await repository.find(by: id)
            return .success(user)
        } catch {
            // error PASTI UserRepositoryError — tidak perlu if let / as?
            return .failure(error)
        }
    }
    
    func registerUser(_ form: RegistrationForm) async throws(UserRepositoryError) {
        let user = User(from: form)
        try await repository.save(user)  // typed error propagates
    }
}