Skip to content

Commit bfdd853

Browse files
committed
refactor(shared): simplify RESTPathResolver by removing resource registry
- Remove resource ID registration and mappings - Derive resource ID parameter dynamically from command and params - Streamline path resolution for resource actions - Clean up related tests accordingly
1 parent f2d391b commit bfdd853

3 files changed

Lines changed: 45 additions & 86 deletions

File tree

Sources/Domain/Shared/RESTPathResolver+Resources.swift

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 44 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import Foundation
22

3-
/// Resolves CLI command + params into a REST API path.
3+
/// Resolves a CLI command + params tuple into a REST API path.
44
///
5-
/// **Open-Closed**: the resolver never needs editing to add routes.
6-
/// Each domain registers its own routes via `RESTPathResolver.registerRoute()`
7-
/// — typically in the same file as its `AffordanceProviding` conformance.
5+
/// **Single rule for resource actions (get/update/delete/submit/…):**
6+
/// the command *is* the REST segment. Whatever `-id` flag appears in
7+
/// `params` names the resource being acted on — the flag shape is a
8+
/// CLI concern, not a REST one.
9+
///
10+
/// **Single rule for list/create under a parent:** domains register a
11+
/// route up-front (`registerRoute`) declaring the parent param and the
12+
/// child segment.
813
///
914
/// Example (in `App.swift`):
1015
/// ```swift
@@ -30,7 +35,6 @@ public final class RESTPathResolver: @unchecked Sendable {
3035

3136
private static let lock = NSLock()
3237
nonisolated(unsafe) private static var routes: [String: Route] = [:]
33-
nonisolated(unsafe) private static var resources: [String: String] = [:]
3438
nonisolated(unsafe) private static var initialized = false
3539

3640
// MARK: - Registration API (called by domain modules)
@@ -42,68 +46,66 @@ public final class RESTPathResolver: @unchecked Sendable {
4246
routes[command] = Route(parentParam: parentParam, parentSegment: parentSegment, segment: segment)
4347
}
4448

45-
/// Register a resource ID mapping (for get/update/delete actions).
46-
public static func registerResource(param: String, segment: String) {
47-
lock.lock()
48-
defer { lock.unlock() }
49-
resources[param] = segment
50-
}
51-
5249
/// Remove a route (for testing cleanup).
5350
public static func removeRoute(command: String) {
5451
lock.lock()
5552
defer { lock.unlock() }
5653
routes.removeValue(forKey: command)
5754
}
5855

59-
/// Remove a resource mapping (for testing cleanup).
60-
public static func removeResource(param: String) {
61-
lock.lock()
62-
defer { lock.unlock() }
63-
resources.removeValue(forKey: param)
64-
}
65-
6656
// MARK: - Resolution
6757

6858
public static func resolve(command: String, action: String, params: [String: String]) -> String {
6959
ensureInitialized()
7060

7161
lock.lock()
7262
let currentRoutes = routes
73-
let currentResources = resources
7463
lock.unlock()
7564

7665
let base = "/api/v1"
7766

78-
// Actions on an existing resource by its own ID (get, update, delete, submit, etc.)
79-
if action != "list" && action != "create" {
80-
// Preferred: the flag matches the singularized command (e.g. `--version-id` for
81-
// `versions`) and may be aliased via a global mapping (e.g. `product-id → xcode-cloud`).
82-
let derivedIdParam = "\(singularize(command))-id"
83-
if let idValue = params[derivedIdParam] {
84-
let segment = currentResources[derivedIdParam] ?? command
85-
return resourcePath(base: base, segment: segment, id: idValue, action: action)
86-
}
87-
// Fallback: the CLI may use a shorter alias shared between resources
88-
// (e.g. `--localization-id` in both version-localizations and app-info-localizations).
89-
// The command name alone determines the segment — global alias tables are ambiguous here.
90-
if let key = params.keys.filter({ $0.hasSuffix("-id") }).sorted().first,
91-
let idValue = params[key] {
92-
return resourcePath(base: base, segment: command, id: idValue, action: action)
93-
}
67+
// Actions on a resource by its own id (get, update, delete, submit, …).
68+
if action != "list", action != "create",
69+
let idValue = resourceId(in: params, command: command) {
70+
return resourcePath(base: base, segment: command, id: idValue, action: action)
9471
}
9572

96-
// List/create under parent
97-
if let route = currentRoutes[command] {
98-
if let parentId = params[route.parentParam] {
99-
return "\(base)/\(route.parentSegment)/\(parentId)/\(route.segment)"
100-
}
73+
// List/create under a parent resource.
74+
if let route = currentRoutes[command],
75+
let parentId = params[route.parentParam] {
76+
return "\(base)/\(route.parentSegment)/\(parentId)/\(route.segment)"
10177
}
10278

103-
// Top-level resource (e.g. apps list)
79+
// Top-level (list/create on the resource itself).
10480
return "\(base)/\(command)"
10581
}
10682

83+
// MARK: - Helpers
84+
85+
/// Pick the id value for a resource action. Prefers the singularized-from-command
86+
/// name (`version-id` for `versions`, `certificate-id` for `certificates`), otherwise
87+
/// accepts any `*-id` key — CLI flags can be short aliases shared across commands
88+
/// (`--localization-id`, `--product-id`) and the *command* is the authoritative segment.
89+
private static func resourceId(in params: [String: String], command: String) -> String? {
90+
if let value = params["\(singularize(command))-id"] { return value }
91+
guard let key = params.keys.filter({ $0.hasSuffix("-id") }).sorted().first else { return nil }
92+
return params[key]
93+
}
94+
95+
private static func resourcePath(base: String, segment: String, id: String, action: String) -> String {
96+
if action == "get" || action == "update" || action == "delete" {
97+
return "\(base)/\(segment)/\(id)"
98+
}
99+
return "\(base)/\(segment)/\(id)/\(action)"
100+
}
101+
102+
private static func singularize(_ command: String) -> String {
103+
if command.hasSuffix("s") {
104+
return String(command.dropLast())
105+
}
106+
return command
107+
}
108+
107109
// MARK: - Lazy initialization
108110

109111
/// Triggers all domain route registrations on first use.
@@ -125,20 +127,5 @@ public final class RESTPathResolver: @unchecked Sendable {
125127
_ = _xcodeCloudRoutes
126128
_ = _codeSigningRoutes
127129
_ = _appShotsRoutes
128-
_ = _resourceMappings
129-
}
130-
131-
private static func resourcePath(base: String, segment: String, id: String, action: String) -> String {
132-
if action == "get" || action == "update" || action == "delete" {
133-
return "\(base)/\(segment)/\(id)"
134-
}
135-
return "\(base)/\(segment)/\(id)/\(action)"
136-
}
137-
138-
private static func singularize(_ command: String) -> String {
139-
if command.hasSuffix("s") {
140-
return String(command.dropLast())
141-
}
142-
return command
143130
}
144131
}

Tests/DomainTests/Shared/AffordanceTests.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -324,14 +324,10 @@ struct AffordanceTests {
324324
RESTPathResolver.removeRoute(command: "custom-widgets")
325325
}
326326

327-
@Test func `custom resource registered at runtime resolves for get`() {
327+
@Test func `get action on custom command resolves to segment matching the command name`() {
328328
// singularize("custom-widgets") → "custom-widget" → param "custom-widget-id"
329-
RESTPathResolver.registerResource(param: "custom-widget-id", segment: "custom-widgets")
330329
let a = Affordance(key: "getWidget", command: "custom-widgets", action: "get", params: ["custom-widget-id": "w-1"])
331330
#expect(a.restLink.href == "/api/v1/custom-widgets/w-1")
332-
333-
// Clean up
334-
RESTPathResolver.removeResource(param: "custom-widget-id")
335331
}
336332

337333
@Test func `update action uses command segment when cli flag differs from singularized name`() {

0 commit comments

Comments
 (0)