@@ -115,33 +115,166 @@ 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+ /// - Returns: A fully-qualified URL for the endpoint.
129+ func distributionEndpoint( forRepository repository: String , andEndpoint endpoint: String ) -> URL {
130+ self . appendingPathComponent ( " /v2/ \( repository) / \( endpoint) " )
126131 }
127132}
128133
129134extension RegistryClient {
135+ /// Represents an operation to be executed on the registry.
136+ struct RegistryOperation {
137+ enum Destination {
138+ case subpath( String ) // Repository subpath on the registry
139+ case url( URL ) // Full destination URL, for example from a Location header returned by the registry
140+ }
141+
142+ var method : HTTPRequest . Method // HTTP method
143+ var repository : String // Repository path on the registry
144+ var destination : Destination // Destination of the operation: can be a subpath or remote URL
145+ var accepting : [ String ] = [ ] // Acceptable response types
146+ var contentType : String ? = nil // Request data type
147+
148+ func url( relativeTo registry: URL ) -> URL {
149+ switch destination {
150+ case . url( let url) : return url
151+ case . subpath( let path) :
152+ let subpath = registry. distributionEndpoint ( forRepository: repository, andEndpoint: path)
153+ return subpath
154+ }
155+ }
156+
157+ // Convenience constructors
158+ static func get(
159+ _ repository: String ,
160+ path: String ,
161+ actions: [ String ] ? = nil ,
162+ accepting: [ String ] = [ ] ,
163+ contentType: String ? = nil
164+ ) -> RegistryOperation {
165+ . init(
166+ method: . get,
167+ repository: repository,
168+ destination: . subpath( path) ,
169+ accepting: accepting,
170+ contentType: contentType
171+ )
172+ }
173+
174+ static func get(
175+ _ repository: String ,
176+ url: URL ,
177+ actions: [ String ] ? = nil ,
178+ accepting: [ String ] = [ ] ,
179+ contentType: String ? = nil
180+ ) -> RegistryOperation {
181+ . init(
182+ method: . get,
183+ repository: repository,
184+ destination: . url( url) ,
185+ accepting: accepting,
186+ contentType: contentType
187+ )
188+ }
189+
190+ static func head(
191+ _ repository: String ,
192+ path: String ,
193+ actions: [ String ] ? = nil ,
194+ accepting: [ String ] = [ ] ,
195+ contentType: String ? = nil
196+ ) -> RegistryOperation {
197+ . init(
198+ method: . head,
199+ repository: repository,
200+ destination: . subpath( path) ,
201+ accepting: accepting,
202+ contentType: contentType
203+ )
204+ }
205+
206+ /// 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
207+ static func put(
208+ _ repository: String ,
209+ url: URL ,
210+ actions: [ String ] ? = nil ,
211+ accepting: [ String ] = [ ] ,
212+ contentType: String ? = nil
213+ ) -> RegistryOperation {
214+ . init(
215+ method: . put,
216+ repository: repository,
217+ destination: . url( url) ,
218+ accepting: accepting,
219+ contentType: contentType
220+ )
221+ }
222+
223+ static func put(
224+ _ repository: String ,
225+ path: String ,
226+ actions: [ String ] ? = nil ,
227+ accepting: [ String ] = [ ] ,
228+ contentType: String ? = nil
229+ ) -> RegistryOperation {
230+ . init(
231+ method: . put,
232+ repository: repository,
233+ destination: . subpath( path) ,
234+ accepting: accepting,
235+ contentType: contentType
236+ )
237+ }
238+
239+ static func post(
240+ _ repository: String ,
241+ path: String ,
242+ actions: [ String ] ? = nil ,
243+ accepting: [ String ] = [ ] ,
244+ contentType: String ? = nil
245+ ) -> RegistryOperation {
246+ . init(
247+ method: . post,
248+ repository: repository,
249+ destination: . subpath( path) ,
250+ accepting: accepting,
251+ contentType: contentType
252+ )
253+ }
254+ }
255+
130256 /// Execute an HTTP request with no request body.
131257 /// - Parameters:
132- /// - request : The HTTP request to execute.
258+ /// - operation : The Registry operation to execute.
133259 /// - success: The HTTP status code expected if the request is successful.
134260 /// - errors: Expected error codes for which the registry sends structured error messages.
135261 /// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPURLResponse.
136262 /// - Throws: If the server response is unexpected or indicates that an error occurred.
137263 ///
138264 /// A plain Data version of this function is required because Data is Decodable and decodes from base64.
139265 /// Plain blobs are not encoded in the registry, so trying to decode them will fail.
140- public func executeRequestThrowing(
141- _ request : HTTPRequest ,
266+ func executeRequestThrowing(
267+ _ operation : RegistryOperation ,
142268 expectingStatus success: HTTPResponse . Status = . ok,
143269 decodingErrors errors: [ HTTPResponse . Status ]
144270 ) async throws -> ( data: Data , response: HTTPResponse ) {
271+ let request = HTTPRequest (
272+ method: operation. method,
273+ url: operation. url ( relativeTo: registryURL) ,
274+ accepting: operation. accepting,
275+ contentType: operation. contentType
276+ )
277+
145278 do {
146279 let authenticatedRequest = auth? . auth ( for: request) ?? request
147280 return try await client. executeRequestThrowing ( authenticatedRequest, expectingStatus: success)
@@ -166,8 +299,8 @@ extension RegistryClient {
166299 /// - errors: Expected error codes for which the registry sends structured error messages.
167300 /// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPURLResponse.
168301 /// - Throws: If the server response is unexpected or indicates that an error occurred.
169- public func executeRequestThrowing< Response: Decodable > (
170- _ request: HTTPRequest ,
302+ func executeRequestThrowing< Response: Decodable > (
303+ _ request: RegistryOperation ,
171304 expectingStatus success: HTTPResponse . Status = . ok,
172305 decodingErrors errors: [ HTTPResponse . Status ]
173306 ) async throws -> ( data: Response , response: HTTPResponse ) {
@@ -182,7 +315,7 @@ extension RegistryClient {
182315
183316 /// Execute an HTTP request uploading a request body.
184317 /// - Parameters:
185- /// - request : The HTTP request to execute.
318+ /// - operation : The Registry operation to execute.
186319 /// - payload: The request body to upload.
187320 /// - success: The HTTP status code expected if the request is successful.
188321 /// - errors: Expected error codes for which the registry sends structured error messages.
@@ -191,12 +324,19 @@ extension RegistryClient {
191324 ///
192325 /// A plain Data version of this function is required because Data is Encodable and encodes to base64.
193326 /// 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 ,
327+ func executeRequestThrowing(
328+ _ operation : RegistryOperation ,
196329 uploading payload: Data ,
197330 expectingStatus success: HTTPResponse . Status ,
198331 decodingErrors errors: [ HTTPResponse . Status ]
199332 ) async throws -> ( data: Data , response: HTTPResponse ) {
333+ let request = HTTPRequest (
334+ method: operation. method,
335+ url: operation. url ( relativeTo: registryURL) ,
336+ accepting: operation. accepting,
337+ contentType: operation. contentType
338+ )
339+
200340 do {
201341 let authenticatedRequest = auth? . auth ( for: request) ?? request
202342 return try await client. executeRequestThrowing (
@@ -224,20 +364,20 @@ extension RegistryClient {
224364
225365 /// Execute an HTTP request uploading a Codable request body.
226366 /// - Parameters:
227- /// - request : The HTTP request to execute.
367+ /// - operation : The Registry operation to execute.
228368 /// - payload: The request body to upload.
229369 /// - success: The HTTP status code expected if the request is successful.
230370 /// - errors: Expected error codes for which the registry sends structured error messages.
231371 /// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPURLResponse.
232372 /// - Throws: If the server response is unexpected or indicates that an error occurred.
233- public func executeRequestThrowing< Body: Encodable > (
234- _ request : HTTPRequest ,
373+ func executeRequestThrowing< Body: Encodable > (
374+ _ operation : RegistryOperation ,
235375 uploading payload: Body ,
236376 expectingStatus success: HTTPResponse . Status ,
237377 decodingErrors errors: [ HTTPResponse . Status ]
238378 ) async throws -> ( data: Data , response: HTTPResponse ) {
239379 try await executeRequestThrowing (
240- request ,
380+ operation ,
241381 uploading: try encoder. encode ( payload) ,
242382 expectingStatus: success,
243383 decodingErrors: errors
0 commit comments