Skip to content

Commit 4e41957

Browse files
dadachiclaude
andcommitted
Make EntityAdapterError and KeychainStoreError conform to CodedError
- EntityAdapterError now conforms to CodedError with codes NATIVEAPPTEMPLATE-2006..2008, so JSON:API parsing failures surface with the standard [NATIVEAPPTEMPLATE-XXXX] prefix instead of an opaque description. - KeychainStoreError now conforms to CodedError and claims a new NATIVEAPPTEMPLATE-4xxx range for persistence failures (4001..4004). - Add unit tests for both error types, update CodedError/CLAUDE.md docs, and bump version to 3.2.3 (build 13). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8c56de3 commit 4e41957

10 files changed

Lines changed: 182 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [3.2.3] - 2026-05-08
11+
12+
### Fixed
13+
- `EntityAdapterError` (JSON:API parsing) now conforms to `CodedError` so failures — including login against an invalid server — surface with `[NATIVEAPPTEMPLATE-2006..2008]` prefixes instead of opaque descriptions
14+
- `KeychainStoreError` now conforms to `CodedError` with new `[NATIVEAPPTEMPLATE-4001..4004]` codes for persistence failures
15+
1016
## [3.2.2] - 2026-05-04
1117

1218
### Changed

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ All errors use the `CodedError` protocol in `NativeAppTemplate/Common/Errors/`.
112112
| Range | Type | File |
113113
|-------|------|------|
114114
| NATIVEAPPTEMPLATE-1xxx | App/general errors | `AppError.swift` |
115-
| NATIVEAPPTEMPLATE-2xxx | API/network errors | `NativeAppTemplateAPIError.swift` |
115+
| NATIVEAPPTEMPLATE-2xxx | API/network errors | `NativeAppTemplateAPIError.swift`, `EntityAdapterError.swift` |
116+
| NATIVEAPPTEMPLATE-4xxx | Persistence/Keychain errors | `KeychainStoreError.swift` |
116117

117118
- New error types must conform to `CodedError` and be placed in `Common/Errors/`
118119
- Use `error.codedDescription` (not `error.localizedDescription`) in all error messages

NativeAppTemplate.xcodeproj/project.pbxproj

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@
149149
A2B3C4D500000002 /* CodedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D500000001 /* CodedError.swift */; };
150150
A2B3C4D500000004 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D500000003 /* AppError.swift */; };
151151
A2B3C4D500000008 /* NativeAppTemplateAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */; };
152+
A2B3C4D50000000C /* EntityAdapterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D50000000B /* EntityAdapterError.swift */; };
153+
A2B3C4D50000000E /* KeychainStoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D50000000D /* KeychainStoreError.swift */; };
152154
/* End PBXBuildFile section */
153155

154156
/* Begin PBXContainerItemProxy section */
@@ -303,6 +305,8 @@
303305
A2B3C4D500000001 /* CodedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodedError.swift; sourceTree = "<group>"; };
304306
A2B3C4D500000003 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
305307
A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAppTemplateAPIError.swift; sourceTree = "<group>"; };
308+
A2B3C4D50000000B /* EntityAdapterError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityAdapterError.swift; sourceTree = "<group>"; };
309+
A2B3C4D50000000D /* KeychainStoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStoreError.swift; sourceTree = "<group>"; };
306310
C597C0551370446BB931F19B /* CertificatePinningDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificatePinningDelegate.swift; sourceTree = "<group>"; };
307311
/* End PBXFileReference section */
308312

@@ -772,6 +776,8 @@
772776
children = (
773777
A2B3C4D500000003 /* AppError.swift */,
774778
A2B3C4D500000001 /* CodedError.swift */,
779+
A2B3C4D50000000B /* EntityAdapterError.swift */,
780+
A2B3C4D50000000D /* KeychainStoreError.swift */,
775781
A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */,
776782
);
777783
path = Errors;
@@ -953,6 +959,8 @@
953959
A2B3C4D500000002 /* CodedError.swift in Sources */,
954960
A2B3C4D500000004 /* AppError.swift in Sources */,
955961
A2B3C4D500000008 /* NativeAppTemplateAPIError.swift in Sources */,
962+
A2B3C4D50000000C /* EntityAdapterError.swift in Sources */,
963+
A2B3C4D50000000E /* KeychainStoreError.swift in Sources */,
956964
01D8AE8B2AB453C1009AFFBA /* ShopBasicSettingsView.swift in Sources */,
957965
01E0A60125BD149200298D35 /* MainButtonView.swift in Sources */,
958966
A1B2C3D401000003 /* GlassCard.swift in Sources */,
@@ -1189,7 +1197,7 @@
11891197
CODE_SIGN_ENTITLEMENTS = "";
11901198
CODE_SIGN_IDENTITY = "Apple Development";
11911199
CODE_SIGN_STYLE = Automatic;
1192-
CURRENT_PROJECT_VERSION = 12;
1200+
CURRENT_PROJECT_VERSION = 13;
11931201
DEVELOPMENT_ASSET_PATHS = "\"NativeAppTemplate/Preview Content\"";
11941202
ENABLE_PREVIEWS = YES;
11951203
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -1201,7 +1209,7 @@
12011209
"$(inherited)",
12021210
"@executable_path/Frameworks",
12031211
);
1204-
MARKETING_VERSION = 3.2.2;
1212+
MARKETING_VERSION = 3.2.3;
12051213
PRODUCT_BUNDLE_IDENTIFIER = "com.nativeapptemplate.NativeAppTemplateFree.ios${SAMPLE_CODE_DISAMBIGUATOR}";
12061214
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "com.nativeapptemplate.NativeAppTemplateFree.ios${SAMPLE_CODE_DISAMBIGUATOR}";
12071215
PRODUCT_NAME = NativeAppTemplate;
@@ -1225,7 +1233,7 @@
12251233
CODE_SIGN_ENTITLEMENTS = "";
12261234
CODE_SIGN_IDENTITY = "Apple Development";
12271235
CODE_SIGN_STYLE = Automatic;
1228-
CURRENT_PROJECT_VERSION = 12;
1236+
CURRENT_PROJECT_VERSION = 13;
12291237
DEVELOPMENT_ASSET_PATHS = "\"NativeAppTemplate/Preview Content\"";
12301238
ENABLE_PREVIEWS = YES;
12311239
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -1237,7 +1245,7 @@
12371245
"$(inherited)",
12381246
"@executable_path/Frameworks",
12391247
);
1240-
MARKETING_VERSION = 3.2.2;
1248+
MARKETING_VERSION = 3.2.3;
12411249
PRODUCT_BUNDLE_IDENTIFIER = "com.nativeapptemplate.NativeAppTemplateFree.ios${SAMPLE_CODE_DISAMBIGUATOR}";
12421250
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "com.nativeapptemplate.NativeAppTemplateFree.ios${SAMPLE_CODE_DISAMBIGUATOR}";
12431251
PRODUCT_NAME = NativeAppTemplate;

NativeAppTemplate/Common/Errors/CodedError.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
//
55

66
// Error codes share the `NATIVEAPPTEMPLATE-XXXX` prefix across iOS and Android.
7-
// Ranges: 1xxx App errors, 2xxx API errors.
7+
// Ranges:
8+
// 1xxx App errors
9+
// 2xxx API errors (NativeAppTemplateAPIError, EntityAdapterError)
10+
// 4xxx Persistence/Keychain errors (KeychainStoreError)
811

912
import Foundation
1013

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// EntityAdapterError.swift
3+
// NativeAppTemplate
4+
//
5+
6+
import Foundation
7+
8+
/// JSON:API response parsing errors
9+
/// Error codes: NATIVEAPPTEMPLATE-2006 to NATIVEAPPTEMPLATE-2008
10+
enum EntityAdapterError: CodedError {
11+
case invalidResourceTypeForAdapter
12+
case invalidOrMissingAttributes
13+
case invalidOrMissingRelationships
14+
15+
nonisolated var errorCode: String {
16+
switch self {
17+
case .invalidResourceTypeForAdapter:
18+
"NATIVEAPPTEMPLATE-2006"
19+
case .invalidOrMissingAttributes:
20+
"NATIVEAPPTEMPLATE-2007"
21+
case .invalidOrMissingRelationships:
22+
"NATIVEAPPTEMPLATE-2008"
23+
}
24+
}
25+
26+
nonisolated var errorDescription: String? {
27+
let prefix = "EntityAdapterError::"
28+
switch self {
29+
case .invalidResourceTypeForAdapter:
30+
return "\(prefix)InvalidResourceTypeForAdapter"
31+
case .invalidOrMissingAttributes:
32+
return "\(prefix)InvalidOrMissingAttributes"
33+
case .invalidOrMissingRelationships:
34+
return "\(prefix)InvalidOrMissingRelationships"
35+
}
36+
}
37+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// KeychainStoreError.swift
3+
// NativeAppTemplate
4+
//
5+
6+
import Foundation
7+
8+
/// Keychain persistence errors
9+
/// Error codes: NATIVEAPPTEMPLATE-4001 to NATIVEAPPTEMPLATE-4004
10+
enum KeychainStoreError: CodedError {
11+
case secCallFailed(Error)
12+
case notFound
13+
case badData
14+
case archiveFailure(Error)
15+
16+
nonisolated var errorCode: String {
17+
switch self {
18+
case .secCallFailed:
19+
"NATIVEAPPTEMPLATE-4001"
20+
case .notFound:
21+
"NATIVEAPPTEMPLATE-4002"
22+
case .badData:
23+
"NATIVEAPPTEMPLATE-4003"
24+
case .archiveFailure:
25+
"NATIVEAPPTEMPLATE-4004"
26+
}
27+
}
28+
29+
nonisolated var errorDescription: String? {
30+
switch self {
31+
case let .secCallFailed(error):
32+
"KeychainStoreError::SecCallFailed[Error: \(error.localizedDescription)]"
33+
case .notFound:
34+
"KeychainStoreError::NotFound"
35+
case .badData:
36+
"KeychainStoreError::BadData"
37+
case let .archiveFailure(error):
38+
"KeychainStoreError::ArchiveFailure[Error: \(error.localizedDescription)]"
39+
}
40+
}
41+
}

NativeAppTemplate/Networking/Adapters/EntityAdapter.swift

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,3 @@ struct EntityRelationship {
4747
let from: EntityIdentity
4848
let to: EntityIdentity
4949
}
50-
51-
enum EntityAdapterError: Error {
52-
case invalidResourceTypeForAdapter
53-
case invalidOrMissingAttributes
54-
case invalidOrMissingRelationships
55-
}
56-
57-
extension EntityAdapterError: LocalizedError {
58-
var errorDescription: String? {
59-
let prefix = "EntityAdapterError::"
60-
switch self {
61-
case .invalidResourceTypeForAdapter:
62-
return "\(prefix)InvalidResourceTypeForAdapter"
63-
case .invalidOrMissingAttributes:
64-
return "\(prefix)InvalidOrMissingAttributes"
65-
case .invalidOrMissingRelationships:
66-
return "\(prefix)InvalidOrMissingRelationships"
67-
}
68-
}
69-
}

NativeAppTemplate/Persistence/KeychainStore/KeychainStore.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@ import Foundation
77
import KeychainAccess
88
import os
99

10-
enum KeychainStoreError: Error {
11-
case secCallFailed(Error)
12-
case notFound
13-
case badData
14-
case archiveFailure(Error)
15-
}
16-
1710
protocol KeychainStore {
1811
associatedtype DataType: NSObject, NSCoding
1912

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// EntityAdapterErrorTest.swift
3+
// NativeAppTemplate
4+
//
5+
6+
@testable import NativeAppTemplate
7+
import Testing
8+
9+
@Suite
10+
struct EntityAdapterErrorTest {
11+
@Test
12+
func errorCodes() {
13+
#expect(EntityAdapterError.invalidResourceTypeForAdapter.errorCode == "NATIVEAPPTEMPLATE-2006")
14+
#expect(EntityAdapterError.invalidOrMissingAttributes.errorCode == "NATIVEAPPTEMPLATE-2007")
15+
#expect(EntityAdapterError.invalidOrMissingRelationships.errorCode == "NATIVEAPPTEMPLATE-2008")
16+
}
17+
18+
@Test
19+
func formattedDescriptions() {
20+
#expect(
21+
EntityAdapterError.invalidResourceTypeForAdapter.formattedDescription
22+
== "[NATIVEAPPTEMPLATE-2006] EntityAdapterError::InvalidResourceTypeForAdapter"
23+
)
24+
#expect(
25+
EntityAdapterError.invalidOrMissingAttributes.formattedDescription
26+
== "[NATIVEAPPTEMPLATE-2007] EntityAdapterError::InvalidOrMissingAttributes"
27+
)
28+
#expect(
29+
EntityAdapterError.invalidOrMissingRelationships.formattedDescription
30+
== "[NATIVEAPPTEMPLATE-2008] EntityAdapterError::InvalidOrMissingRelationships"
31+
)
32+
}
33+
34+
@Test
35+
func codedDescriptionPrependsCode() {
36+
let error: Error = EntityAdapterError.invalidOrMissingAttributes
37+
38+
#expect(error.codedDescription == "[NATIVEAPPTEMPLATE-2007] EntityAdapterError::InvalidOrMissingAttributes")
39+
}
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// KeychainStoreErrorTest.swift
3+
// NativeAppTemplate
4+
//
5+
6+
@testable import NativeAppTemplate
7+
import Foundation
8+
import Testing
9+
10+
@Suite
11+
struct KeychainStoreErrorTest {
12+
@Test
13+
func errorCodes() {
14+
let underlying = NSError(domain: "test", code: 1)
15+
16+
#expect(KeychainStoreError.secCallFailed(underlying).errorCode == "NATIVEAPPTEMPLATE-4001")
17+
#expect(KeychainStoreError.notFound.errorCode == "NATIVEAPPTEMPLATE-4002")
18+
#expect(KeychainStoreError.badData.errorCode == "NATIVEAPPTEMPLATE-4003")
19+
#expect(KeychainStoreError.archiveFailure(underlying).errorCode == "NATIVEAPPTEMPLATE-4004")
20+
}
21+
22+
@Test
23+
func formattedDescriptions() {
24+
#expect(
25+
KeychainStoreError.notFound.formattedDescription
26+
== "[NATIVEAPPTEMPLATE-4002] KeychainStoreError::NotFound"
27+
)
28+
#expect(
29+
KeychainStoreError.badData.formattedDescription
30+
== "[NATIVEAPPTEMPLATE-4003] KeychainStoreError::BadData"
31+
)
32+
}
33+
34+
@Test
35+
func codedDescriptionPrependsCode() {
36+
let error: Error = KeychainStoreError.notFound
37+
38+
#expect(error.codedDescription == "[NATIVEAPPTEMPLATE-4002] KeychainStoreError::NotFound")
39+
}
40+
}

0 commit comments

Comments
 (0)