Repository navigation
Refactor networking, JSON:API parsing, keychain reads and sign-out - #97
Merged
Merged
Conversation
AccountPasswordRepository.update logged a failure as Failed_destroy. LoginRepository.login and .logout, and SignUpRepository.signUp and .destroy, logged theirs as Failed_fetch. They now use update, login, logout, create and destroy. failedUpdateIsLoggedAsAnUpdateFailure reads the log back through OSLogStore, the same way failedLogoutIsLoggedAsALogoutFailure does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
…hService URLRequest(url:queryItems:for:) builds the request and URLSession.apiResponse(to:sending:announcingRejectionOf:) sends it and maps a non-2xx response to requestFailed. NativeAppTemplateAPI.sourceHeader replaces the second "ios" literal. The differences between the two services are now explicit at the call sites: the path prefix, query items (none for RootPathService), session headers only when signed in, and no expired-session notice from devise_token_auth's root routes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
No request reads them: every request adapts doc.data itself, and DataCacheUpdate.loadFrom had no production caller. JSONAPIDocument and JSONAPIResource become structs with only what the requests and adapters read. JSONAPIRelationship, DataCacheUpdate, the EntityAdapter protocol, EntityIdentity and EntityRelationship are removed, and the adapters become caseless enums with process(resource:). Tests that only pinned the removed internals go with them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
A keychain read error is now secCallFailed instead of being turned into notFound by try?, as store and remove already do. The badData case for an archive of another class keeps its error, archiveFailure(badData), now outside the do/catch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
shopkeeper's didSet already resets userState and permissionState, so one defer replaces the duplicated lines in both branches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 5 (refactoring). This mirrors the paid app's nativeapptemplate/NativeAppTemplate-iOS#117 where the same code exists in Free. It builds on the characterization tests from #96. One bug fix comes first, as a separate commit. Apart from that fix, behavior doesn't change.
0. Bug fix (separate commit): failures were logged under the wrong action
What was wrong. Five repository methods logged their failure with the wrong
Failureaction:AccountPasswordRepository.update: loggeddestroy, nowupdate.SignUpRepository.signUp: loggedfetch, nowcreate.SignUpRepository.destroy: loggedfetch, nowdestroy.LoginRepository.login: loggedfetch, nowlogin.LoginRepository.logout: loggedfetch, nowlogout.These are the same mislabels the paid app fixed in #118. The two
send…Instructionmethods still logfetch, as in paid.Reproduction test:
AccountPasswordRepositoryTest.failedUpdateIsLoggedAsAnUpdateFailure.OSLogStore(scope: .currentProcessIdentifier), the same way the existingSessionControllerTest.failedLogoutIsLoggedAsALogoutFailuredoes.Failed_update, and there was aFailed_destroy. After the fix it passes.Why the other four have no test:
loginandsignUpbuild their own client with the fixed.prodenvironment. A test can't point them atTestHTTPServerwithout a new seam.logoutanddestroyalways delete the app's real saved session (LoggedInShopkeeperKeychainStore). The suite deliberately leaves that item alone (seeKeychainStoreTest), and deleting it on the device would sign the app out.1.
ServiceandRootPathServiceshare one implementationURLRequest(url:queryItems:for:)sets the method, the JSON Content-Type and the body.URLSession.apiResponse(to:sending:announcingRejectionOf:)sends the request. A non-2xx response becomesrequestFailed(status, error_message). When devise_token_auth answers with its 401, the rejected token is announced.NativeAppTemplateAPI.sourceHeaderreplaces the second"ios"literal.Servicesends tobaseURL/{accountId}/api/v1/{path}with the request's query items.RootPathServicesends tobaseURL/{path}withqueryItems: [].Servicealways sends them.RootPathServicesends them only when signed in, and otherwise sends justsource.Servicepasses itsauthToken.RootPathServicepassesnil, so it never announces an expired session.RootPathServiceused ado/catchandServiceusedtry?. Either way, a body that isn't JSON gives no message.ServiceTest,RootPathServicesTest,SessionsServiceTest,SignUpsServiceTestand the header tests are unchanged. That includes the test that checks for onesourceheader, notios,ios.2. JSON:API layer: remove what no caller reads
JSONAPIDocumentis now a struct withmeta,dataandincluded.JSONAPIResourceis now a struct withid,typeandattributes.links, the resourcemeta, the relationships and the weakparentare removed.process(resource:). SwiftFormat'senumNamespacesasks for this once the protocol conformance is gone.JSONAPIRelationship.swift.DataCacheUpdate.swift. In Free,DataCacheUpdate.loadFromhas no production caller: every request adaptsdoc.dataitself. Unlike paid, nothing still needs it.EntityAdapterprotocol (never used as a type),EntityIdentityandEntityRelationship.EntityTypestays."0"defaultidwas always overwritten by?? "", so no reader saw it.DataCacheUpdateTest.swiftis deleted. Its tests characterizedDataCacheUpdateitself, and no request calls it. Its own doc comment said the tests existed "before it is simplified or removed". The cases it covered:merged(with:).JSONAPIDocumentTest:onlyStringLinksAreKept,resourceMetaIsKept,resourcesKnowTheirDocument,toOneToManyAndNullRelationshipsandnonObjectRelationshipHasNoNameOrData. Links, resource meta, parent and relationships are gone.resourceWithoutIdOrTypeHasEmptyStringsAndNoEntityIDis renamed…NoEntityType. It andentityTypeFromTypeNamenow checkentityType/idinstead ofentityID.otherTypeNamesHaveNoEntityTypeandnonObjectJsonIsAnEmptyDocumenteach lose one line, onentityIDandlinks.data,dataorder,includedread as an array or an object, top-level meta, an empty document from non-object JSON, and every adapter and request test.3.
KeychainStore.retrieve()error handling is explicitkeychain.data(for:)is nowsecCallFailed(error). Before,try?turned it intonotFound.storeandremovealready map read errors this way.LoginRepository.currentShopkeeper, catches every error, logs it and treats the user as signed out. Only the debug log differs.Keychainto make the read fail.badDatabranch:archiveFailure(badData), now outside thedo/catch.retrieveOfAnotherClassIsAnArchiveFailureOfBadDataalready pins exactly this and is unchanged. Only its comment, which described the old control flow, is updated.4.
SessionController.logout()userState,permissionStateandshopkeeper = nil.shopkeeper's didSet already resets the first two, so onedefer { shopkeeper = nil }replaces them.shouldPopToRootView = truemoves before thedoblock. It can't throw.logoutSignsOutAndPopsToRootandfailedLogoutStillSignsOuttests from Harden tests: characterize JSON:API, sessions and keychain; replace double-only tests #96, which are unchanged.Red checks
failedUpdateIsLoggedAsAnUpdateFailurefailed on the iPhone before the fix (2 issues) and passes after it.Local checks
make lint: passes. SwiftLint found 0 violations, and SwiftFormat found 0 of 134 files to format.withKnownIssue).JSONAPIRelationship.swiftandDataCacheUpdate.swift. Xcode's re-sort noise was reverted.Device check (local API at 192.168.1.11:3000, server log)
devicectl, without uninstalling, and launched it.KeychainStore.retrieve.GET .../api/v1/shopkeeper/permissionsandGET .../shopkeeper/shops, both 200.POST /shopkeeper_auth/sign_inreturned 200. It went to the root path, with no account and no /api/v1.permissions,shops,shops/:id,shops/:id/item_tagsanditem_tags?page=2returned 200. The query item was sent.DELETE /shopkeeper_auth/sign_outreturned 200..apiSessionDidExpirewas posted with that token.Reported, not changed
RootPathServicedrops query items. No devise_token_auth request has any, so this is only latent. It's kept, and it's now visible asqueryItems: [].Keychainto test thesecCallFailedread path.LoginRepository/SignUpRepository's environment and keychain, to test their log actions.🤖 Generated with Claude Code
https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH