Skip to content

feat: CDYelpFusionKit v6.0.0 — drop Alamofire, native URLSession, async/await-only API#43

Open
chrisdhaan wants to merge 66 commits into
masterfrom
feat/v6-urlsession-rewrite
Open

feat: CDYelpFusionKit v6.0.0 — drop Alamofire, native URLSession, async/await-only API#43
chrisdhaan wants to merge 66 commits into
masterfrom
feat/v6-urlsession-rewrite

Conversation

@chrisdhaan

Copy link
Copy Markdown
Owner

Summary

CDYelpFusionKit v6.0.0 is a major architectural overhaul that removes the Alamofire dependency entirely, replaces it with a native URLSession-backed actor pipeline, and drops the completion-handler API in favour of a pure async throws interface.

Target release date: June 23, 2026

Breaking Changes

  • Completion handlers removed — all 19 CDYelpAPIClient methods are now async throws only; completion: variants are gone
  • Error type changedAFError replaced by CDYelpNetworkError (.invalidURL, .networkFailure(underlying:), .httpError(statusCode:data:), .decodingFailed(underlying:))
  • Alamofire removed — no external dependencies; SPM and CocoaPods consumers must remove the Alamofire package reference
  • Deployment targets raised — iOS 15.0 · macOS 12.0 · tvOS 15.0 · watchOS 8.0 (visionOS 1.0 unchanged)

What Didn't Change

All public configuration types, protocols, and response models are identical to v5.x — only the transport layer and API surface changed:

  • CDYelpCacheConfiguration, CDYelpRetryConfiguration, CDYelpDecoderConfiguration
  • CDYelpEventMonitor, CDYelpRequestAdapter protocols
  • CDYelpMockURLProtocol, CDYelpMockClientFactory testing utilities
  • All response model types and enum types

New Internal Types

  • CDYelpNetworkError — native Swift error enum
  • CDYelpURLSession — Swift actor owning the full networking pipeline (adapter chain → cache → URLSession → retry → decode)
  • CDYelpRouter (renamed from CDYelpNativeRouter) — internal URL routing enum; asURLRequest(apiKey:) now takes an explicit API key parameter

Files Changed

Area Change
Source/CDYelpNetworkError.swift New — 4-case native error enum
Source/Internal/CDYelpURLSession.swift New — URLSession actor pipeline
Source/Internal/CDYelpRouter.swift New (renamed from CDYelpNativeRouter.swift)
Source/CDYelpAPIClient.swift Rewritten — pure async throws, URLSession-backed
Source/Parameters+CDYelpFusionKit.swift Alamofire encoding removed
Source/CDYelpFusionKit.swift Version → "6.0.0"
Source/CDYelpRetryConfiguration.swift Stale Alamofire comment updated
CDYelpFusionKit.podspec Version 5.1.0 → 6.0.0; Alamofire dependency removed
Package.swift Alamofire dependency removed; targets raised
CDYelpFusionKit.xcodeproj New files added, dead files removed, deployment targets updated, Alamofire SPM package removed
Example/Source/ViewController.swift Cases 2–11 converted to async/await
Tests/ Router tests updated; cache tests fixed (apiKey: argument added)
Documentation/Usage.md Full rewrite — async/await only, CDYelpNetworkError
Documentation/ARCHITECTURE.md Full rewrite — URLSession-native architecture
Documentation/CDYelpFusionKit 6.0 Migration Guide.md New migration guide
CHANGELOG.md v6.0.0 entry added
README.md Platform minimums, version refs, features updated

Test Plan

  • swift build — zero warnings, zero errors
  • swift test — all 193 tests pass
  • swiftlint lint --strict — zero violations
  • swiftformat Source Tests --lint — zero violations
  • bundle exec pod lib lint — passes
  • CI green on all matrix jobs
  • After merge: tag 6.0.0, pod trunk push CDYelpFusionKit.podspec, bash scripts/generate-docs.sh + push docs/

🤖 Generated with Claude Code

chrisdhaan and others added 30 commits June 16, 2026 22:20
…ependency declarations

Bumps minimums to iOS 15 / macOS 12 / tvOS 15 / watchOS 8 (required for
URLSession async/await) in both Package.swift and the podspec. Removes the
Alamofire package and target dependencies in preparation for the native
URLSession rewrite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces the native Swift error type that replaces AFError in v6.
Four cases cover the full request lifecycle: invalidRequest, httpError,
decodingFailed, and networkFailure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Internal actor that owns the URLSession and implements the full
request/response cycle: adapter chain → cache read → network →
retry with backoff → cache write → decode. Replaces the Alamofire
Session and the cachedRequest helper in CDYelpAPIClient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces CDYelpRouter.swift with a pure URLComponents/URLQueryItem
implementation. Covers all 19 endpoint cases (17 GET + 2 POST), injects
the Bearer Authorization header directly, and routes aiChat to its
hardcoded URL outside the /v3/ base path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drops the Alamofire import and replaces it with a native
typealias Parameters = [String: Any], keeping all existing
function signatures source-compatible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes Alamofire entirely from the API client. Replaces the Alamofire
Session, cachedRequest helper, makeSession factory, and all
completion-handler overloads with direct async/await methods that
delegate to the CDYelpURLSession actor via CDYelpNativeRouter.
The testing init signature is unchanged so CDYelpMockClientFactory
requires no modifications.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes CDYelpRouter.swift, CDYelpAlamofireEventMonitor.swift, and
CDYelpAlamofireRequestAdapter.swift. These are fully replaced by
CDYelpNativeRouter, CDYelpURLSession's inline monitor calls, and
the adapter loop in CDYelpURLSession.perform(_:).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CDYelpMockClientFactory calls the preserved testing init signature and
CDYelpMockURLProtocol intercepts the native URLSession unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces all CDYelpRouter.X.asURLRequest() call sites with
try CDYelpNativeRouter.X.asURLRequest(apiKey: "test-key").
Updates the three typed-array bulk tests from [CDYelpRouter]
to [CDYelpNativeRouter]. No assertions changed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All existing simulator destinations (lowest: iOS 18.6, tvOS 18.5,
watchOS 11.5) already exceed the new minimums imposed by the
URLSession async/await deployment targets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Squash of all v6.0.0 review, fix, and polish commits:

Core changes (Haiku's original work):
- CDYelpNetworkError: native 4-case error enum replacing AFError
- CDYelpURLSession: internal Swift actor owning URLSession pipeline
  (adapter chain → cache → network → retry → decode)
- CDYelpAPIClient: all 19 endpoints rewritten as pure async throws;
  completion-handler overloads removed entirely
- Parameters+CDYelpFusionKit: Alamofire encoding removed
- Alamofire source files deleted (CDYelpAlamofireEventMonitor,
  CDYelpAlamofireRequestAdapter)
- Package.swift / podspec: Alamofire dependency removed; deployment
  targets raised to iOS 15 / macOS 12 / tvOS 15 / watchOS 8

Review fixes applied in this session:
- CDYelpNativeRouter renamed to CDYelpRouter for naming continuity
- Pattern binding variables renamed p → params (fixes 17 SwiftLint
  identifier_name violations)
- hoistPatternLet violations fixed in CDYelpRouter.queryParameters
- forEach → for-in loops in CDYelpURLSession (SwiftFormat preferForLoop)
- wrapLoopBodies: all inline for bodies expanded to multi-line
- CDYelpResponseCacheTests: added missing apiKey: argument on
  asURLRequest() calls (4 call sites)
- isAuthenticated() re-added to CDYelpAPIClient (used by 2 test files)
- CDYelpRetryConfiguration: stale Alamofire comment updated
- Xcode project: dead file refs removed, new files added with correct
  Internal/ path prefix, deployment targets updated, Alamofire SPM
  package removed, MARKETING_VERSION bumped to 6.0.0

Version bump:
- CDYelpFusionKit.podspec: 5.1.0 → 6.0.0
- Source/CDYelpFusionKit.swift: version string → "6.0.0"

Example app:
- ViewController.swift cases 2–11 converted from completion handlers
  to Task { try await } pattern

Documentation:
- CHANGELOG.md: [6.0.0] entry added
- README.md: platform minimums, SPM/CocoaPods versions, features list
- Documentation/Usage.md: full rewrite — async/await only, CDYelpNetworkError
- Documentation/ARCHITECTURE.md: full rewrite — URLSession-native architecture
- Documentation/CDYelpFusionKit 6.0 Migration Guide.md: new file
- Documentation/API_SCHEMA.md: hypothetical code snippets updated
- Source/CDYelpFusionKit.docc/CDYelpFusionKit.md: overview updated
- CLAUDE.md: description, platform targets, CDYelpRouter row updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…g selection handlers

Extract didSelectRowAt into handleAPIEndpointSelection, handleDeepLinkSelection, and
handleWebLinkSelection, and pull cellForRowAt label logic into a cellTitle(for:) helper
to eliminate all swiftlint:disable comments except the unavoidable function_body_length
on the 12-case API handler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… review rounds

- CDYelpNetworkError: @unchecked Sendable (Error is not Sendable; Swift 6 compat)
- fetchAIChat: replace force-unwrap with if-let for latitude/longitude userContext
- Parameters+CDYelpFusionKit: remove IUO ! from phoneParameters, matchesParameters, autocompleteParameters; fix corresponding if-let body bindings
- CDYelpURLSession: cap backoffNanoseconds at 300 s to prevent UInt64 overflow; compute cacheKey only for GET requests
- CDYelpResponseCache: CDYelpCacheEntry struct → final class, NSCache now typed directly (no AnyObject boxing)
- CDYelpRouter: document String(describing:) parameter-value assumption
- CDYelpMockClientFactory: add retryConfiguration parameter
- Tests: add postEndpointResponseIsNotCached, http500TriggersRetryUpToLimit, non500StatusCodeDoesNotTriggerRetry (198 tests, all passing)
- Docs: .invalidURL → .invalidRequest(underlying:) across CHANGELOG, ARCHITECTURE, Migration Guide, Usage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cast event start/end dates to Int to avoid float strings in URLs (e.g. "1750000000.0")
- Clamp backoff delay to >= 0 before UInt64 cast to prevent crash with negative initialDelay
- Pass data.count as NSCache cost so totalCostLimit byte-based eviction is active
- Add precondition to fetchAIChat requiring latitude and longitude together
- Fix fetchReviews limit precondition to reject 0 (was >= 0, now > 0)
- Notify event monitors when a CDYelpRequestAdapter throws
- Guard nil HTTPURLResponse after network call; throw networkFailure instead of httpError(statusCode: 0)
- Add Accept: application/json header to aiChat and jobs POST routes
- Build aiChat URL from CDYelpURL.rootBase + path (eliminates hardcoded duplicate)
- Add rootBase constant to CDYelpURL for non-v3 endpoints
- Replace three manual append-loop-plus-strip patterns with .joined(separator:)
- Remove implicitly-unwrapped optional from matchThresholdType parameter
- Fix @unchecked Sendable comment on CDYelpNetworkError to reflect adapter-error risk
- Add defer stub cleanup to two tests that lacked it (cachedResponse, postEndpoint)
- Document async-delivery limitation on cancelAllTasks()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CDYelpURLSession: wrap cache-hit decode errors in CDYelpNetworkError.decodingFailed
  so callers see a consistent error type regardless of cache state
- CDYelpURLSession: guard httpResponse before notifying monitors to prevent false
  success signals on non-HTTP responses
- CDYelpURLSession: extract private perform(_:decoder:attempt:) so the recursion
  detail (attempt counter) is not exposed in the module-internal interface
- CDYelpRouter: convert if-case-let POST dispatch to exhaustive switch so the
  compiler catches missing cases when new POST endpoints are added
- CDYelpRouter: wrap JSONEncoder.encode calls in do/catch to keep EncodingError
  inside the CDYelpNetworkError.invalidRequest contract
- CDYelpRouter: force-encode + as %2B in query strings — URLComponents allows +
  per RFC 3986 but servers decode it as a space, breaking phone number parameters
- CDYelpNetworkError: correct @unchecked Sendable doc comment — Error has been
  Sendable since SE-0337; the real risk is adapter-supplied concrete error types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove spurious `- returns: Void` from convenience init DocC
- Add `- Throws: CDYelpNetworkError` annotation to all 19 async methods
- Fix cancelAllPendingAPIRequests() doc to note asynchronous delivery
- Fix isAuthenticated() doc to clarify it always returns true post-init
- Fix fetchFeaturedEvent() parameter docs — remove contradictory
  "Required / Can be Optional" phrasing
- Fix CDYelpNetworkError @unchecked Sendable comment — Error has been
  Sendable since SE-0337; clarify actual risk surface across all three
  underlying-error cases, not just .invalidRequest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ME, and DocC

- Usage.md: add missing await to clearCache() example
- Usage.md: correct cache behaviour description — bytes are stored before
  decode, not after; remove false claim about poisoned-entry prevention
- Usage.md: fix enum case .reservation → .restaurantReservation
- ARCHITECTURE.md: replace non-existent sortedQueryItems() pseudocode with
  actual URLQueryItem map; note that cache keys use sorted canonical form
- ARCHITECTURE.md: fix decoder configuration default (.default, not CDYelpDecoderConfiguration())
- ARCHITECTURE.md: remove claim about "data tasks list" — no such list
  exists; cancellation is delegated to URLSession
- README.md: correct Sendable claim — CDYelpAPIClient is plain Sendable,
  not @unchecked Sendable; @unchecked is on CDYelpNetworkError
- README.md: update test count 193 → 198
- CDYelpFusionKit.docc: expand overview to list all 15 endpoint groups

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- malformedResponseBodyThrowsDecodingFailed: verify 200 with type-mismatch
  JSON throws CDYelpNetworkError.decodingFailed (not raw DecodingError)
- cachedMalformedResponseThrowsDecodingFailed: verify cache-hit path also
  wraps decode errors in .decodingFailed (tests the fix in CDYelpURLSession)
- searchBusinessesReturns404AsSpecificHttpError: assert thrown error is
  CDYelpNetworkError.httpError(404, _) not just Error.self
- http500FinalThrowIsSpecificHttpError: assert final throw after retries is
  CDYelpNetworkError.httpError(500, _)
- adapterFailureThrowsInvalidRequestError: verify adapter throw propagates
  as CDYelpNetworkError.invalidRequest end-to-end
- eventMonitorReceivesCompletedCallbackOnSuccess: verify requestDidComplete
  fires with a non-nil response and nil error on success
- fetchReviewsDecodesDateFields: exercise date-aware DateFormatter.reviews
  decoder used by fetchReviews (previously no integration coverage)
- fetchJobsPostResponseIsNotCached: verify POST jobs endpoint bypasses cache
- phoneNumberPlusSignIsPercentEncoded: verify + in E.164 number encodes as
  %2B not literal + (tests the CDYelpRouter fix)

Refactor: extract CDYelpURLSession.decodeWrapping(_:decoder:) to eliminate
duplicate do/catch pattern and bring private perform under the 70-line limit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cache write moved after decode — malformed 2xx no longer poisons cache
- Bool query params encode as "1"/"0" (was "true"/"false")
- fetchReviews limit precondition restored to >= 0 (limit: 0 was valid in v5)
- clearCache() made synchronous via nonisolated (NSCache is thread-safe)
- requestDidComplete(error: nil) no longer fires for 4xx/5xx responses
- Auth header re-injected after adapter chain — adapters cannot strip Bearer token
- cancelAllTasks() now cancels Swift Tasks sleeping in retry backoff via trackedSleep
- Adapters run only on attempt 0; retry passes already-adapted request
- requestDidComplete fires once per logical call (not per intermediate retry)
- requestDidStart fires once per logical call (not per intermediate retry)
- Transport failures pass CDYelpNetworkError to monitors (not raw URLError)
- Cache hits fire requestDidStart + requestDidComplete on monitors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CDYelpRouter: bool params now encode as true/false (was 1/0), fixing
  open_now, is_free, and get_covers_range silently sending wrong values
- CDYelpURLSession: requestDidComplete fires after decodeWrapping on the
  success path so monitors never see a success signal before JSON is valid
- CDYelpURLSession: cache decode failure now evicts the stale entry so
  the next call falls through to the network instead of failing for the
  full TTL
- CDYelpURLSession: adapter failure path fires requestDidStart before
  requestDidComplete, keeping the monitor lifecycle always paired
- CDYelpURLSession: both retry sleep paths wrap trackedSleep in try/catch
  so requestDidComplete fires on cancellation
- CDYelpURLSession: auth header only restored when an adapter removes it
  entirely; token rotation adapters keep their replacement value
- CDYelpURLSession: trackedSleep cancels the inner Task in defer to
  prevent orphaned sleeps after outer-task cancellation
- CDYelpResponseCache: expired entries return nil without removeObject,
  eliminating a TOCTOU race that could evict a freshly-written entry
- CDYelpURLSession: cache key computation guarded by cache != nil to
  avoid URL parse + sort on every GET when caching is disabled
- CDYelpMockClientFactory: expose decoderConfiguration so tests can
  exercise non-default decoder strategies
- .swiftlint.yml: raise function_body_length threshold to 130/140 to
  accommodate CDYelpURLSession.perform's inherent lifecycle complexity
  rather than suppressing it with an inline disable comment
- Parameters+CDYelpFusionKit: remove now-superfluous function_body_length
  disable (only function_parameter_count remains)
- Tests: 5 new tests covering auth restoration, token rotation, monitor
  decode-failure signalling, monitor lifecycle on adapter failure, and
  cache targeted eviction (218 tests total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CDYelpAPIClient: package init → public init; package access requires a
  -package-name flag that Xcode project builds and CocoaPods lint don't set,
  so the iOS/macOS/tvOS/watchOS/visionOS/Catalyst and CocoaPods CI jobs were
  failing to compile entirely
- CDYelpAPIClient: remove isAuthenticated() — init's precondition already
  makes an empty apiKey unreachable, so the method could never return false
- CDYelpAPIClient/CDYelpURLSession: cancelAllPendingAPIRequests() and
  cancelAllTasks() are now async and await actual cancellation via
  URLSession.tasks instead of returning before cancellation takes effect
- CDYelpRouter: percent-encode path-embedded identifiers (business id, event
  id, category alias, etc.) so a "?"/"#" in an id can't be reinterpreted as a
  query/fragment delimiter when the concatenated URL string is re-parsed
- CDYelpRouter: encode boolean query parameters numerically ("1"/"0") instead
  of "true"/"false", restoring the wire format this library has always sent
  (previously via Alamofire's default .numeric boolEncoding)
- CDYelpURLSession: shouldRetry now excludes non-idempotent HTTP methods
  (POST), matching Alamofire's prior RetryPolicy default — aiChat/jobs
  requests were being silently auto-retried, risking duplicate server-side
  processing
- CDYelpURLSession: wrap retry-backoff sleep cancellation in
  CDYelpNetworkError instead of leaking a raw CancellationError, honoring the
  documented "Throws: CDYelpNetworkError" contract
- CDYelpURLSession: gate cache lookup to the first attempt only — a retry
  means the caller's retry policy asked for a fresh network attempt, not an
  opportunistic cache hit
- CDYelpURLSession: extract shared notifyStart/notifyComplete/notifyRetry and
  retryOrThrow helpers, removing ~15 duplicated monitor-notification loops
  and two duplicated retry-and-backoff blocks
- Example/ViewController: await cancelAllPendingAPIRequests() before starting
  a new request so the async cancellation can't race with and cancel the
  request it was meant to make way for
- Docs: CHANGELOG and Migration Guide updated for the isAuthenticated()
  removal and cancelAllPendingAPIRequests() async signature change

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- CDYelpRouterTests: id containing "?" or "#" is percent-encoded rather than
  reinterpreted as a query/fragment delimiter; boolean query parameters
  encode as "1"/"0"
- CDYelpAPIClientTests: a POST endpoint (jobs) does not retry on a retryable
  status code

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mums

CDYelpFusionKit.xcodeproj's project-level defaults (IPHONEOS 15.6, MACOSX
12.4, TVOS 15.6, WATCHOS 8.7) were higher than the iOS 15.0 / macOS 12.0 /
tvOS 15.0 / watchOS 8.0 minimums documented in Package.swift, the podspec,
and CLAUDE.md, and weren't overridden by any of the platform-specific
targets (only CDYelpFusionKitTesting explicitly set the correct values).
Every Xcode-built scheme was silently requiring a newer OS than promised.

Lower the project-level defaults to match the documented minimums, and raise
the Example app's own IPHONEOS_DEPLOYMENT_TARGET from 13.0 to 15.0 to match
the framework's actual minimum — the Example app previously failed to build
against the v6.0.0 framework because of this mismatch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CDYelpFusionKit.xcodeproj's "CDYelpFusionKit macOS" native target set
MACOSX_DEPLOYMENT_TARGET = 11.0 at the target level, overriding the
project-level default of 12.0 that commit 3cc7aac fixed. This target-level
override was missed, leaving the macOS scheme's actual minimum inconsistent
with CLAUDE.md, Package.swift, and the podspec (all documented as 12.0).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
trackedSleep wrapped Task.sleep in a detached, unstructured Task so it
could be tracked and cancelled by cancelAllTasks(). But an unstructured
Task's cancellation state isn't linked to the task that created it, so
cancelling the caller's own Task (e.g. `let t = Task { try await
client.searchBusinesses(...) }; t.cancel()`) had no effect on an in-flight
retry-backoff sleep — only the library's explicit
cancelAllPendingAPIRequests() could interrupt it, silently breaking the
standard Swift concurrency expectation that cancelling a Task cancels
everything it's awaiting.

Wrap the sleep in withTaskCancellationHandler so ambient cancellation is
forwarded into the detached task. Add regression tests verifying both
cancelAllPendingAPIRequests() and direct Task.cancel() interrupt an
in-flight retry backoff promptly rather than waiting out the full delay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
expiredEntryDoesNotEvictConcurrentlyWrittenFreshEntry never actually
created an expired cache entry, so it never exercised the code path the
TOCTOU fix (bd1a402) touched — reverting that fix locally left the test
green. Attach an NSCacheDelegate (exposing `cache` as internal instead of
private for test access) and assert directly that reading a genuinely
expired entry triggers zero evictions, which fails immediately if the old
removeObject-on-expiry behavior is reintroduced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
String(describing:) renders Double values under ~0.0001 in scientific
notation (e.g. "1e-05"), so a latitude or longitude within about 11 meters
of the equator or prime meridian was sent as an unparseable coordinate.
Format Double query parameters with %.8f instead, matching the fixed
decimal notation the Yelp Fusion API expects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
API_SCHEMA.md still listed every endpoint's Swift method with a trailing
completion: parameter, even though completion handlers were fully removed
in the v6.0.0 async/await rewrite. Drop the stale parameter from all 19
signatures, and fix a leftover stray code-fence artifact. Update README's
test count, which had drifted from 198 to the current 226 across several
rounds of added regression coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e cache

fetchEngagementMetrics, fetchServiceOfferings, fetchBusinessInsights,
fetchReviewHighlights, and fetchOpenings bypassed caching entirely in the
pre-v6 Alamofire client. The v6 URLSession pipeline caches any GET request
whenever a cache is configured, with no per-endpoint opt-out, silently
making these five cache-eligible — most notably fetchOpenings, which
returns real-time reservation availability.

Add a `cacheable` flag to CDYelpURLSession.perform (default true) and pass
false from these five endpoints to restore the original behavior.
%.8f formatting rendered Double.nan/.infinity as literal "nan"/"inf"/"-inf"
with no validation, silently sending a malformed request (e.g.
latitude=nan) to the Yelp Fusion API. Throw CDYelpNetworkError.invalidRequest
instead, since none of searchBusinesses(byTerm:), searchTransactions(byType:),
searchEvents(), or fetchFeaturedEvent() precondition-check finiteness before
reaching the router.
chrisdhaan and others added 15 commits July 4, 2026 10:15
Reflects the 8 tests added by this round's fixes (cache scope, NaN/Infinity
guard, header restoration, .networkFailure coverage, cancelled-retry
exclusion, and cache self-heal).
The non-HTTPURLResponse guard in CDYelpURLSession.perform threw directly
instead of going through retryOrThrow like every other failure branch,
silently skipping retry/backoff for that path. Header restoration after
running request adapters was also hardcoded to three specific header names;
generalize it to snapshot and restore the full original header set so a
future router-added header isn't silently dropped by a wholesale-replacing
adapter. Also documents that CDYelpEventMonitor.requestDidComplete's
response can be nil for cache-served completions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ent header

- Add CDYelpRouter.isCacheable as the single source of truth for which
  endpoints are cacheable, replacing five scattered `cacheable: false`
  literals in CDYelpAPIClient plus the implicit default for the rest — a
  future endpoint's cacheability is no longer something a caller can forget
  to opt out of.
- Restore a User-Agent header on every request; it was dropped along with
  Alamofire's HTTPHeaders.default and never replaced.
- Extract applyStandardHeaders/postRequest helpers in CDYelpRouter to
  de-duplicate the .aiChat/.jobs POST construction and the three separate
  inline Authorization/Accept/Content-Type header assignments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ange

API_SCHEMA.md and the 6.0 migration guide still described parameter
validation as "asserted" (Debug-only), left over from before parameter
validation was widened to precondition (traps in all build configurations,
including Release) across ~28 call sites in CDYelpAPIClient — an
intentional, established pattern in this codebase (see the 3.x CHANGELOG
entry doing the same for apiKey), not something being reverted here. Update
the schema doc's wording and add a migration guide section calling out the
Release-build behavior change explicitly. Also adds a CHANGELOG entry for
the fixes in the prior two commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No functional change. Adds an in-code note explaining the TOCTOU
rationale (commit bd1a402) so a future reviewer doesn't mistake the
missing removeObject call for a regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ow-list

Alamofire's HTTPHeaders.default previously sent Accept-Language (derived
from Locale.preferredLanguages) on every request; the v6 rewrite dropped
it along with Alamofire. Restore it via a native Accept-Language builder
in CDYelpRouter, mirroring Alamofire's quality-encoded format.

Also convert isCacheable from an exclusion list (default: cacheable) to
an exhaustive allow-list with no default case, so a newly added router
case fails to compile until someone decides which bucket it belongs in,
instead of silently inheriting "cacheable".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
startLoading() force-unwrapped HTTPURLResponse's failable initializer.
Stub.init accepts any Int statusCode with no validation, so a caller
(this repo's tests or a downstream consumer, since this is a public
Testing-target helper) registering an out-of-range statusCode or a
rejected header value would crash the test process instead of failing
the request. Guard it and fail with URLError(.badServerResponse).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…etry-After

httpError previously only carried statusCode and data, so the retry
engine could never see a server-provided Retry-After header and always
fell back to blind exponential backoff — mistimed waits under sustained
429 rate-limiting. Add headers: [String: String] to the case (sourced
via a new HTTPURLResponse.stringHeaderFields snapshot) and prefer a
valid Retry-After value (seconds or HTTP-date form, per RFC 9110 10.2.3)
over exponential backoff when present, still capped at the existing
300s ceiling.

Updates the two existing httpError pattern-match tests for the new
three-value tuple arity; no behavior they assert on changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Extract a shared private perform<T>(_:decoder:) helper that builds the
  request from a router case and calls urlSession.perform(...), used by
  all 19 endpoint methods instead of each repeating the same two/three
  lines verbatim.
- Extract makeDecoder(dateFormat:) to replace the four copies of
  "decoderConfiguration.makeDecoder() + .formatted date strategy" in the
  reviews/events endpoints.
- Document fetchAIChat's latitude/longitude joint precondition in its
  doc comment — pre-rewrite code silently dropped a lone coordinate
  instead of trapping, and neither the doc comment nor the migration
  guide mentioned the new stricter behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The three failure branches in perform()'s retry loop each repeated the
same attempt += 1; continue pair after calling retryOrThrow. Move the
increment into retryOrThrow itself (attempt now inout), so each call
site only needs `continue` and a future change to loop bookkeeping only
needs to be made in one place.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…attempt

adapt(_:) runs once before the first attempt; a retried request resends
the same already-adapted URLRequest. This is deliberate (see
requestAdapterRunsOncePerLogicalCallWithRetry) but wasn't called out
anywhere an adapter author would see it — add a protocol doc note and a
matching callout in the Usage.md request-signing example.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…fixes

Reflects the CDYelpNetworkError.httpError signature change (added
headers:), the corrected retry-loop description (it was still described
as recursive after an earlier refactor to an explicit loop), the new
fetchAIChat migration guide entry, and a changelog summary of every fix
applied in this pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nguage

Reformats the immediately-invoked map/joined closure into a single
expression per CI's swiftformat --lint check; no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6.0.0 hasn't shipped yet, so a running list of bugs fixed during its own
PR review isn't a meaningful "Fixed" entry — those are for issues found
in an already-released version. Condense the genuinely user-facing
outcomes (Retry-After support, restored default headers, adapter header
restoration, fetchAIChat's coordinate precondition) into Added/Updated
instead, and drop the purely internal implementation-detail bullets
(isCacheable's list shape, aiChat/jobs dedup, etc.).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…links

CDYelpMockClientFactory and CDYelpMockURLProtocol live in the separate
CDYelpFusionKitTesting target. CI's DocC Build job only documents the
CDYelpFusionKit target and fails on any warning, so the double-backtick
symbol links to those two types in CDYelpAPIClient's init doc comment
("doesn't exist at ...") failed the build. Switch to plain inline code
spans, which render the same but don't attempt symbol resolution.

Verified locally: swift package generate-documentation --target
CDYelpFusionKit now produces zero warnings (previously 2).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@chrisdhaan

Copy link
Copy Markdown
Owner Author

Code review

Found 1 issue:

  1. The "CDYelpFusionKit iOS" native target still declares MACOSX_DEPLOYMENT_TARGET = 11.0 in both its Debug and Release configs, inconsistent with the documented macOS 12.0 minimum (CLAUDE.md's Platform Targets table, Package.swift, and CDYelpFusionKit.podspec). This isn't inert boilerplate for this target: CI's Catalyst job builds exactly this scheme with -destination "platform=macOS" (both Debug and Release), so this value governs a real build surface.

Notably, commit 928cbc7 already fixed this identical issue on the sibling "CDYelpFusionKit macOS" target ("fix: correct macOS target deployment target left at 11.0 by prior fix") — the iOS target's own override was missed in that pass.

Debug config:

"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;

Release config:

"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;

CI Catalyst job building this scheme against platform=macOS:

- name: Catalyst - Debug
run: |
set -o pipefail
env NSUnbufferedIO=YES xcodebuild -project "CDYelpFusionKit.xcodeproj" \
-scheme "CDYelpFusionKit iOS" \
-destination "platform=macOS" \
-configuration Debug clean build 2>&1 | xcbeautify --renderer github-actions
- name: Catalyst - Release
run: |
set -o pipefail
env NSUnbufferedIO=YES xcodebuild -project "CDYelpFusionKit.xcodeproj" \
-scheme "CDYelpFusionKit iOS" \
-destination "platform=macOS" \

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

chrisdhaan and others added 14 commits July 14, 2026 22:19
The "CDYelpFusionKit iOS" target's Debug/Release configs still had
MACOSX_DEPLOYMENT_TARGET = 11.0, inconsistent with the documented
macOS 12.0 minimum (CLAUDE.md, Package.swift, podspec). This target
is built for Catalyst in CI via `-destination "platform=macOS"`, so
the stale value was a real build-surface mismatch, not inert
boilerplate. Commit 928cbc7 fixed the identical issue on the sibling
"CDYelpFusionKit macOS" target but missed this one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace stale completion-handler example and incorrect iOS 13+ claim with correct async/await-only example and accurate platform minimums (iOS 15.0+, macOS 12.0+, tvOS 15.0+, watchOS 8.0+, visionOS 1.0+).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CDYelpRouter.asURLRequest(apiKey:) can throw .invalidRequest before
CDYelpURLSession.perform is ever invoked (e.g. a NaN/Infinity query
parameter), so those failures previously never reached a registered
CDYelpEventMonitor. CDYelpURLSession.perform now takes a buildRequest
closure instead of a pre-built URLRequest, so a construction failure
gets the same paired requestDidStart/requestDidComplete treatment as
an adapter-thrown .invalidRequest, using a fixed placeholder URL since
no real request exists yet in that failure case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…utocomplete endpoints

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dpoints

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ints

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s/reviewHighlights endpoints

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fetchEventReturnsDecodedResponse used a fixture with no date fields, so a
regression in DateFormatter.events's format string would go undetected.
Add a time_start field and assert the decoded Date matches
DateFormatter.events.date(from:) directly, so the test can't pass no
matter what the formatter does.

Also add MARK sectioning for the four thematic test groups in
CDYelpAPIClientTests and document why @suite(.serialized) is required
(overlapping substring stub keys with no deterministic lookup priority).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ntracked

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolves GHSA advisories for AtomicReference#update livelock on NaN
(high), and two ReadWriteLock/ReentrantReadWriteLock correctness issues
(low), all fixed upstream in concurrent-ruby 1.3.7.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant