Skip to content

Commit 65bf4dc

Browse files
authored
Merge pull request #12 from FlineDev/wip/common-errors
Add ErrorKit.enhancedDescription(for:) API with common error mappings
2 parents 14f67e3 + 751e3fc commit 65bf4dc

File tree

10 files changed

+657
-13
lines changed

10 files changed

+657
-13
lines changed

.editorconfig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# EditorConfig is awesome: https://editorconfig.org
2+
root = true
3+
4+
[*]
5+
6+
indent_style = space
7+
tab_width = 6
8+
indent_size = 3
9+
10+
end_of_line = lf
11+
insert_final_newline = true
12+
13+
max_line_length = 160
14+
trim_trailing_whitespace = true

.github/workflows/main.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,6 @@ on:
44
pull_request:
55

66
jobs:
7-
test-linux:
8-
runs-on: ubuntu-latest
9-
10-
steps:
11-
- uses: actions/checkout@v4
12-
13-
- name: Run tests
14-
run: swift test
15-
167
test-macos:
178
runs-on: macos-15
189

Package.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import PackageDescription
33

44
let package = Package(
55
name: "ErrorKit",
6+
defaultLocalization: "en",
67
platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)],
78
products: [.library(name: "ErrorKit", targets: ["ErrorKit"])],
89
targets: [
9-
.target(name: "ErrorKit"),
10+
.target(
11+
name: "ErrorKit",
12+
resources: [.process("Resources/Localizable.xcstrings")]
13+
),
1014
.testTarget(name: "ErrorKitTests", dependencies: ["ErrorKit"]),
1115
]
1216
)

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,44 @@ This approach eliminates boilerplate code while keeping the error definitions co
111111
### Summary
112112

113113
> Conform your custom error types to `Throwable` instead of `Error` or `LocalizedError`. The `Throwable` protocol requires only `localizedDescription: String`, ensuring your error messages are exactly what you expect – no surprises.
114+
115+
116+
## Enhanced Error Descriptions with `enhancedDescription(for:)`
117+
118+
ErrorKit goes beyond simplifying error handling — it enhances the clarity of error messages by providing improved, localized descriptions. With the `ErrorKit.enhancedDescription(for:)` function, developers can deliver clear, user-friendly error messages tailored to their audience.
119+
120+
### How It Works
121+
122+
The `enhancedDescription(for:)` function analyzes the provided `Error` and returns an enhanced, localized message. It draws on a community-maintained collection of descriptions to ensure the messages are accurate, helpful, and continuously evolving.
123+
124+
### Supported Error Domains
125+
126+
ErrorKit supports errors from various domains such as `Foundation`, `CoreData`, `MapKit`, and more. These domains are continuously updated, providing coverage for the most common error types in Swift development.
127+
128+
### Usage Example
129+
130+
Here’s how to use `enhancedDescription(for:)` to handle errors gracefully:
131+
132+
```swift
133+
do {
134+
// Attempt a network request
135+
let url = URL(string: "https://example.com")!
136+
let _ = try Data(contentsOf: url)
137+
} catch {
138+
// Print or show the enhanced error message to a user
139+
print(ErrorKit.enhancedDescription(for: error))
140+
// Example output: "You are not connected to the Internet. Please check your connection."
141+
}
142+
```
143+
144+
### Why Use `enhancedDescription(for:)`?
145+
146+
- **Localization**: Error messages are localized to ~40 languages to provide a better user experience.
147+
- **Clarity**: Returns clear and concise error messages, avoiding cryptic system-generated descriptions.
148+
- **Community Contributions**: The descriptions are regularly improved by the developer community. If you encounter a new or unexpected error, feel free to contribute by submitting a pull request.
149+
150+
### Contribution Welcome!
151+
152+
Found a bug or missing description? We welcome your contributions! Submit a pull request (PR), and we’ll gladly review and merge it to enhance the library further.
153+
154+
> **Note:** The enhanced error descriptions are constantly evolving, and we’re committed to making them as accurate and helpful as possible.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#if canImport(CoreData)
2+
import CoreData
3+
#endif
4+
5+
extension ErrorKit {
6+
static func enhancedCoreDataDescription(for error: Error) -> String? {
7+
#if canImport(CoreData)
8+
let nsError = error as NSError
9+
10+
if nsError.domain == NSCocoaErrorDomain {
11+
switch nsError.code {
12+
13+
case NSPersistentStoreSaveError:
14+
return String(
15+
localized: "CommonErrors.CoreData.NSPersistentStoreSaveError",
16+
defaultValue: "Failed to save the data. Please try again.",
17+
bundle: .module
18+
)
19+
case NSValidationMultipleErrorsError:
20+
return String(
21+
localized: "CommonErrors.CoreData.NSValidationMultipleErrorsError",
22+
defaultValue: "Multiple validation errors occurred while saving.",
23+
bundle: .module
24+
)
25+
case NSValidationMissingMandatoryPropertyError:
26+
return String(
27+
localized: "CommonErrors.CoreData.NSValidationMissingMandatoryPropertyError",
28+
defaultValue: "A mandatory property is missing. Please fill all required fields.",
29+
bundle: .module
30+
)
31+
case NSValidationRelationshipLacksMinimumCountError:
32+
return String(
33+
localized: "CommonErrors.CoreData.NSValidationRelationshipLacksMinimumCountError",
34+
defaultValue: "A relationship is missing required related objects.",
35+
bundle: .module
36+
)
37+
case NSPersistentStoreIncompatibleVersionHashError:
38+
return String(
39+
localized: "CommonErrors.CoreData.NSPersistentStoreIncompatibleVersionHashError",
40+
defaultValue: "The data store is incompatible with the current model version.",
41+
bundle: .module
42+
)
43+
case NSPersistentStoreOpenError:
44+
return String(
45+
localized: "CommonErrors.CoreData.NSPersistentStoreOpenError",
46+
defaultValue: "Unable to open the persistent store. Please check your storage or permissions.",
47+
bundle: .module
48+
)
49+
case NSManagedObjectValidationError:
50+
return String(
51+
localized: "CommonErrors.CoreData.NSManagedObjectValidationError",
52+
defaultValue: "An object validation error occurred.",
53+
bundle: .module
54+
)
55+
default:
56+
return nil
57+
}
58+
}
59+
#endif
60+
61+
return nil
62+
}
63+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import Foundation
2+
#if canImport(FoundationNetworking)
3+
import FoundationNetworking
4+
#endif
5+
6+
extension ErrorKit {
7+
static func enhancedFoundationDescription(for error: Error) -> String? {
8+
switch error {
9+
10+
// URLError: Networking errors
11+
case let urlError as URLError:
12+
switch urlError.code {
13+
case .notConnectedToInternet:
14+
return String(
15+
localized: "CommonErrors.URLError.notConnectedToInternet",
16+
defaultValue: "You are not connected to the Internet. Please check your connection.",
17+
bundle: .module
18+
)
19+
case .timedOut:
20+
return String(
21+
localized: "CommonErrors.URLError.timedOut",
22+
defaultValue: "The request timed out. Please try again later.",
23+
bundle: .module
24+
)
25+
case .cannotFindHost:
26+
return String(
27+
localized: "CommonErrors.URLError.cannotFindHost",
28+
defaultValue: "Unable to find the server. Please check the URL or your network.",
29+
bundle: .module
30+
)
31+
case .networkConnectionLost:
32+
return String(
33+
localized: "CommonErrors.URLError.networkConnectionLost",
34+
defaultValue: "The network connection was lost. Please try again.",
35+
bundle: .module
36+
)
37+
default:
38+
return String(
39+
localized: "CommonErrors.URLError.default",
40+
defaultValue: "A network error occurred: \(urlError.localizedDescription)",
41+
bundle: .module
42+
)
43+
}
44+
45+
// CocoaError: File-related errors
46+
case let cocoaError as CocoaError:
47+
switch cocoaError.code {
48+
case .fileNoSuchFile:
49+
return String(
50+
localized: "CommonErrors.CocoaError.fileNoSuchFile",
51+
defaultValue: "The file could not be found.",
52+
bundle: .module
53+
)
54+
case .fileReadNoPermission:
55+
return String(
56+
localized: "CommonErrors.CocoaError.fileReadNoPermission",
57+
defaultValue: "You do not have permission to read this file.",
58+
bundle: .module
59+
)
60+
case .fileWriteOutOfSpace:
61+
return String(
62+
localized: "CommonErrors.CocoaError.fileWriteOutOfSpace",
63+
defaultValue: "There is not enough disk space to complete the operation.",
64+
bundle: .module
65+
)
66+
default:
67+
return String(
68+
localized: "CommonErrors.CocoaError.default",
69+
defaultValue: "A file system error occurred: \(cocoaError.localizedDescription)",
70+
bundle: .module
71+
)
72+
}
73+
74+
// POSIXError: POSIX errors
75+
case let posixError as POSIXError:
76+
switch posixError.code {
77+
case .ENOSPC:
78+
return String(
79+
localized: "CommonErrors.POSIXError.ENOSPC",
80+
defaultValue: "There is no space left on the device.",
81+
bundle: .module
82+
)
83+
case .EACCES:
84+
return String(
85+
localized: "CommonErrors.POSIXError.EACCES",
86+
defaultValue: "Permission denied. Please check your file permissions.",
87+
bundle: .module
88+
)
89+
case .EBADF:
90+
return String(
91+
localized: "CommonErrors.POSIXError.EBADF",
92+
defaultValue: "Bad file descriptor. The file may be closed or invalid.",
93+
bundle: .module
94+
)
95+
default:
96+
return String(
97+
localized: "CommonErrors.POSIXError.default",
98+
defaultValue: "A system error occurred: \(posixError.localizedDescription)",
99+
bundle: .module
100+
)
101+
}
102+
103+
default:
104+
return nil
105+
}
106+
}
107+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#if canImport(MapKit)
2+
import MapKit
3+
#endif
4+
5+
extension ErrorKit {
6+
static func enhancedMapKitDescription(for error: Error) -> String? {
7+
#if canImport(MapKit)
8+
if let mkError = error as? MKError {
9+
switch mkError.code {
10+
case .unknown:
11+
return String(
12+
localized: "CommonErrors.MKError.unknown",
13+
defaultValue: "An unknown error occurred in MapKit.",
14+
bundle: .module
15+
)
16+
case .serverFailure:
17+
return String(
18+
localized: "CommonErrors.MKError.serverFailure",
19+
defaultValue: "The MapKit server returned an error. Please try again later.",
20+
bundle: .module
21+
)
22+
case .loadingThrottled:
23+
return String(
24+
localized: "CommonErrors.MKError.loadingThrottled",
25+
defaultValue: "Map loading is being throttled. Please wait a moment and try again.",
26+
bundle: .module
27+
)
28+
case .placemarkNotFound:
29+
return String(
30+
localized: "CommonErrors.MKError.placemarkNotFound",
31+
defaultValue: "The requested placemark could not be found. Please check the location details.",
32+
bundle: .module
33+
)
34+
case .directionsNotFound:
35+
return String(
36+
localized: "CommonErrors.MKError.directionsNotFound",
37+
defaultValue: "No directions could be found for the specified route.",
38+
bundle: .module
39+
)
40+
default:
41+
return String(
42+
localized: "CommonErrors.MKError.default",
43+
defaultValue: "A MapKit error occurred: \(mkError.localizedDescription)",
44+
bundle: .module
45+
)
46+
}
47+
}
48+
#endif
49+
50+
return nil
51+
}
52+
}

Sources/ErrorKit/ErrorKit.swift

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,58 @@
11
import Foundation
22

3-
#warning("🚧 not yet implemented")
3+
public enum ErrorKit {
4+
/// Provides enhanced, user-friendly, localized error descriptions for a wide range of system errors.
5+
///
6+
/// This function analyzes the given `Error` and returns a clearer, more helpful message than the default system-provided description.
7+
/// All descriptions are localized, ensuring that users receive messages in their preferred language where available.
8+
///
9+
/// The list of enhanced descriptions is maintained and regularly improved by the developer community. Contributions are welcome—if you find bugs or encounter new errors, feel free to submit a pull request (PR) for review.
10+
///
11+
/// Errors from various domains, such as `Foundation`, `CoreData`, `MapKit`, and more, are supported. As the project evolves, additional domains may be included to ensure comprehensive coverage.
12+
///
13+
/// - Parameter error: The `Error` instance for which an enhanced description is needed.
14+
/// - Returns: A `String` containing an enhanced, localized, user-readable error message.
15+
///
16+
/// ## Usage Example:
17+
/// ```swift
18+
/// do {
19+
/// // Example network request
20+
/// let url = URL(string: "https://example.com")!
21+
/// let _ = try Data(contentsOf: url)
22+
/// } catch {
23+
/// print(ErrorKit.enhancedDescription(for: error))
24+
/// // Output: "You are not connected to the Internet. Please check your connection." (if applicable)
25+
/// }
26+
/// ```
27+
public static func enhancedDescription(for error: Error) -> String {
28+
// Any types conforming to `Throwable` are assumed to already have a good description
29+
if let throwable = error as? Throwable {
30+
return throwable.localizedDescription
31+
}
32+
33+
if let foundationDescription = Self.enhancedFoundationDescription(for: error) {
34+
return foundationDescription
35+
}
36+
37+
if let coreDataDescription = Self.enhancedCoreDataDescription(for: error) {
38+
return coreDataDescription
39+
}
40+
41+
if let mapKitDescription = Self.enhancedMapKitDescription(for: error) {
42+
return mapKitDescription
43+
}
44+
45+
// LocalizedError: The recommended error type to conform to in Swift by default.
46+
if let localizedError = error as? LocalizedError {
47+
return [
48+
localizedError.errorDescription,
49+
localizedError.failureReason,
50+
localizedError.recoverySuggestion,
51+
].compactMap(\.self).joined(separator: " ")
52+
}
53+
54+
// Default fallback (adds domain & code at least)
55+
let nsError = error as NSError
56+
return "[\(nsError.domain): \(nsError.code)] \(nsError.localizedDescription)"
57+
}
58+
}

0 commit comments

Comments
 (0)