The domain framework. Targets macOS only so all tests run natively without a simulator.
Allowed imports: Foundation, Observation
Forbidden imports: UIKit, SwiftUI, AppKit, AppIntents, SwiftSoup, Combine
Contains:
- Domain models:
Car,PlateFormat,SearchHistoryItem,CarInfoError - Domain logic:
PlateValidator - Protocol contracts:
HTMLClient,CarInfoLoader,CarStore,FavoritesStore - ViewModel:
PlateFinderViewModel(@Observable)
The app target. Hosts all concrete infrastructure types and the Composition Root.
Allowed imports: anything
Rule: No domain model or protocol implementation may import a concrete infrastructure type.
Contains:
URLSessionHTTPClient(conforms toHTMLClient)ANTCarInfoService(conforms toCarInfoLoader) — fetches HTML, parses viaCarHTMLMapperCarHTMLMapper— static type; maps HTMLDocument→CarUserDefaultsCarStore(conforms toCarStore+FavoritesStore)- SwiftUI Views
- Composition Root (
PlateFinderApp.body) AppIntents(FindPlateIntent,PlateFinderShortcuts)
Linked against the macOS domain framework. All tests run on Mac — no simulator.
Uses: Swift Testing (import Testing)
iOS App Target ──────────────────────────────────────────────────────────┐
│ │
│ Composition Root wires: │
│ URLSessionHTTPClient ──► HTMLClient (protocol) │
│ ANTCarInfoService ──► CarInfoLoader (protocol) │
│ UserDefaultsCarStore ──► CarStore + FavoritesStore (protocols) │
│ │
│ SwiftUI Views bind to: │
│ PlateFinderViewModel ◄── @Observable (domain framework) │
│ │
└──────────────────────────────────┬───────────────────────────────────────┘
│ depends on (protocols only)
┌──────────────▼──────────────┐
│ PlateFinder (macOS Framework)│
│ │
│ Car │
│ PlateFormat │
│ SearchHistoryItem │
│ CarInfoError │
│ PlateValidator │
│ │
│ HTMLClient (protocol)│
│ CarInfoLoader (protocol)│
│ CarStore (protocol)│
│ FavoritesStore (protocol)│
│ │
│ PlateFinderViewModel │
│ (@Observable) │
└──────────────────────────────┘
Rule: Arrows never point from the domain framework toward the app target. The app target is the only module that knows about both layers.
All methods use async throws (iOS 17+ / Swift 5.5+).
Abstracts HTTP fetching. The domain framework depends on this; URLSession is never mentioned inside the framework.
protocol HTMLClient {
func fetchHTML(from url: URL) async throws -> String
}Errors surfaced to callers: CarInfoError.networkError, CarInfoError.serverError
Single use-case protocol. One method, one responsibility.
protocol CarInfoLoader {
func loadCarInfo(for plateNumber: String) async throws -> Car
}Success: returns a fully populated Car.
Failure: throws CarInfoError (.networkError, .serverError, .noDataFound, .parsingError, .invalidPlateFormat)
Abstracts search-history persistence. Implementations (UserDefaults, CoreData, in-memory spy) are invisible to the domain.
protocol CarStore {
func retrieve() async throws -> [SearchHistoryItem]
func insert(_ item: SearchHistoryItem) async throws
func delete(_ item: SearchHistoryItem) async throws
func deleteAll() async throws
}Abstracts favorite-plates persistence. Kept separate from CarStore (Interface Segregation).
protocol FavoritesStore {
func retrieveFavorites() async throws -> [String]
func insertFavorite(_ plateNumber: String) async throws
func deleteFavorite(_ plateNumber: String) async throws
}Pure value type. No Codable conformance in the domain — that is an infrastructure concern.
| Property | Type | Source (HTML table cell) |
|---|---|---|
plate |
String |
row 0, cell 0 |
manufacturer |
String |
row 0, cell 2 |
colorName |
String |
row 0, cell 4 |
registrationYear |
String |
row 0, cell 6 |
model |
String |
row 1, cell 1 |
segment |
String |
row 1, cell 3 |
registrationDate |
String |
row 1, cell 5 |
year |
String |
row 2, cell 1 |
service |
String |
row 2, cell 3 |
expirationDate |
String |
row 2, cell 5 |
tint |
String |
row 3, cell 1 |
tintExpirationDate |
String |
row 3, cell 3 |
struct SearchHistoryItem {
let plateNumber: String
let searchDate: Date
let car: Car? // nil if the search failed
}Codable conformance lives in infrastructure (for UserDefaults serialization), not in the domain model.
enum PlateFormat: Equatable {
case empty
case partiallyValid
case invalid
case bike // AA123A
case car // AAA1234
case special // CD1234
}enum CarInfoError: Error {
case networkError(Error)
case noDataFound
case invalidPlateFormat
case parsingError(String)
case serverError
}LocalizedError conformance (user-facing strings) stays in the app target — it is a UI concern.
| Current (pre-TDD) | TDD target |
|---|---|
CarInfoService hardcoded in ViewModel |
Inject CarInfoLoader protocol |
URLSession.shared in AntScrapper |
URLSessionHTTPClient behind HTMLClient |
isTesting: Bool flag |
Removed — use injected spy in tests |
ObservableObject + @Published in ViewModel |
@Observable (import Observation) |
Car: Codable in domain |
Car is plain struct; Codable in infrastructure DTO |
UserDataService.shared singleton |
Injected CarStore + FavoritesStore protocols |
Localized strings in CarInfoError |
Move to app target (LocalizedError extension) |
SwiftUI imported in ViewModel |
Removed — ViewModel has zero UI framework imports |