Technical documentation describing the internal design and structure of CDYelpFusionKit.
CDYelpFusionKit
↓
Alamofire (≥ 5.9.0)
↓
URLSession (Apple Foundation)
↓
Network stack (OS-level)
CDYelpFusionKit wraps Alamofire to provide:
- Request routing —
CDYelpRouterenum implements Alamofire'sURLRequestConvertibleprotocol - Response decoding — Uses Alamofire's
responseData()+ manualJSONDecoderto parse JSON into typed models - Authentication — Adds Bearer token to every request via
HTTPHeadersonURLSessionConfiguration - Session management — A single
Alamofire.Sessionis created eagerly inCDYelpAPIClient.initviamakeSession() - Interceptors — Alamofire's
EventMonitorandRequestInterceptorextension points are bridged to the publicCDYelpEventMonitorandCDYelpRequestAdapterprotocols - Retry — Alamofire's
RetryPolicyis wired into the sessionInterceptorwhenCDYelpRetryConfiguration.retryLimit > 0
Alamofire itself manages:
- HTTP connection pooling
- SSL/TLS certificate validation
- Request retry signalling (coordinated with
CDYelpRetryConfiguration) - URLSession delegate lifecycle
Alamofire is built on top of Apple's URLSession, which handles:
- Low-level networking
- DNS resolution and connection establishment
- HTTP/2 multiplexing (when available)
- Background session support
let client = CDYelpAPIClient(apiKey: "key")
client.searchBusinesses(byTerm: "coffee", location: "San Francisco", ...) { response in
// Handle response
}The CDYelpAPIClient instance is created once and reused for multiple requests.
The API method builds parameters into a dictionary:
var parameters: [String: Any] = [:]
if let term = term {
parameters["term"] = term
}
if let location = location {
parameters["location"] = location
}
// ... additional parametersParameters are validated and filtered by Parameters+CDYelpFusionKit.swift.
A CDYelpRouter enum case is created with the parameters:
let router = CDYelpRouter.search(parameters: parameters)Each router case contains:
- The Yelp API endpoint path
- HTTP method (GET for all current endpoints)
- Request headers (including Bearer token)
- Query parameters
The router's asURLRequest() method (conforming to Alamofire's URLRequestConvertible) constructs a URLRequest:
// Inside CDYelpRouter.asURLRequest()
var urlComponents = URLComponents(string: baseURL + path)
urlComponents?.queryItems = parameters.map { URLQueryItem(name: $0, value: String(describing: $1)) }
var request = URLRequest(url: urlComponents.url!)
request.httpMethod = "GET"
request.allHTTPHeaderFields = [
"Authorization": "Bearer \(apiKey)",
"Accept": "application/json"
]
return requestThe CDYelpAPIClient passes the request to Alamofire's Session:
manager.request(router)
.responseDecodable(of: CDYelpSearchResponse.self) { response in
switch response.result {
case .success(let value):
completion(value)
case .failure(let error):
print("error: \(error)")
completion(nil)
}
}Alamofire:
- Constructs the actual HTTP request
- Manages the network connection
- Sends the request to
api.yelp.com - Receives the HTTP response
When the response arrives, Alamofire automatically decodes the JSON body into a Swift model:
// Alamofire calls JSONDecoder internally
let decoder = JSONDecoder()
let response = try decoder.decode(CDYelpSearchResponse.self, from: data)The CDYelpSearchResponse struct (and all nested models) conform to Decodable, allowing Swift's built-in JSON decoder to parse the response.
The decoded response (or error) is passed back to the caller:
completion(response) // CDYelpSearchResponse or nil on errorAsync/await overloads use withCheckedThrowingContinuation to bridge the completion-handler-based Alamofire API:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public func searchBusinesses(...) async throws -> CDYelpSearchResponse {
try await withCheckedThrowingContinuation { continuation in
manager.request(router)
.responseDecodable(of: CDYelpSearchResponse.self) { response in
switch response.result {
case .success(let value):
continuation.resume(returning: value)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}All Yelp Fusion API requests require authentication via HTTP Bearer token:
Authorization: Bearer YOUR_API_KEY
The API key is stored as a private property and injected into every request:
public class CDYelpAPIClient {
private let apiKey: String
private let manager: Session
public init(apiKey: String) {
precondition(!apiKey.isEmpty, "An apiKey is required...")
self.apiKey = apiKey
// Create a custom Session with default headers
var headers = HTTPHeaders.default
headers["Authorization"] = "Bearer \(apiKey)"
let configuration = URLSessionConfiguration.default
self.manager = Session(
configuration: configuration,
httpHeaders: headers
)
}
}Every request includes:
Authorization: Bearer <apiKey>— Required for authenticationAccept: application/json— Tells Yelp API to return JSON- Standard
User-AgentandAccept-Encodingheaders (added by Alamofire)
- The API key is not logged or exposed in request bodies
- The API key is transmitted via HTTPS only (enforced by the router)
- Never commit API keys to source control; use environment variables or secure configuration
- Consider rotating API keys periodically via the Yelp Developer Console
CDYelpFusionKit uses nested structs to represent complex API responses:
CDYelpSearchResponse
├── businesses: [CDYelpBusiness.BusinessSearch]
├── total: Int
├── region: CDYelpRegion
└── ...
The CDYelpBusiness type has multiple variants to represent different API endpoints:
Used in /businesses/search endpoint:
public struct BusinessSearch: Decodable {
public let id: String?
public let name: String?
public let rating: Double?
public let price: String?
public let isOpen: Bool?
public let imageUrl: String?
// ... ~20 fields
}Contains essential fields for search results (ID, name, rating, price, open status).
Used in /businesses/{id} endpoint:
public struct Detailed: Decodable {
public let id: String?
public let name: String?
public let rating: Double?
// ... all BusinessSearch fields plus:
public let phone: String?
public let hours: [CDYelpHour]?
public let specialHours: [CDYelpSpecialHour]?
public let categories: [CDYelpCategory]?
public let location: CDYelpLocation?
// ... ~40 total fields
}Includes additional fields like phone, hours, and full location details.
Nesting prevents namespace pollution — without nesting, we would need:
CDYelpBusinessSearchCDYelpBusinessDetailedCDYelpBusinessMatch
Instead, we have clean namespacing:
CDYelpBusiness.BusinessSearchCDYelpBusiness.DetailedCDYelpBusiness.Match
Response objects nest their contained models:
public struct CDYelpSearchResponse: Decodable {
public let businesses: [CDYelpBusiness.BusinessSearch]?
public let total: Int?
public let region: CDYelpRegion?
}
public struct CDYelpBusinessResponse: Decodable {
public let business: CDYelpBusiness.Detailed?
}All models conform to Decodable (not Codable) because:
- The library only receives JSON from Yelp (decoding)
- The library doesn't send models as JSON (encoding is not needed)
- This avoids unnecessary
Encodableconformance boilerplate
All public enum types are defined in a single file (CDYelpEnums.swift) containing 12+ enums:
// Sort types
public enum CDYelpBusinessSortType: String {
case bestMatch = "best_match"
case rating = "rating"
case reviewCount = "review_count"
case distance = "distance"
}
// Price tiers
public enum CDYelpPriceTier: String {
case oneDollarSign = "1"
case twoDollarSigns = "2"
case threeDollarSigns = "3"
case fourDollarSigns = "4"
}
// Locales
public enum CDYelpLocale: String {
case english_unitedStates = "en_US"
case english_canada = "en_CA"
// ... 20+ locales
}
// ... and moreThe single-file approach was adopted for two reasons:
- Reduced import complexity — Users import once:
import CDYelpFusionKit - Simplified maintenance — All enum variants in one place for consistency checks
A future major version (v5.0) could split enums into logical files:
Source/
├── Enums/
│ ├── CDYelpSortEnums.swift
│ ├── CDYelpFilterEnums.swift
│ ├── CDYelpLocaleEnums.swift
│ ├── CDYelpCategoryEnums.swift
│ └── CDYelpAttributeEnums.swift
└── CDYelpEnums.swift (re-exports all for backward compatibility)
This would:
- Improve code organization
- Make git diffs cleaner for enum-specific changes
- Maintain backward compatibility via re-exports
All enums use String raw values because they directly map to Yelp API query parameters:
public enum CDYelpBusinessSortType: String {
case bestMatch = "best_match"
}
// In request building:
parameters["sort_by"] = CDYelpBusinessSortType.bestMatch.rawValue // "best_match"This avoids conversion logic — the enum value is used directly in the API request.
Source/CDColor.swift and Source/CDColor+CDYelpFusionKit.swift provide color constants for Yelp branding:
public extension UIColor {
class var yelpRed: UIColor {
return UIColor(red: 0.835, green: 0.000, blue: 0.000, alpha: 1.0)
}
}Available on iOS, tvOS, and visionOS (UIKit-based platforms). macOS uses NSColor.
Source/CDImage.swift and Source/CDImage+CDYelpFusionKit.swift provide star rating images:
public extension UIImage {
class func yelpStars(rating: CDYelpStars, size: CDYelpStarsSize) -> UIImage? {
// Returns pre-rendered star rating image (e.g., 4.5 stars as image)
}
}Stars are rendered at multiple sizes (small, regular, large, extraLarge).
Resources/Assets.xcassets contains:
- Star rating images (0-5 stars, in 0.5 increments, at 4 sizes = 44 images)
- Yelp logo variations (light, dark, monochrome)
- Brand colors (pre-configured in the asset catalog for SwiftUI/Storyboard compatibility)
The asset catalog is included in the SPM package via resources in Package.swift:
.target(
name: "CDYelpFusionKit",
resources: [.process("PrivacyInfo.xcprivacy")]
)Rendering star ratings in a UITableViewCell:
if let stars = business.rating {
let starEnum = CDYelpStars(rating: stars) // Converts 4.5 → .fourHalf
let starImage = UIImage.yelpStars(rating: starEnum, size: .regular)
self.starImageView.image = starImage
}The library uses Alamofire's AFError enum for all network-related errors:
public enum AFError: Error {
case invalidURL(url: URLConvertible)
case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
case responseValidationFailed(reason: ResponseValidationFailureReason)
case responseSerializationFailed(reason: ResponseSerializationFailureReason)
// ... more cases
}Completion-handler-based methods return the response or nil on error:
public func searchBusinesses(..., completion: @escaping (CDYelpSearchResponse?) -> Void) {
manager.request(...)
.responseDecodable(of: CDYelpSearchResponse.self) { response in
switch response.result {
case .success(let value):
completion(value)
case .failure(let error):
// Error is silently dropped; completion called with nil
completion(nil)
}
}
}This pattern maintains backward compatibility — callers check for nil rather than handling errors.
Async/await overloads throw AFError for proper error handling:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public func searchBusinesses(...) async throws -> CDYelpSearchResponse {
try await withCheckedThrowingContinuation { continuation in
manager.request(...)
.responseDecodable(of: CDYelpSearchResponse.self) { response in
switch response.result {
case .success(let value):
continuation.resume(returning: value)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}| Error | Cause | Solution |
|---|---|---|
invalidURL |
Search parameters contain invalid characters | Validate input strings |
responseSerializationFailed |
API returned non-JSON response | Check API status; verify endpoint exists |
responseValidationFailed |
HTTP status is not 2xx | Check API key validity; check rate limits |
| Network timeout | Request took too long | Check network connectivity; retry with backoff |
Alamofire's Session is thread-safe and can be safely accessed from multiple threads/queues. CDYelpFusionKit's manager property is shared across all API methods and requires no synchronization.
For Swift 6 strict concurrency:
public class CDYelpAPIClient: @unchecked Sendable {
// manager is thread-safe (Alamofire.Session is Sendable)
}The @unchecked Sendable conformance is justified because:
manager(Alamofire.Session) is thread-safeapiKeyis immutable (never modified after init)- No other mutable state is accessed from concurrent tasks
- Create a single
CDYelpAPIClientinstance and reuse it (don't create per-request) - Safe to call from any thread or async context
- No need for explicit synchronization in user code
Multiple concurrent requests don't require multiple API client instances:
async let search1 = client.searchBusinesses(...)
async let search2 = client.searchBusinesses(...)
async let search3 = client.searchBusinesses(...)
let (result1, result2, result3) = try await (search1, search2, search3)Alamofire's session automatically manages connection pooling and multiplexing.
- Response models are value types (structs) — they're cheap to copy
- Decodable deserialization is fast (Swift's built-in JSON decoder is optimized)
- Models are not cached; each request returns a fresh decoded object
- Completion handlers should use weak captures to avoid retain cycles:
client.searchBusinesses(...) { [weak self] response in
self?.updateUI(response)
}CDYelpFusionKit provides opt-in in-memory response caching via CDYelpCacheConfiguration. The cache is backed by CDYelpResponseCache (an NSCache wrapper with TTL tracking).
Cache keys are built by CDYelpCacheKey.key(for:), which normalises URL query parameters to a sorted canonical form so that parameter dictionary ordering does not produce cache misses.
Bytes are only stored after a successful decode, preventing a bad response from poisoning the cache for the TTL window.
let client = CDYelpAPIClient(
apiKey: "key",
cacheConfiguration: CDYelpCacheConfiguration(ttl: 300)
)All functionality available. Full UI rendering support via UIKit/SwiftUI.
All functionality available. Uses AppKit (NSColor, NSImage) instead of UIKit.
All functionality available for network requests. Limited UI rendering:
- No access to
UIImage(WatchKit does not support UIKit) - Use image URLs directly; render via
AsyncImageor WatchKit image views - All API methods work identically to iOS
Five improvements were added in v5.0.0, all additive and opt-in via CDYelpAPIClient.init parameters:
- Middleware/Interceptor Pattern —
CDYelpEventMonitorandCDYelpRequestAdapterprotocols; bridged to Alamofire internals viaCDYelpAlamofireEventMonitorandCDYelpAlamofireRequestAdapter - Response Caching Layer —
CDYelpCacheConfiguration+ internalCDYelpResponseCache(NSCache + TTL) +CDYelpCacheKey - Retry Strategy —
CDYelpRetryConfiguration; wires AlamofireRetryPolicyinto the sessionInterceptor - Custom Decoders —
CDYelpDecoderConfiguration; injected into everycachedRequestcall path - Testing Utilities —
CDYelpMockURLProtocol+CDYelpMockClientFactory; compiled under#if DEBUG || TESTING
- Drop Alamofire entirely; replace with a native
CDYelpURLSessionactor - All v5 public configuration structs and protocols survive unchanged
- See
Documentation/IMPROVEMENTS.mdfor the full v6 implementation plan
CDYelpFusionKit commits to semantic versioning:
- Patch versions — Bug fixes only
- Minor versions — New features, backward-compatible
- Major versions — Breaking changes allowed (e.g., v3.x → v4.0)
Tests use Swift Testing framework (import Testing) with fixtures:
@Suite struct CDYelpBusinessTests {
@Test func businessSearchDecodesFromJSON() throws {
let json = """
{ "id": "test", "name": "Test Business" }
"""
let business = try JSONDecoder().decode(
CDYelpBusiness.BusinessSearch.self,
from: json.data(using: .utf8)!
)
#expect(business.id == "test")
}
}Router tests validate URL construction without network access:
@Suite struct CDYelpRouterTests {
@Test func searchRouterProducesGetRequest() throws {
let router = CDYelpRouter.search(parameters: ["term": "coffee"])
let request = try router.asURLRequest()
#expect(request.httpMethod == "GET")
#expect(request.url?.host == "api.yelp.com")
}
}CDYelpMockURLProtocol and CDYelpMockClientFactory (in Source/Testing/, compiled under #if DEBUG || TESTING) enable end-to-end integration tests against the real CDYelpAPIClient without network access:
CDYelpMockURLProtocol.register(
stub: .init(data: fixtureData, statusCode: 200),
forURLContaining: "businesses/search"
)
let client = CDYelpMockClientFactory.makeClient()
let response = try await client.searchBusinesses(...)JSON fixtures live in Tests/CDYelpFusionKitTests/Fixtures/ and are loaded via FixtureLoader.
API documentation is generated via Swift's native DocC system:
swift package --disable-sandbox generate-documentation \
--target CDYelpFusionKit \
--output-path docs \
--transform-for-static-hosting \
--hosting-base-path CDYelpFusionKitGenerated docs are published to GitHub Pages at chrisdhaan.github.io/CDYelpFusionKit.
All public API includes triple-slash /// documentation comments:
/// Searches for businesses using the Yelp Fusion Search API.
/// - Parameter term: Business type or name (e.g., "coffee", "restaurants").
/// - Parameter location: Location string (e.g., "San Francisco").
/// - Returns: A ``CDYelpSearchResponse`` containing matching businesses.
/// - Throws: ``AFError`` if the request fails.
public func searchBusinesses(...) async throws -> CDYelpSearchResponseDocC cross-references use backtick syntax (e.g., CDYelpSearchResponse ).