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
}
}