11import 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}
0 commit comments