@@ -115,33 +115,165 @@ public struct RegistryClient {
115115 let urlsession = URLSession ( configuration: . ephemeral)
116116 try await self . init ( registry: registryURL, client: urlsession, auth: auth)
117117 }
118+ }
118119
119- func registryURLForPath( _ path: String ) throws -> URL {
120- var components = URLComponents ( )
121- components. path = path
122- guard let url = components. url ( relativeTo: registryURL) else {
123- throw RegistryClientError . invalidRegistryPath ( path)
124- }
125- return url
120+ extension URL {
121+ /// The base distribution endpoint URL
122+ var distributionEndpoint : URL { self . appendingPathComponent ( " /v2/ " ) }
123+
124+ /// The URL for a particular endpoint relating to a particular repository
125+ /// - Parameters:
126+ /// - repository: The name of the repository. May include path separators.
127+ /// - endpoint: The distribution endpoint e.g. "tags/list"
128+ func distributionEndpoint( forRepository repository: String , andEndpoint endpoint: String ) -> URL {
129+ self . appendingPathComponent ( " /v2/ \( repository) / \( endpoint) " )
126130 }
127131}
128132
129133extension RegistryClient {
134+ /// Represents an operation to be executed on the registry.
135+ struct RegistryOperation {
136+ enum Destination {
137+ case subpath( String ) // Repository subpath on the registry
138+ case url( URL ) // Full destination URL, for example from a Location header returned by the registry
139+ }
140+
141+ var method : HTTPRequest . Method // HTTP method
142+ var repository : String // Repository path on the registry
143+ var destination : Destination // Destination of the operation: can be a subpath or remote URL
144+ var accepting : [ String ] = [ ] // Acceptable response types
145+ var contentType : String ? = nil // Request data type
146+
147+ func url( relativeTo registry: URL ) -> URL {
148+ switch destination {
149+ case . url( let url) : return url
150+ case . subpath( let path) :
151+ let subpath = registry. distributionEndpoint ( forRepository: repository, andEndpoint: path)
152+ return subpath
153+ }
154+ }
155+
156+ // Convenience constructors
157+ static func get(
158+ _ repository: String ,
159+ path: String ,
160+ actions: [ String ] ? = nil ,
161+ accepting: [ String ] = [ ] ,
162+ contentType: String ? = nil
163+ ) -> RegistryOperation {
164+ . init(
165+ method: . get,
166+ repository: repository,
167+ destination: . subpath( path) ,
168+ accepting: accepting,
169+ contentType: contentType
170+ )
171+ }
172+
173+ static func get(
174+ _ repository: String ,
175+ url: URL ,
176+ actions: [ String ] ? = nil ,
177+ accepting: [ String ] = [ ] ,
178+ contentType: String ? = nil
179+ ) -> RegistryOperation {
180+ . init(
181+ method: . get,
182+ repository: repository,
183+ destination: . url( url) ,
184+ accepting: accepting,
185+ contentType: contentType
186+ )
187+ }
188+
189+ static func head(
190+ _ repository: String ,
191+ path: String ,
192+ actions: [ String ] ? = nil ,
193+ accepting: [ String ] = [ ] ,
194+ contentType: String ? = nil
195+ ) -> RegistryOperation {
196+ . init(
197+ method: . head,
198+ repository: repository,
199+ destination: . subpath( path) ,
200+ accepting: accepting,
201+ contentType: contentType
202+ )
203+ }
204+
205+ /// This handles the 'put' case where the registry gives us a location URL which we must not alter, aside from adding the digest to it
206+ static func put(
207+ _ repository: String ,
208+ url: URL ,
209+ actions: [ String ] ? = nil ,
210+ accepting: [ String ] = [ ] ,
211+ contentType: String ? = nil
212+ ) -> RegistryOperation {
213+ . init(
214+ method: . put,
215+ repository: repository,
216+ destination: . url( url) ,
217+ accepting: accepting,
218+ contentType: contentType
219+ )
220+ }
221+
222+ static func put(
223+ _ repository: String ,
224+ path: String ,
225+ actions: [ String ] ? = nil ,
226+ accepting: [ String ] = [ ] ,
227+ contentType: String ? = nil
228+ ) -> RegistryOperation {
229+ . init(
230+ method: . put,
231+ repository: repository,
232+ destination: . subpath( path) ,
233+ accepting: accepting,
234+ contentType: contentType
235+ )
236+ }
237+
238+ static func post(
239+ _ repository: String ,
240+ path: String ,
241+ actions: [ String ] ? = nil ,
242+ accepting: [ String ] = [ ] ,
243+ contentType: String ? = nil
244+ ) -> RegistryOperation {
245+ . init(
246+ method: . post,
247+ repository: repository,
248+ destination: . subpath( path) ,
249+ accepting: accepting,
250+ contentType: contentType
251+ )
252+ }
253+ }
254+
130255 /// Execute an HTTP request with no request body.
131256 /// - Parameters:
132- /// - request : The HTTP request to execute.
257+ /// - operation : The Registry operation to execute.
133258 /// - success: The HTTP status code expected if the request is successful.
134259 /// - errors: Expected error codes for which the registry sends structured error messages.
135260 /// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPURLResponse.
136261 /// - Throws: If the server response is unexpected or indicates that an error occurred.
137262 ///
138263 /// A plain Data version of this function is required because Data is Decodable and decodes from base64.
139264 /// Plain blobs are not encoded in the registry, so trying to decode them will fail.
140- public func executeRequestThrowing(
141- _ request : HTTPRequest ,
265+ func executeRequestThrowing(
266+ _ operation : RegistryOperation ,
142267 expectingStatus success: HTTPResponse . Status = . ok,
143268 decodingErrors errors: [ HTTPResponse . Status ]
144269 ) async throws -> ( data: Data , response: HTTPResponse ) {
270+ let request = HTTPRequest (
271+ method: operation. method,
272+ url: operation. url ( relativeTo: registryURL) ,
273+ accepting: operation. accepting,
274+ contentType: operation. contentType
275+ )
276+
145277 do {
146278 let authenticatedRequest = auth? . auth ( for: request) ?? request
147279 return try await client. executeRequestThrowing ( authenticatedRequest, expectingStatus: success)
@@ -166,8 +298,8 @@ extension RegistryClient {
166298 /// - errors: Expected error codes for which the registry sends structured error messages.
167299 /// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPURLResponse.
168300 /// - Throws: If the server response is unexpected or indicates that an error occurred.
169- public func executeRequestThrowing< Response: Decodable > (
170- _ request: HTTPRequest ,
301+ func executeRequestThrowing< Response: Decodable > (
302+ _ request: RegistryOperation ,
171303 expectingStatus success: HTTPResponse . Status = . ok,
172304 decodingErrors errors: [ HTTPResponse . Status ]
173305 ) async throws -> ( data: Response , response: HTTPResponse ) {
@@ -182,7 +314,7 @@ extension RegistryClient {
182314
183315 /// Execute an HTTP request uploading a request body.
184316 /// - Parameters:
185- /// - request : The HTTP request to execute.
317+ /// - operation : The Registry operation to execute.
186318 /// - payload: The request body to upload.
187319 /// - success: The HTTP status code expected if the request is successful.
188320 /// - errors: Expected error codes for which the registry sends structured error messages.
@@ -191,12 +323,19 @@ extension RegistryClient {
191323 ///
192324 /// A plain Data version of this function is required because Data is Encodable and encodes to base64.
193325 /// Accidentally encoding data blobs will cause digests to fail and runtimes to be unable to run the images.
194- public func executeRequestThrowing(
195- _ request : HTTPRequest ,
326+ func executeRequestThrowing(
327+ _ operation : RegistryOperation ,
196328 uploading payload: Data ,
197329 expectingStatus success: HTTPResponse . Status ,
198330 decodingErrors errors: [ HTTPResponse . Status ]
199331 ) async throws -> ( data: Data , response: HTTPResponse ) {
332+ let request = HTTPRequest (
333+ method: operation. method,
334+ url: operation. url ( relativeTo: registryURL) ,
335+ accepting: operation. accepting,
336+ contentType: operation. contentType
337+ )
338+
200339 do {
201340 let authenticatedRequest = auth? . auth ( for: request) ?? request
202341 return try await client. executeRequestThrowing (
@@ -224,20 +363,20 @@ extension RegistryClient {
224363
225364 /// Execute an HTTP request uploading a Codable request body.
226365 /// - Parameters:
227- /// - request : The HTTP request to execute.
366+ /// - operation : The Registry operation to execute.
228367 /// - payload: The request body to upload.
229368 /// - success: The HTTP status code expected if the request is successful.
230369 /// - errors: Expected error codes for which the registry sends structured error messages.
231370 /// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPURLResponse.
232371 /// - Throws: If the server response is unexpected or indicates that an error occurred.
233- public func executeRequestThrowing< Body: Encodable > (
234- _ request : HTTPRequest ,
372+ func executeRequestThrowing< Body: Encodable > (
373+ _ operation : RegistryOperation ,
235374 uploading payload: Body ,
236375 expectingStatus success: HTTPResponse . Status ,
237376 decodingErrors errors: [ HTTPResponse . Status ]
238377 ) async throws -> ( data: Data , response: HTTPResponse ) {
239378 try await executeRequestThrowing (
240- request ,
379+ operation ,
241380 uploading: try encoder. encode ( payload) ,
242381 expectingStatus: success,
243382 decodingErrors: errors
0 commit comments