diff --git a/.claude/docs/https_-swiftpackageindex.com-apple-swift-openapi-runtime-1.9.0-documentation-openapiruntime.md b/.claude/docs/https_-swiftpackageindex.com-apple-swift-openapi-runtime-1.9.0-documentation-openapiruntime.md new file mode 100644 index 00000000..e5cf177e --- /dev/null +++ b/.claude/docs/https_-swiftpackageindex.com-apple-swift-openapi-runtime-1.9.0-documentation-openapiruntime.md @@ -0,0 +1,5944 @@ + + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime + +Framework + +# OpenAPIRuntime + +Use and extend your client and server code generated by Swift OpenAPI Generator. + +## Overview + +This library provides common abstractions and helper functions used by the client and server code generated by Swift OpenAPI Generator. + +It contains: + +- Common types used in the code generated by the `swift-openapi-generator` package plugin. + +- Protocol definitions for pluggable layers, including `ClientTransport`, `ServerTransport`, `ClientMiddleware`, and `ServerMiddleware`. + +Many of the HTTP currency types used are defined in the Swift HTTP Types library. + +### Usage + +Add the package dependency in your `Package.swift`: + +.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"), + +Next, in your target, add `OpenAPIRuntime` to your dependencies: + +.target(name: "MyTarget", dependencies: [\ +.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),\ +]), + +The next step depends on your use case. + +#### Using Swift OpenAPI Generator for code generation + +The generated code depends on types from this library. Check out the adoption guides in the Swift OpenAPI Generator documentation to see how the packages fit together. + +#### Implementing transports and middlewares + +Swift OpenAPI Generator generates client and server code that is designed to be used with pluggable transports and middlewares. + +Implement a new transport or middleware by providing a type that adopts one of the protocols from the runtime library: + +- `ClientTransport` + +- `ClientMiddleware` + +- `ServerTransport` + +- `ServerMiddleware` + +You can also publish your transport or middleware as a Swift package to allow others to use it with their generated code. + +## Topics + +### Essentials + +`protocol ClientTransport` + +A type that performs HTTP operations. + +`protocol ServerTransport` + +A type that registers and handles HTTP operations. + +`protocol ClientMiddleware` + +A type that intercepts HTTP requests and responses. + +`protocol ServerMiddleware` + +### Customization + +`struct Configuration` + +A set of configuration values used by the generated client and server types. + +`protocol DateTranscoder` + +A type that allows customization of Date encoding and decoding. + +`struct ISO8601DateTranscoder` + +A transcoder for dates encoded as an ISO-8601 string (in RFC 3339 format). + +`protocol MultipartBoundaryGenerator` + +A generator of a new boundary string used by multipart messages to separate parts. + +`struct RandomMultipartBoundaryGenerator` + +A generator that returns a boundary containg a constant prefix and a random suffix. + +`struct ConstantMultipartBoundaryGenerator` + +A generator that always returns the same constant boundary string. + +`enum IterationBehavior` + +Describes how many times the provided sequence can be iterated. + +### Content types + +`class HTTPBody` + +A body of an HTTP request or HTTP response. + +`struct Base64EncodedData` + +A type for converting data as a base64 string. + +`class MultipartBody` + +The body of multipart requests and responses. + +`struct MultipartRawPart` + +A raw multipart part containing the header fields and the body stream. + +`struct MultipartPart` + +A wrapper of a typed part with a statically known name that adds other dynamic `content-disposition` parameter values, such as `filename`. + +`struct MultipartDynamicallyNamedPart` + +A wrapper of a typed part without a statically known name that adds dynamic `content-disposition` parameter values, such as `name` and `filename`. + +### Errors + +`struct ClientError` + +An error thrown by a client performing an OpenAPI operation. + +`struct ServerError` + +An error thrown by a server handling an OpenAPI operation. + +`struct UndocumentedPayload` + +A payload value used by undocumented operation responses. + +### HTTP Currency Types + +`struct ServerRequestMetadata` + +A container for request metadata already parsed and validated by the server transport. + +`protocol AcceptableProtocol` + +The protocol that all generated `AcceptableContentType` enums conform to. + +`struct AcceptHeaderContentType` + +A wrapper of an individual content type in the accept header. + +`struct QualityValue` + +A quality value used to describe the order of priority in a comma-separated list of values, such as in the Accept header. + +### Dynamic Payloads + +`struct OpenAPIValueContainer` + +A container for a value represented by JSON Schema. + +`struct OpenAPIObjectContainer` + +A container for a dictionary with values represented by JSON Schema. + +`struct OpenAPIArrayContainer` + +A container for an array with values represented by JSON Schema. + +### Protocols + +`protocol CustomCoder` + +A type that allows custom content type encoding and decoding. + +`protocol HTTPResponseConvertible` + +A value that can be converted to an HTTP response and body. + +### Structures + +`struct ErrorHandlingMiddleware` + +An opt-in error handling middleware that converts an error to an HTTP response. + +`struct JSONEncodingOptions` + +The options that control the encoded JSON data. + +`struct JSONLinesDeserializationSequence` + +A sequence that parses arbitrary byte chunks into lines using the JSON Lines format. + +`struct JSONLinesSerializationSequence` + +A sequence that serializes lines by concatenating them using the JSON Lines format. + +`struct JSONSequenceDeserializationSequence` + +A sequence that parses arbitrary byte chunks into lines using the JSON Sequence format. + +`struct JSONSequenceSerializationSequence` + +A sequence that serializes lines by concatenating them using the JSON Sequence format. + +`struct ServerSentEvent` + +An event sent by the server. + +`struct ServerSentEventWithJSONData` + +An event sent by the server that has a JSON payload in the data field. + +`struct ServerSentEventsDeserializationSequence` + +A sequence that parses arbitrary byte chunks into events using the Server-sent Events format. + +`struct ServerSentEventsLineDeserializationSequence` + +A sequence that parses arbitrary byte chunks into lines using the Server-sent Events format. + +`struct ServerSentEventsSerializationSequence` + +A sequence that serializes Server-sent Events. + +### Extended Modules + +Foundation + +Swift + +\_Concurrency + +- OpenAPIRuntime +- Overview +- Usage +- Topics + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clienttransport + +- OpenAPIRuntime +- ClientTransport + +Protocol + +# ClientTransport + +A type that performs HTTP operations. + +protocol ClientTransport : Sendable + +ClientTransport.swift + +## Overview + +Decouples an underlying HTTP library from generated client code. + +### Choose between a transport and a middleware + +The `ClientTransport` and `ClientMiddleware` protocols look similar, however each serves a different purpose. + +A _transport_ abstracts over the underlying HTTP library that actually performs the HTTP operation by using the network. A generated `Client` requires an exactly one client transport. + +A _middleware_ intercepts the HTTP request and response, without being responsible for performing the HTTP operation itself. That’s why middlewares take the extra `next` parameter, to delegate making the HTTP call to the transport at the top of the middleware stack. + +### Use an existing client transport + +Instantiate the transport using the parameters required by the specific implementation. For example, using the client transport for the `URLSession` HTTP client provided by the Foundation framework: + +let transport = URLSessionTransport() + +Instantiate the `Client` type generated by the Swift OpenAPI Generator for your provided OpenAPI document. For example: + +let client = Client( +serverURL: URL(string: "https://example.com")!, +transport: transport +) + +Use the client to make HTTP calls defined in your OpenAPI document. For example, if the OpenAPI document contains an HTTP operation with the identifier `checkHealth`, call it from Swift with: + +let response = try await client.checkHealth() + +The generated operation method takes an `Input` type unique to the operation, and returns an `Output` type unique to the operation. + +### Implement a custom client transport + +If a client transport implementation for your preferred HTTP library doesn’t yet exist, or you need to simulate rare network conditions in your tests, consider implementing a custom client transport. + +For example, to implement a test client transport that allows you to test both a healthy and unhealthy response from a `checkHealth` operation, define a new struct that conforms to the `ClientTransport` protocol: + +struct TestTransport: ClientTransport { +var isHealthy: Bool = true +func send( +_ request: HTTPRequest, +body: HTTPBody?, +baseURL: URL, +operationID: String + +( +HTTPResponse(status: isHealthy ? .ok : .internalServerError), +nil +) +} +} + +Then in your test code, instantiate and provide the test transport to your generated client instead: + +var transport = TestTransport() +transport.isHealthy = true // for HTTP status code 200 (success) +let client = Client( +serverURL: URL(string: "https://example.com")!, +transport: transport +) +let response = try await client.checkHealth() + +Implementing a test client transport is just one way to help test your code that integrates with a generated client. Another is to implement a type conforming to the generated protocol `APIProtocol`, and to implement a custom `ClientMiddleware`. + +## Topics + +### Instance Methods + +Sends the underlying HTTP request and returns the received HTTP response. + +**Required** + +## Relationships + +### Inherits From + +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Essentials + +`protocol ServerTransport` + +A type that registers and handles HTTP operations. + +`protocol ClientMiddleware` + +A type that intercepts HTTP requests and responses. + +`protocol ServerMiddleware` + +- ClientTransport +- Overview +- Choose between a transport and a middleware +- Use an existing client transport +- Implement a custom client transport +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servertransport + +- OpenAPIRuntime +- ServerTransport + +Protocol + +# ServerTransport + +A type that registers and handles HTTP operations. + +protocol ServerTransport + +ServerTransport.swift + +## Overview + +Decouples the HTTP server framework from the generated server code. + +### Choose between a transport and a middleware + +The `ServerTransport` and `ServerMiddleware` protocols look similar, however each serves a different purpose. + +A _transport_ abstracts over the underlying HTTP library that actually receives the HTTP requests from the network. An implemented _handler_ (a type implemented by you that conforms to the generated `APIProtocol` protocol) is generally configured with exactly one server transport. + +A _middleware_ intercepts the HTTP request and response, without being responsible for receiving the HTTP operations itself. That’s why middlewares take the extra `next` parameter, to delegate calling the handler to the transport at the top of the middleware stack. + +### Use an existing server transport + +Instantiate the transport using the parameters required by the specific implementation. For example, using the server transport for the `Vapor` web framework, first create the `Application` object provided by Vapor, and provided it to the initializer of `VaporTransport`: + +let app = Vapor.Application() +let transport = VaporTransport(routesBuilder: app) + +Implement a new type that conforms to the generated `APIProtocol`, which serves as the request handler of your server’s business logic. For example, this is what a simple implementation of a server that has a single HTTP operation called `checkHealth` defined in the OpenAPI document, and it always returns the 200 HTTP status code: + +struct MyAPIImplementation: APIProtocol { +func checkHealth( +_ input: Operations.checkHealth.Input + +.ok(.init()) +} +} + +The generated operation method takes an `Input` type unique to the operation, and returns an `Output` type unique to the operation. + +Create an instance of your handler: + +let handler = MyAPIImplementation() + +Create the URL where the server will run. The path of the URL is extracted by the transport to create a common prefix (such as `/api/v1`) that might be expected by the clients. + +Register the generated request handlers by calling the method generated on the `APIProtocol` protocol: + +try handler.registerHandlers( +on: transport, +serverURL: URL(string: "/api/v1")! +) + +Start the server by following the documentation of your chosen transport: + +try await app.execute() + +### Implement a custom server transport + +If a server transport implementation for your preferred web framework doesn’t yet exist, or you need to simulate rare network conditions in your tests, consider implementing a custom server transport. + +Define a new type that conforms to the `ServerTransport` protocol by registering request handlers with the underlying web framework, to be later called when the web framework receives an HTTP request to one of the HTTP routes. + +In tests, this might require using the web framework’s specific test APIs to allow for simulating incoming HTTP requests. + +Implementing a test server transport is just one way to help test your code that integrates with your handler. Another is to implement a type conforming to the generated protocol `APIProtocol`, and to implement a custom `ServerMiddleware`. + +## Topics + +### Instance Methods + +Registers an HTTP operation handler at the provided path and method. + +**Required** + +## See Also + +### Essentials + +`protocol ClientTransport` + +A type that performs HTTP operations. + +`protocol ClientMiddleware` + +A type that intercepts HTTP requests and responses. + +`protocol ServerMiddleware` + +- ServerTransport +- Overview +- Choose between a transport and a middleware +- Use an existing server transport +- Implement a custom server transport +- Topics +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clientmiddleware + +- OpenAPIRuntime +- ClientMiddleware + +Protocol + +# ClientMiddleware + +A type that intercepts HTTP requests and responses. + +protocol ClientMiddleware : Sendable + +ClientTransport.swift + +## Overview + +It allows you to read and modify the request before it is received by the transport and the response after it is returned by the transport. + +Appropriate for handling authentication, logging, metrics, tracing, injecting custom headers such as “user-agent”, and more. + +### Choose between a transport and a middleware + +The `ClientTransport` and `ClientMiddleware` protocols look similar, however each serves a different purpose. + +A _transport_ abstracts over the underlying HTTP library that actually performs the HTTP operation by using the network. A generated `Client` requires an exactly one client transport. + +A _middleware_ intercepts the HTTP request and response, without being responsible for performing the HTTP operation itself. That’s why middlewares take the extra `next` parameter, to delegate making the HTTP call to the transport at the top of the middleware stack. + +### Use an existing client middleware + +Instantiate the middleware using the parameters required by the specific implementation. For example, using a hypothetical existing middleware that logs every request and response: + +let loggingMiddleware = LoggingMiddleware() + +Similarly to the process of using an existing `ClientTransport`, provide the middleware to the initializer of the generated `Client` type: + +let client = Client( +serverURL: URL(string: "https://example.com")!, +transport: transport, +middlewares: [\ +loggingMiddleware,\ +] +) + +Then make a call to one of the generated client methods: + +let response = try await client.checkHealth() + +As part of the invocation of `checkHealth`, the client first invokes the middlewares in the order you provided them, and then passes the request to the transport. When a response is received, the last middleware handles it first, in the reverse order of the `middlewares` array. + +### Implement a custom client middleware + +If a client middleware implementation with your desired behavior doesn’t yet exist, or you need to simulate rare network conditions your tests, consider implementing a custom client middleware. + +For example, to implement a middleware that injects the “Authorization” header to every outgoing request, define a new struct that conforms to the `ClientMiddleware` protocol: + +/// Injects an authorization header to every request. +struct AuthenticationMiddleware: ClientMiddleware { + +/// The token value. +var bearerToken: String + +func intercept( +_ request: HTTPRequest, +body: HTTPBody?, +baseURL: URL, +operationID: String, + +var request = request +request.headerFields[.authorization] = "Bearer \(bearerToken)" +return try await next(request, body, baseURL) +} +} + +An alternative use case for a middleware is to inject random failures when calling a real server, to test your retry and error-handling logic. + +Implementing a test client middleware is just one way to help test your code that integrates with a generated client. Another is to implement a type conforming to the generated protocol `APIProtocol`, and to implement a custom `ClientTransport`. + +## Topics + +### Instance Methods + +Intercepts an outgoing HTTP request and an incoming HTTP response. + +**Required** + +## Relationships + +### Inherits From + +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Essentials + +`protocol ClientTransport` + +A type that performs HTTP operations. + +`protocol ServerTransport` + +A type that registers and handles HTTP operations. + +`protocol ServerMiddleware` + +- ClientMiddleware +- Overview +- Choose between a transport and a middleware +- Use an existing client middleware +- Implement a custom client middleware +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servermiddleware + +- OpenAPIRuntime +- ServerMiddleware + +Protocol + +# ServerMiddleware + +A type that intercepts HTTP requests and responses. + +protocol ServerMiddleware : Sendable + +ServerTransport.swift + +## Overview + +It allows you to customize the request after it was provided by the transport, but before it was parsed, validated, and provided to the request handler; and the response after it was provided by the request handler, but before it was handed + +The `ServerTransport` and `ServerMiddleware` protocols look similar, however each serves a different purpose. + +A _transport_ abstracts over the underlying HTTP library that actually receives the HTTP requests from the network. An implemented _handler_ (a type implemented by you that conforms to the generated `APIProtocol` protocol) is generally configured with exactly one server transport. + +A _middleware_ intercepts the HTTP request and response, without being responsible for receiving the HTTP operations itself. That’s why middlewares take the extra `next` parameter, to delegate calling the handler to the transport at the top of the middleware stack. + +### Use an existing server middleware + +Instantiate the middleware using the parameters required by the specific implementation. For example, using a hypothetical existing middleware that logs every request and response: + +let loggingMiddleware = LoggingMiddleware() + +Similarly to the process of using an existing `ServerTransport`, provide the middleware to the call to register handlers: + +try handler.registerHandlers( +on: transport, +serverURL: URL(string: "/api/v1")!, +middlewares: [\ +loggingMiddleware,\ +] +) + +Then when an HTTP request is received, the server first invokes the middlewares in the order you provided them, and then passes the parsed request to your handler. When a response is received from the handler, the last middleware handles the response first, and it goes back in the reverse order of the `middlewares` array. At the end, the transport sends the final response + +If a server middleware implementation with your desired behavior doesn’t yet exist, or you need to simulate rare requests in your tests, consider implementing a custom server middleware. + +For example, an implementation a middleware that prints only basic information about the incoming request and outgoing response: + +/// A middleware that prints request and response metadata. +struct PrintingMiddleware: ServerMiddleware { +func intercept( +_ request: HTTPRequest, +body: HTTPBody?, +metadata: ServerRequestMetadata, +operationID: String, + +print(">>>: \(request.method.rawValue) \(request.soar_pathOnly)") +do { +let (response, responseBody) = try await next(request, body, metadata) +print("<<<: \(response.status.code)") +return (response, responseBody) +} catch { +print("!!!: \(error)") +throw error +} +} +} + +Implementing a test server middleware is just one way to help test your code that integrates with your handler. Another is to implement a type conforming to the generated protocol `APIProtocol`, and to implement a custom `ServerTransport`. + +## Topics + +### Instance Methods + +Intercepts an incoming HTTP request and an outgoing HTTP response. + +**Required** + +## Relationships + +### Inherits From + +- `Swift.Sendable` +- `Swift.SendableMetatype` + +### Conforming Types + +- `ErrorHandlingMiddleware` + +## See Also + +### Essentials + +`protocol ClientTransport` + +A type that performs HTTP operations. + +`protocol ServerTransport` + +A type that registers and handles HTTP operations. + +`protocol ClientMiddleware` + +- ServerMiddleware +- Overview +- Choose between a transport and a middleware +- Use an existing server middleware +- Implement a custom server middleware +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration + +- OpenAPIRuntime +- Configuration + +Structure + +# Configuration + +A set of configuration values used by the generated client and server types. + +struct Configuration + +Configuration.swift + +## Topics + +### Initializers + +`init(dateTranscoder: any DateTranscoder, jsonEncodingOptions: JSONEncodingOptions, multipartBoundaryGenerator: any MultipartBoundaryGenerator, xmlCoder: (any CustomCoder)?)` + +Creates a new configuration with the specified values. + +`init(dateTranscoder: any DateTranscoder, multipartBoundaryGenerator: any MultipartBoundaryGenerator)` + +Deprecated + +`init(dateTranscoder: any DateTranscoder, multipartBoundaryGenerator: any MultipartBoundaryGenerator, xmlCoder: (any CustomCoder)?)` + +### Instance Properties + +`var dateTranscoder: any DateTranscoder` + +The transcoder used when converting between date and string values. + +`var jsonEncodingOptions: JSONEncodingOptions` + +The options for the underlying JSON encoder. + +`var multipartBoundaryGenerator: any MultipartBoundaryGenerator` + +The generator to use when creating mutlipart bodies. + +`var xmlCoder: (any CustomCoder)?` + +Custom XML coder for encoding and decoding xml bodies. + +## Relationships + +### Conforms To + +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Customization + +`protocol DateTranscoder` + +A type that allows customization of Date encoding and decoding. + +`struct ISO8601DateTranscoder` + +A transcoder for dates encoded as an ISO-8601 string (in RFC 3339 format). + +`protocol MultipartBoundaryGenerator` + +A generator of a new boundary string used by multipart messages to separate parts. + +`struct RandomMultipartBoundaryGenerator` + +A generator that returns a boundary containg a constant prefix and a random suffix. + +`struct ConstantMultipartBoundaryGenerator` + +A generator that always returns the same constant boundary string. + +`enum IterationBehavior` + +Describes how many times the provided sequence can be iterated. + +- Configuration +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/datetranscoder + +- OpenAPIRuntime +- DateTranscoder + +Protocol + +# DateTranscoder + +A type that allows customization of Date encoding and decoding. + +protocol DateTranscoder : Sendable + +Configuration.swift + +## Overview + +See `ISO8601DateTranscoder`. + +## Topics + +### Instance Methods + +Decodes a `String` as a `Date`. + +**Required** + +Encodes the `Date` as a `String`. + +### Type Properties + +`static var iso8601: ISO8601DateTranscoder` + +A transcoder that transcodes dates as ISO-8601–formatted string (in RFC 3339 format). + +`static var iso8601WithFractionalSeconds: ISO8601DateTranscoder` + +A transcoder that transcodes dates as ISO-8601–formatted string (in RFC 3339 format) with fractional seconds. + +## Relationships + +### Inherits From + +- `Swift.Sendable` +- `Swift.SendableMetatype` + +### Conforming Types + +- `ISO8601DateTranscoder` + +## See Also + +### Customization + +`struct Configuration` + +A set of configuration values used by the generated client and server types. + +`struct ISO8601DateTranscoder` + +A transcoder for dates encoded as an ISO-8601 string (in RFC 3339 format). + +`protocol MultipartBoundaryGenerator` + +A generator of a new boundary string used by multipart messages to separate parts. + +`struct RandomMultipartBoundaryGenerator` + +A generator that returns a boundary containg a constant prefix and a random suffix. + +`struct ConstantMultipartBoundaryGenerator` + +A generator that always returns the same constant boundary string. + +`enum IterationBehavior` + +Describes how many times the provided sequence can be iterated. + +- DateTranscoder +- Overview +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder + +- OpenAPIRuntime +- ISO8601DateTranscoder + +Structure + +# ISO8601DateTranscoder + +A transcoder for dates encoded as an ISO-8601 string (in RFC 3339 format). + +struct ISO8601DateTranscoder + +Configuration.swift + +## Topics + +### Initializers + +`init(options: ISO8601DateFormatter.Options?)` + +Creates a new transcoder with the provided options. + +### Instance Methods + +Creates and returns a date object from the specified ISO 8601 formatted string representation. + +Creates and returns an ISO 8601 formatted string representation of the specified date. + +## Relationships + +### Conforms To + +- `DateTranscoder` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Customization + +`struct Configuration` + +A set of configuration values used by the generated client and server types. + +`protocol DateTranscoder` + +A type that allows customization of Date encoding and decoding. + +`protocol MultipartBoundaryGenerator` + +A generator of a new boundary string used by multipart messages to separate parts. + +`struct RandomMultipartBoundaryGenerator` + +A generator that returns a boundary containg a constant prefix and a random suffix. + +`struct ConstantMultipartBoundaryGenerator` + +A generator that always returns the same constant boundary string. + +`enum IterationBehavior` + +Describes how many times the provided sequence can be iterated. + +- ISO8601DateTranscoder +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartboundarygenerator + +- OpenAPIRuntime +- MultipartBoundaryGenerator + +Protocol + +# MultipartBoundaryGenerator + +A generator of a new boundary string used by multipart messages to separate parts. + +protocol MultipartBoundaryGenerator : Sendable + +MultipartBoundaryGenerator.swift + +## Topics + +### Instance Methods + +Generates a boundary string for a multipart message. + +**Required** + +### Type Properties + +`static var constant: ConstantMultipartBoundaryGenerator` + +A generator that always returns the same boundary string. + +`static var random: RandomMultipartBoundaryGenerator` + +A generator that produces a random boundary every time. + +## Relationships + +### Inherits From + +- `Swift.Sendable` +- `Swift.SendableMetatype` + +### Conforming Types + +- `ConstantMultipartBoundaryGenerator` +- `RandomMultipartBoundaryGenerator` + +## See Also + +### Customization + +`struct Configuration` + +A set of configuration values used by the generated client and server types. + +`protocol DateTranscoder` + +A type that allows customization of Date encoding and decoding. + +`struct ISO8601DateTranscoder` + +A transcoder for dates encoded as an ISO-8601 string (in RFC 3339 format). + +`struct RandomMultipartBoundaryGenerator` + +A generator that returns a boundary containg a constant prefix and a random suffix. + +`struct ConstantMultipartBoundaryGenerator` + +A generator that always returns the same constant boundary string. + +`enum IterationBehavior` + +Describes how many times the provided sequence can be iterated. + +- MultipartBoundaryGenerator +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator + +- OpenAPIRuntime +- RandomMultipartBoundaryGenerator + +Structure + +# RandomMultipartBoundaryGenerator + +A generator that returns a boundary containg a constant prefix and a random suffix. + +struct RandomMultipartBoundaryGenerator + +MultipartBoundaryGenerator.swift + +## Topics + +### Initializers + +`init(boundaryPrefix: String, randomNumberSuffixLength: Int)` + +Create a new generator. + +### Instance Properties + +`let boundaryPrefix: String` + +The constant prefix of each boundary. + +`let randomNumberSuffixLength: Int` + +The length, in bytes, of the random boundary suffix. + +### Instance Methods + +Generates a boundary string for a multipart message. + +## Relationships + +### Conforms To + +- `MultipartBoundaryGenerator` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Customization + +`struct Configuration` + +A set of configuration values used by the generated client and server types. + +`protocol DateTranscoder` + +A type that allows customization of Date encoding and decoding. + +`struct ISO8601DateTranscoder` + +A transcoder for dates encoded as an ISO-8601 string (in RFC 3339 format). + +`protocol MultipartBoundaryGenerator` + +A generator of a new boundary string used by multipart messages to separate parts. + +`struct ConstantMultipartBoundaryGenerator` + +A generator that always returns the same constant boundary string. + +`enum IterationBehavior` + +Describes how many times the provided sequence can be iterated. + +- RandomMultipartBoundaryGenerator +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/constantmultipartboundarygenerator + +- OpenAPIRuntime +- ConstantMultipartBoundaryGenerator + +Structure + +# ConstantMultipartBoundaryGenerator + +A generator that always returns the same constant boundary string. + +struct ConstantMultipartBoundaryGenerator + +MultipartBoundaryGenerator.swift + +## Topics + +### Initializers + +`init(boundary: String)` + +Creates a new generator. + +### Instance Properties + +`let boundary: String` + +The boundary string to return. + +### Instance Methods + +Generates a boundary string for a multipart message. + +## Relationships + +### Conforms To + +- `MultipartBoundaryGenerator` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Customization + +`struct Configuration` + +A set of configuration values used by the generated client and server types. + +`protocol DateTranscoder` + +A type that allows customization of Date encoding and decoding. + +`struct ISO8601DateTranscoder` + +A transcoder for dates encoded as an ISO-8601 string (in RFC 3339 format). + +`protocol MultipartBoundaryGenerator` + +A generator of a new boundary string used by multipart messages to separate parts. + +`struct RandomMultipartBoundaryGenerator` + +A generator that returns a boundary containg a constant prefix and a random suffix. + +`enum IterationBehavior` + +Describes how many times the provided sequence can be iterated. + +- ConstantMultipartBoundaryGenerator +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iterationbehavior + +- OpenAPIRuntime +- IterationBehavior + +Enumeration + +# IterationBehavior + +Describes how many times the provided sequence can be iterated. + +enum IterationBehavior + +AsyncSequenceCommon.swift + +## Topics + +### Enumeration Cases + +`case multiple` + +The input sequence can be iterated multiple times. + +`case single` + +The input sequence can only be iterated once. + +## Relationships + +### Conforms To + +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Customization + +`struct Configuration` + +A set of configuration values used by the generated client and server types. + +`protocol DateTranscoder` + +A type that allows customization of Date encoding and decoding. + +`struct ISO8601DateTranscoder` + +A transcoder for dates encoded as an ISO-8601 string (in RFC 3339 format). + +`protocol MultipartBoundaryGenerator` + +A generator of a new boundary string used by multipart messages to separate parts. + +`struct RandomMultipartBoundaryGenerator` + +A generator that returns a boundary containg a constant prefix and a random suffix. + +`struct ConstantMultipartBoundaryGenerator` + +A generator that always returns the same constant boundary string. + +- IterationBehavior +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/httpbody + +- OpenAPIRuntime +- HTTPBody + +Class + +# HTTPBody + +A body of an HTTP request or HTTP response. + +final class HTTPBody + +HTTPBody.swift + +## Overview + +Under the hood, it represents an async sequence of byte chunks. + +## Creating a body from a buffer + +Create an empty body: + +let body = HTTPBody() + +Create a body from a byte chunk: + +let body = HTTPBody(bytes) + +Create a body from `Foundation.Data`: + +let data: Foundation.Data = ... +let body = HTTPBody(data) + +Create a body from a string: + +let body = HTTPBody("Hello, world!") + +## Creating a body from an async sequence + +The body type also supports initialization from an async sequence. + +let producingSequence = ... // an AsyncSequence +let length: HTTPBody.Length = .known(1024) // or .unknown +let body = HTTPBody( +producingSequence, +length: length, +iterationBehavior: .single // or .multiple +) + +In addition to the async sequence, also provide the total body length, if known (this can be sent in the `content-length` header), and whether the sequence is safe to be iterated multiple times, or can only be iterated once. + +Sequences that can be iterated multiple times work better when an HTTP request needs to be retried, or if a redirect is encountered. + +In addition to providing the async sequence, you can also produce the body using an `AsyncStream` or `AsyncThrowingStream`: + +let body = HTTPBody( + +continuation.yield([72, 69]) +continuation.yield([76, 76, 79]) +continuation.finish() +}), +length: .known(5) +) + +## Consuming a body as an async sequence + +For example, to get another sequence that contains only the size of each chunk, and print each size, use: + +let chunkSizes = body.map { chunk in chunk.count } +for try await chunkSize in chunkSizes { +print("Chunk size: \(chunkSize)") +} + +## Consuming a body as a buffer + +If you need to collect the whole body before processing it, use one of the convenience initializers on the target types that take an `HTTPBody`. + +let buffer = try await ArraySlice(collecting: body, upTo: 2 * 1024 * 1024) + +The body type provides more variants of the collecting initializer on commonly used buffers, such as: + +- `Foundation.Data` + +- `Swift.String` + +## Topics + +### Structures + +`struct Iterator` + +An async iterator of both input async sequences and of the body itself. + +### Initializers + +`convenience init()` + +Creates a new empty body. + +`convenience init(some Sendable & StringProtocol)` + +Creates a new body with the provided string encoded as UTF-8 bytes. + +`convenience init(Data)` + +Creates a new body from the provided data chunk. + +Creates a new body with the provided byte collection. + +`convenience init(HTTPBody.ByteChunk)` + +Creates a new body with the provided byte chunk. + +[`convenience init([UInt8])`](https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/httpbody/init(_:)-9eeet) + +Creates a new body from the provided array of bytes. + +`convenience init(some Sendable & StringProtocol, length: HTTPBody.Length)` + +Creates a new body with the provided async throwing stream of strings. + +Creates a new body with the provided async stream. + +`convenience init(HTTPBody.ByteChunk, length: HTTPBody.Length)` + +Creates a new body with the provided async stream of strings. + +Creates a new body with the provided async throwing stream. + +Creates a new body with the provided async sequence of byte sequences. + +Creates a new body with the provided async sequence of string chunks. + +Creates a new body with the provided byte sequence. + +Creates a new body with the provided async sequence. + +### Instance Properties + +`let iterationBehavior: IterationBehavior` + +The iteration behavior, which controls how many times the input sequence can be iterated. + +`let length: HTTPBody.Length` + +The total length of the body, in bytes, if known. + +### Type Aliases + +`typealias ByteChunk` + +The underlying byte chunk type. + +### Enumerations + +`enum Length` + +Describes the total length of the body, in bytes, if known. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Equatable` +- `Swift.ExpressibleByArrayLiteral` +- `Swift.ExpressibleByExtendedGraphemeClusterLiteral` +- `Swift.ExpressibleByStringLiteral` +- `Swift.ExpressibleByUnicodeScalarLiteral` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` +- `_Concurrency.AsyncSequence` + +## See Also + +### Content types + +`struct Base64EncodedData` + +A type for converting data as a base64 string. + +`class MultipartBody` + +The body of multipart requests and responses. + +`struct MultipartRawPart` + +A raw multipart part containing the header fields and the body stream. + +`struct MultipartPart` + +A wrapper of a typed part with a statically known name that adds other dynamic `content-disposition` parameter values, such as `filename`. + +`struct MultipartDynamicallyNamedPart` + +A wrapper of a typed part without a statically known name that adds dynamic `content-disposition` parameter values, such as `name` and `filename`. + +- HTTPBody +- Overview +- Creating a body from a buffer +- Creating a body from an async sequence +- Consuming a body as an async sequence +- Consuming a body as a buffer +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/base64encodeddata + +- OpenAPIRuntime +- Base64EncodedData + +Structure + +# Base64EncodedData + +A type for converting data as a base64 string. + +struct Base64EncodedData + +Base64EncodedData.swift + +## Overview + +This type holds raw, unencoded, data as a slice of bytes. It can be used to encode that data to a provided `Encoder` as base64-encoded data or to decode from base64 encoding when initialized from a decoder. + +There is a convenience initializer to create an instance backed by provided data in the form of a slice of bytes: + +let base64EncodedData = Base64EncodedData(data: bytes) + +To decode base64-encoded data it is possible to call the initializer directly, providing a decoder: + +let base64EncodedData = Base64EncodedData(from: decoder) + +However more commonly the decoding initializer would be called by a decoder, for example: + +let encodedData: Data = ... +let decoded = try JSONDecoder().decode(Base64EncodedData.self, from: encodedData) + +Once an instance is holding data, it may be base64 encoded to a provided encoder: + +let base64EncodedData = Base64EncodedData(data: bytes) +base64EncodedData.encode(to: encoder) + +However more commonly it would be called by an encoder, for example: + +let encodedData = JSONEncoder().encode(encodedBytes) + +## Topics + +### Initializers + +Initializes an instance of `Base64EncodedData` wrapping the provided slice of bytes. + +Initializes an instance of `Base64EncodedData` wrapping the provided sequence of bytes. + +### Instance Properties + +A container of the raw bytes. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Decodable` +- `Swift.Encodable` +- `Swift.Equatable` +- `Swift.ExpressibleByArrayLiteral` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Content types + +`class HTTPBody` + +A body of an HTTP request or HTTP response. + +`class MultipartBody` + +The body of multipart requests and responses. + +`struct MultipartRawPart` + +A raw multipart part containing the header fields and the body stream. + +`struct MultipartPart` + +A wrapper of a typed part with a statically known name that adds other dynamic `content-disposition` parameter values, such as `filename`. + +`struct MultipartDynamicallyNamedPart` + +A wrapper of a typed part without a statically known name that adds dynamic `content-disposition` parameter values, such as `name` and `filename`. + +- Base64EncodedData +- Overview +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody + +- OpenAPIRuntime +- MultipartBody + +Class + +# MultipartBody + +The body of multipart requests and responses. + +MultipartPublicTypes.swift + +## Overview + +`MultipartBody` represents an async sequence of multipart parts of a specific type. + +The `Part` generic type parameter is usually a generated enum representing the different values documented for this multipart body. + +## Creating a body from buffered parts + +Create a body from an array of values of type `Part`: + +.myCaseA(...),\ +.myCaseB(...),\ +] + +## Creating a body from an async sequence of parts + +The body type also supports initialization from an async sequence. + +let producingSequence = ... // an AsyncSequence of MyPartType +let body = MultipartBody( +producingSequence, +iterationBehavior: .single // or .multiple +) + +In addition to the async sequence, also specify whether the sequence is safe to be iterated multiple times, or can only be iterated once. + +Sequences that can be iterated multiple times work better when an HTTP request needs to be retried, or if a redirect is encountered. + +In addition to providing the async sequence, you can also produce the body using an `AsyncStream` or `AsyncThrowingStream`: + +let (stream, continuation) = AsyncStream.makeStream(of: MyPartType.self) +// Pass the continuation to another task that produces the parts asynchronously. +Task { +continuation.yield(.myCaseA(...)) +// ... later +continuation.yield(.myCaseB(...)) +continuation.finish() +} +let body = MultipartBody(stream) + +## Consuming a body as an async sequence + +The `MultipartBody` type conforms to `AsyncSequence` and uses a generic element type, so it can be consumed in a streaming fashion, without ever buffering the whole body in your process. + +for try await part in multipartBody { +switch part { +case .myCaseA(let myCaseAValue): +// Handle myCaseAValue. +case .myCaseB(let myCaseBValue): +// Handle myCaseBValue, which is a raw type with a streaming part body. +// +// Option 1: Process the part body bytes in chunks. +for try await bodyChunk in myCaseBValue.body { +// Handle bodyChunk. +} +// Option 2: Accumulate the body into a byte array. +// (For other convenience initializers, check out ``HTTPBody``. +let fullPartBody = try await UInt8 +// ... +} +} + +Multipart parts of different names can arrive in any order, and the order is not significant. + +Consuming the multipart body should be resilient to parts of different names being reordered. + +However, multiple parts of the same name, if allowed by the OpenAPI document by defining it as an array, should be treated as an ordered array of values, and those cannot be reordered without changing the message’s meaning. + +## Topics + +### Structures + +`struct Iterator` + +An async iterator of both input async sequences and of the sequence itself. + +### Initializers + +Creates a new sequence with the provided async throwing stream. + +Creates a new sequence with the provided collection of parts. + +Creates a new sequence with the provided async stream. + +Creates a new sequence with the provided async sequence of parts. + +### Instance Properties + +`let iterationBehavior: IterationBehavior` + +The iteration behavior, which controls how many times the input sequence can be iterated. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Equatable` +- `Swift.ExpressibleByArrayLiteral` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` +- `_Concurrency.AsyncSequence` + +## See Also + +### Content types + +`class HTTPBody` + +A body of an HTTP request or HTTP response. + +`struct Base64EncodedData` + +A type for converting data as a base64 string. + +`struct MultipartRawPart` + +A raw multipart part containing the header fields and the body stream. + +`struct MultipartPart` + +A wrapper of a typed part with a statically known name that adds other dynamic `content-disposition` parameter values, such as `filename`. + +`struct MultipartDynamicallyNamedPart` + +A wrapper of a typed part without a statically known name that adds dynamic `content-disposition` parameter values, such as `name` and `filename`. + +- MultipartBody +- Overview +- Creating a body from buffered parts +- Creating a body from an async sequence of parts +- Consuming a body as an async sequence +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartrawpart + +- OpenAPIRuntime +- MultipartRawPart + +Structure + +# MultipartRawPart + +A raw multipart part containing the header fields and the body stream. + +struct MultipartRawPart + +MultipartPublicTypes.swift + +## Topics + +### Initializers + +`init(headerFields: HTTPFields, body: HTTPBody)` + +Creates a new part. + +`init(name: String?, filename: String?, headerFields: HTTPFields, body: HTTPBody)` + +Creates a new raw part by injecting the provided name and filename into the `content-disposition` header field. + +### Instance Properties + +`var body: HTTPBody` + +The body stream of this part. + +`var filename: String?` + +The file name of the part stored in the `content-disposition` header field. + +`var headerFields: HTTPFields` + +The header fields contained in this part, such as `content-disposition`. + +`var name: String?` + +The name of the part stored in the `content-disposition` header field. + +## Relationships + +### Conforms To + +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Content types + +`class HTTPBody` + +A body of an HTTP request or HTTP response. + +`struct Base64EncodedData` + +A type for converting data as a base64 string. + +`class MultipartBody` + +The body of multipart requests and responses. + +`struct MultipartPart` + +A wrapper of a typed part with a statically known name that adds other dynamic `content-disposition` parameter values, such as `filename`. + +`struct MultipartDynamicallyNamedPart` + +A wrapper of a typed part without a statically known name that adds dynamic `content-disposition` parameter values, such as `name` and `filename`. + +- MultipartRawPart +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartpart + +- OpenAPIRuntime +- MultipartPart + +Structure + +# MultipartPart + +A wrapper of a typed part with a statically known name that adds other dynamic `content-disposition` parameter values, such as `filename`. + +MultipartPublicTypes.swift + +## Topics + +### Initializers + +`init(payload: Payload, filename: String?)` + +Creates a new wrapper. + +### Instance Properties + +`var filename: String?` + +A file name parameter provided in the `content-disposition` part header field. + +`var payload: Payload` + +The underlying typed part payload, which has a statically known part name. + +## Relationships + +### Conforms To + +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Content types + +`class HTTPBody` + +A body of an HTTP request or HTTP response. + +`struct Base64EncodedData` + +A type for converting data as a base64 string. + +`class MultipartBody` + +The body of multipart requests and responses. + +`struct MultipartRawPart` + +A raw multipart part containing the header fields and the body stream. + +`struct MultipartDynamicallyNamedPart` + +A wrapper of a typed part without a statically known name that adds dynamic `content-disposition` parameter values, such as `name` and `filename`. + +- MultipartPart +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartdynamicallynamedpart + +- OpenAPIRuntime +- MultipartDynamicallyNamedPart + +Structure + +# MultipartDynamicallyNamedPart + +A wrapper of a typed part without a statically known name that adds dynamic `content-disposition` parameter values, such as `name` and `filename`. + +MultipartPublicTypes.swift + +## Topics + +### Initializers + +`init(payload: Payload, filename: String?, name: String?)` + +Creates a new wrapper. + +### Instance Properties + +`var filename: String?` + +A file name parameter provided in the `content-disposition` part header field. + +`var name: String?` + +A name parameter provided in the `content-disposition` part header field. + +`var payload: Payload` + +The underlying typed part payload, which has a statically known part name. + +## Relationships + +### Conforms To + +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Content types + +`class HTTPBody` + +A body of an HTTP request or HTTP response. + +`struct Base64EncodedData` + +A type for converting data as a base64 string. + +`class MultipartBody` + +The body of multipart requests and responses. + +`struct MultipartRawPart` + +A raw multipart part containing the header fields and the body stream. + +`struct MultipartPart` + +A wrapper of a typed part with a statically known name that adds other dynamic `content-disposition` parameter values, such as `filename`. + +- MultipartDynamicallyNamedPart +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clienterror + +- OpenAPIRuntime +- ClientError + +Structure + +# ClientError + +An error thrown by a client performing an OpenAPI operation. + +struct ClientError + +ClientError.swift + +## Overview + +Use a `ClientError` to inspect details about the request and response that resulted in an error. + +You don’t create or throw instances of `ClientError` yourself; they are created and thrown on your behalf by the runtime library when a client operation fails. + +## Topics + +### Initializers + +`init(operationID: String, operationInput: any Sendable, request: HTTPRequest?, requestBody: HTTPBody?, baseURL: URL?, response: HTTPResponse?, responseBody: HTTPBody?, causeDescription: String, underlyingError: any Error)` + +Creates a new error. + +### Instance Properties + +`var baseURL: URL?` + +The base URL for HTTP requests. + +`var causeDescription: String` + +A user-facing description of what caused the underlying error to be thrown. + +`var operationID: String` + +The identifier of the operation, as defined in the OpenAPI document. + +`var operationInput: any Sendable` + +The operation-specific Input value. + +`var request: HTTPRequest?` + +The HTTP request created during the operation. + +`var requestBody: HTTPBody?` + +The HTTP request body created during the operation. + +`var response: HTTPResponse?` + +The HTTP response received during the operation. + +`var responseBody: HTTPBody?` + +The HTTP response body received during the operation. + +`var underlyingError: any Error` + +The underlying error that caused the operation to fail. + +## Relationships + +### Conforms To + +- `Foundation.LocalizedError` +- `Swift.Copyable` +- `Swift.CustomStringConvertible` +- `Swift.Error` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Errors + +`struct ServerError` + +An error thrown by a server handling an OpenAPI operation. + +`struct UndocumentedPayload` + +A payload value used by undocumented operation responses. + +- ClientError +- Overview +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servererror + +- OpenAPIRuntime +- ServerError + +Structure + +# ServerError + +An error thrown by a server handling an OpenAPI operation. + +struct ServerError + +ServerError.swift + +## Topics + +### Initializers + +`init(operationID: String, request: HTTPRequest, requestBody: HTTPBody?, requestMetadata: ServerRequestMetadata, operationInput: (any Sendable)?, operationOutput: (any Sendable)?, causeDescription: String, underlyingError: any Error)` + +Creates a new error. + +`init(operationID: String, request: HTTPRequest, requestBody: HTTPBody?, requestMetadata: ServerRequestMetadata, operationInput: (any Sendable)?, operationOutput: (any Sendable)?, causeDescription: String, underlyingError: any Error, httpStatus: HTTPResponse.Status, httpHeaderFields: HTTPFields, httpBody: HTTPBody?)` + +### Instance Properties + +`var causeDescription: String` + +A user-facing description of what caused the underlying error to be thrown. + +`var httpBody: HTTPBody?` + +The body of the HTTP response. + +`var httpHeaderFields: HTTPFields` + +The HTTP header fields of the response. + +`var httpStatus: HTTPResponse.Status` + +An HTTP status to return in the response. + +`var operationID: String` + +Identifier of the operation that threw the error. + +`var operationInput: (any Sendable)?` + +An operation-specific Input value. + +`var operationOutput: (any Sendable)?` + +An operation-specific Output value. + +`var request: HTTPRequest` + +The HTTP request provided to the server. + +`var requestBody: HTTPBody?` + +The HTTP request body provided to the server. + +`var requestMetadata: ServerRequestMetadata` + +The request metadata extracted by the server. + +`var underlyingError: any Error` + +The underlying error that caused the operation to fail. + +## Relationships + +### Conforms To + +- `Foundation.LocalizedError` +- `HTTPResponseConvertible` +- `Swift.Copyable` +- `Swift.CustomStringConvertible` +- `Swift.Error` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Errors + +`struct ClientError` + +An error thrown by a client performing an OpenAPI operation. + +`struct UndocumentedPayload` + +A payload value used by undocumented operation responses. + +- ServerError +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/undocumentedpayload + +- OpenAPIRuntime +- UndocumentedPayload + +Structure + +# UndocumentedPayload + +A payload value used by undocumented operation responses. + +struct UndocumentedPayload + +UndocumentedPayload.swift + +## Overview + +Each operation’s `Output` enum type needs to exhaustively cover all the possible HTTP response status codes, so when not all are defined by the user in the OpenAPI document, an extra `undocumented` enum case is used when such a status code is detected. + +## Topics + +### Initializers + +`init()` + +Creates a new payload. + +Deprecated + +`init(headerFields: HTTPFields, body: HTTPBody?)` + +Creates a new part. + +### Instance Properties + +`var body: HTTPBody?` + +The body stream of this part, if present. + +`var headerFields: HTTPFields` + +The header fields contained in the response. + +## Relationships + +### Conforms To + +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Errors + +`struct ClientError` + +An error thrown by a client performing an OpenAPI operation. + +`struct ServerError` + +An error thrown by a server handling an OpenAPI operation. + +- UndocumentedPayload +- Overview +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serverrequestmetadata + +- OpenAPIRuntime +- ServerRequestMetadata + +Structure + +# ServerRequestMetadata + +A container for request metadata already parsed and validated by the server transport. + +struct ServerRequestMetadata + +CurrencyTypes.swift + +## Topics + +### Initializers + +[`init(pathParameters: [String : Substring])`](https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serverrequestmetadata/init(pathparameters:)) + +Creates a new metadata wrapper with the specified path and query parameters. + +### Instance Properties + +[`var pathParameters: [String : Substring]`](https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serverrequestmetadata/pathparameters) + +The path parameters parsed from the URL of the HTTP request. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.CustomStringConvertible` +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### HTTP Currency Types + +`class HTTPBody` + +A body of an HTTP request or HTTP response. + +`protocol AcceptableProtocol` + +The protocol that all generated `AcceptableContentType` enums conform to. + +`struct AcceptHeaderContentType` + +A wrapper of an individual content type in the accept header. + +`struct QualityValue` + +A quality value used to describe the order of priority in a comma-separated list of values, such as in the Accept header. + +- ServerRequestMetadata +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/acceptableprotocol + +- OpenAPIRuntime +- AcceptableProtocol + +Protocol + +# AcceptableProtocol + +The protocol that all generated `AcceptableContentType` enums conform to. + +protocol AcceptableProtocol : CaseIterable, Hashable, RawRepresentable, Sendable where Self.RawValue == String + +Acceptable.swift + +## Relationships + +### Inherits From + +- `Swift.CaseIterable` +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.RawRepresentable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### HTTP Currency Types + +`class HTTPBody` + +A body of an HTTP request or HTTP response. + +`struct ServerRequestMetadata` + +A container for request metadata already parsed and validated by the server transport. + +`struct AcceptHeaderContentType` + +A wrapper of an individual content type in the accept header. + +`struct QualityValue` + +A quality value used to describe the order of priority in a comma-separated list of values, such as in the Accept header. + +- AcceptableProtocol +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/acceptheadercontenttype + +- OpenAPIRuntime +- AcceptHeaderContentType + +Structure + +# AcceptHeaderContentType + +A wrapper of an individual content type in the accept header. + +Acceptable.swift + +## Topics + +### Initializers + +`init(contentType: ContentType, quality: QualityValue)` + +Creates a new content type from the provided parameters. + +### Instance Properties + +`var contentType: ContentType` + +The value representing the content type. + +`var quality: QualityValue` + +The quality value of this content type. + +### Type Properties + +Returns the default set of acceptable content types for this type, in the order specified in the OpenAPI document. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.RawRepresentable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### HTTP Currency Types + +`class HTTPBody` + +A body of an HTTP request or HTTP response. + +`struct ServerRequestMetadata` + +A container for request metadata already parsed and validated by the server transport. + +`protocol AcceptableProtocol` + +The protocol that all generated `AcceptableContentType` enums conform to. + +`struct QualityValue` + +A quality value used to describe the order of priority in a comma-separated list of values, such as in the Accept header. + +- AcceptHeaderContentType +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/qualityvalue + +- OpenAPIRuntime +- QualityValue + +Structure + +# QualityValue + +A quality value used to describe the order of priority in a comma-separated list of values, such as in the Accept header. + +struct QualityValue + +Acceptable.swift + +## Topics + +### Initializers + +`init(doubleValue: Double)` + +Creates a new quality value from the provided floating-point number. + +### Instance Properties + +`var doubleValue: Double` + +The value represented as a floating-point number between 0.0 and 1.0, inclusive. + +`var isDefault: Bool` + +Returns a Boolean value indicating whether the quality value is at its default value 1.0. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Equatable` +- `Swift.ExpressibleByFloatLiteral` +- `Swift.ExpressibleByIntegerLiteral` +- `Swift.Hashable` +- `Swift.RawRepresentable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### HTTP Currency Types + +`class HTTPBody` + +A body of an HTTP request or HTTP response. + +`struct ServerRequestMetadata` + +A container for request metadata already parsed and validated by the server transport. + +`protocol AcceptableProtocol` + +The protocol that all generated `AcceptableContentType` enums conform to. + +`struct AcceptHeaderContentType` + +A wrapper of an individual content type in the accept header. + +- QualityValue +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/openapivaluecontainer + +- OpenAPIRuntime +- OpenAPIValueContainer + +Structure + +# OpenAPIValueContainer + +A container for a value represented by JSON Schema. + +struct OpenAPIValueContainer + +OpenAPIValue.swift + +## Overview + +Contains an untyped JSON value. In some cases, the structure of the data may not be known in advance and must be dynamically iterated at decoding time. This is an advanced feature that requires extra validation of the input before use, and is at a higher risk of a security vulnerability. + +Supported nested Swift types: + +- `nil` + +- `String` + +- `Int` + +- `Double` + +- `Bool` + +- `[Any?]` + +- `[String: Any?]` + +Where the element type of the array, and the value type of the dictionary must also be supported types. + +## Topics + +### Operators + +Compares two `OpenAPIValueContainer` instances for equality. + +### Initializers + +`init(from: any Decoder) throws` + +Initializes an `OpenAPIValueContainer` by decoding it from a decoder. + +`init(unvalidatedValue: (any Sendable)?) throws` + +Creates a new container with the given unvalidated value. + +### Instance Properties + +`var value: (any Sendable)?` + +The underlying dynamic value. + +### Instance Methods + +`func encode(to: any Encoder) throws` + +Encodes the `OpenAPIValueContainer` and writes it to an encoder. + +`func hash(into: inout Hasher)` + +Hashes the `OpenAPIValueContainer` instance into a hasher. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Decodable` +- `Swift.Encodable` +- `Swift.Equatable` +- `Swift.ExpressibleByBooleanLiteral` +- `Swift.ExpressibleByExtendedGraphemeClusterLiteral` +- `Swift.ExpressibleByFloatLiteral` +- `Swift.ExpressibleByIntegerLiteral` +- `Swift.ExpressibleByNilLiteral` +- `Swift.ExpressibleByStringLiteral` +- `Swift.ExpressibleByUnicodeScalarLiteral` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Dynamic Payloads + +`struct OpenAPIObjectContainer` + +A container for a dictionary with values represented by JSON Schema. + +`struct OpenAPIArrayContainer` + +A container for an array with values represented by JSON Schema. + +- OpenAPIValueContainer +- Overview +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/openapiobjectcontainer + +- OpenAPIRuntime +- OpenAPIObjectContainer + +Structure + +# OpenAPIObjectContainer + +A container for a dictionary with values represented by JSON Schema. + +struct OpenAPIObjectContainer + +OpenAPIValue.swift + +## Overview + +Contains a dictionary of untyped JSON values. In some cases, the structure of the data may not be known in advance and must be dynamically iterated at decoding time. This is an advanced feature that requires extra validation of the input before use, and is at a higher risk of a security vulnerability. + +Supported nested Swift types: + +- `nil` + +- `String` + +- `Int` + +- `Double` + +- `Bool` + +- `[Any?]` + +- `[String: Any?]` + +Where the element type of the array, and the value type of the dictionary must also be supported types. + +## Topics + +### Initializers + +`init()` + +Creates a new empty container. + +`init(from: any Decoder) throws` + +[`init(unvalidatedValue: [String : (any Sendable)?]) throws`](https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/openapiobjectcontainer/init(unvalidatedvalue:)) + +Creates a new container with the given unvalidated value. + +### Instance Properties + +[`var value: [String : (any Sendable)?]`](https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/openapiobjectcontainer/value) + +The underlying dynamic dictionary value. + +### Instance Methods + +`func encode(to: any Encoder) throws` + +`func hash(into: inout Hasher)` + +## Relationships + +### Conforms To + +- `Swift.Decodable` +- `Swift.Encodable` +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Dynamic Payloads + +`struct OpenAPIValueContainer` + +A container for a value represented by JSON Schema. + +`struct OpenAPIArrayContainer` + +A container for an array with values represented by JSON Schema. + +- OpenAPIObjectContainer +- Overview +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/openapiarraycontainer + +- OpenAPIRuntime +- OpenAPIArrayContainer + +Structure + +# OpenAPIArrayContainer + +A container for an array with values represented by JSON Schema. + +struct OpenAPIArrayContainer + +OpenAPIValue.swift + +## Overview + +Contains an array of untyped JSON values. In some cases, the structure of the data may not be known in advance and must be dynamically iterated at decoding time. This is an advanced feature that requires extra validation of the input before use, and is at a higher risk of a security vulnerability. + +Supported nested Swift types: + +- `nil` + +- `String` + +- `Int` + +- `Double` + +- `Bool` + +- `[Any?]` + +- `[String: Any?]` + +Where the element type of the array, and the value type of the dictionary must also be supported types. + +## Topics + +### Operators + +Compares two `OpenAPIArrayContainer` instances for equality. + +### Initializers + +`init()` + +Creates a new empty container. + +`init(from: any Decoder) throws` + +Initializes a new instance by decoding a validated array of values from a decoder. + +[`init(unvalidatedValue: [(any Sendable)?]) throws`](https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/openapiarraycontainer/init(unvalidatedvalue:)) + +Creates a new container with the given unvalidated value. + +### Instance Properties + +[`var value: [(any Sendable)?]`](https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/openapiarraycontainer/value) + +The underlying dynamic array value. + +### Instance Methods + +`func encode(to: any Encoder) throws` + +Encodes the array of validated values and stores the result in the given encoder. + +`func hash(into: inout Hasher)` + +Hashes the `OpenAPIArrayContainer` instance into a hasher. + +## Relationships + +### Conforms To + +- `Swift.Decodable` +- `Swift.Encodable` +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +## See Also + +### Dynamic Payloads + +`struct OpenAPIValueContainer` + +A container for a value represented by JSON Schema. + +`struct OpenAPIObjectContainer` + +A container for a dictionary with values represented by JSON Schema. + +- OpenAPIArrayContainer +- Overview +- Topics +- Relationships +- See Also + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/customcoder + +- OpenAPIRuntime +- CustomCoder + +Protocol + +# CustomCoder + +A type that allows custom content type encoding and decoding. + +protocol CustomCoder : Sendable + +Configuration.swift + +## Topics + +### Instance Methods + +Decodes a value of the given type from the given custom representation. + +**Required** + +Encodes the given value and returns its custom encoded representation. + +## Relationships + +### Inherits From + +- `Swift.Sendable` +- `Swift.SendableMetatype` + +- CustomCoder +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/httpresponseconvertible + +- OpenAPIRuntime +- HTTPResponseConvertible + +Protocol + +# HTTPResponseConvertible + +A value that can be converted to an HTTP response and body. + +protocol HTTPResponseConvertible + +ErrorHandlingMiddleware.swift + +## Overview + +Conform your error type to this protocol to convert it to an `HTTPResponse` and `HTTPBody`. + +Used by `ErrorHandlingMiddleware`. + +## Topics + +### Instance Properties + +`var httpBody: HTTPBody?` + +The body of the HTTP response. + +**Required** Default implementation provided. + +`var httpHeaderFields: HTTPFields` + +The HTTP header fields of the response. This is optional as default values are provided in the extension. + +`var httpStatus: HTTPResponse.Status` + +An HTTP status to return in the response. + +**Required** + +## Relationships + +### Conforming Types + +- `ServerError` + +- HTTPResponseConvertible +- Overview +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/errorhandlingmiddleware + + + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/jsonencodingoptions + +- OpenAPIRuntime +- JSONEncodingOptions + +Structure + +# JSONEncodingOptions + +The options that control the encoded JSON data. + +struct JSONEncodingOptions + +Configuration.swift + +## Topics + +### Initializers + +`init(rawValue: UInt)` + +Creates a JSONEncodingOptions value with the given raw value. + +### Instance Properties + +`let rawValue: UInt` + +The format’s default value. + +### Type Properties + +`static let prettyPrinted: JSONEncodingOptions` + +Include newlines and indentation to make the output more human-readable. + +`static let sortedKeys: JSONEncodingOptions` + +Serialize JSON objects with field keys sorted in lexicographic order. + +`static let withoutEscapingSlashes: JSONEncodingOptions` + +Omit escaping forward slashes with backslashes. + +## Relationships + +### Conforms To + +- `Swift.Equatable` +- `Swift.ExpressibleByArrayLiteral` +- `Swift.OptionSet` +- `Swift.RawRepresentable` +- `Swift.Sendable` +- `Swift.SendableMetatype` +- `Swift.SetAlgebra` + +- JSONEncodingOptions +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/jsonlinesdeserializationsequence + +- OpenAPIRuntime +- JSONLinesDeserializationSequence + +Structure + +# JSONLinesDeserializationSequence + +A sequence that parses arbitrary byte chunks into lines using the JSON Lines format. + +JSONLinesDecoding.swift + +## Topics + +### Structures + +`struct Iterator` + +The iterator of `JSONLinesDeserializationSequence`. + +### Initializers + +`init(upstream: Upstream)` + +Creates a new sequence. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Sendable` +- `Swift.SendableMetatype` +- `_Concurrency.AsyncSequence` + +- JSONLinesDeserializationSequence +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/jsonlinesserializationsequence + +- OpenAPIRuntime +- JSONLinesSerializationSequence + +Structure + +# JSONLinesSerializationSequence + +A sequence that serializes lines by concatenating them using the JSON Lines format. + +JSONLinesEncoding.swift + +## Topics + +### Structures + +`struct Iterator` + +The iterator of `JSONLinesSerializationSequence`. + +### Initializers + +`init(upstream: Upstream)` + +Creates a new sequence. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Sendable` +- `Swift.SendableMetatype` +- `_Concurrency.AsyncSequence` + +- JSONLinesSerializationSequence +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/jsonsequencedeserializationsequence + +- OpenAPIRuntime +- JSONSequenceDeserializationSequence + +Structure + +# JSONSequenceDeserializationSequence + +A sequence that parses arbitrary byte chunks into lines using the JSON Sequence format. + +JSONSequenceDecoding.swift + +## Topics + +### Structures + +`struct Iterator` + +The iterator of `JSONSequenceDeserializationSequence`. + +### Initializers + +`init(upstream: Upstream)` + +Creates a new sequence. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Sendable` +- `Swift.SendableMetatype` +- `_Concurrency.AsyncSequence` + +- JSONSequenceDeserializationSequence +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/jsonsequenceserializationsequence + +- OpenAPIRuntime +- JSONSequenceSerializationSequence + +Structure + +# JSONSequenceSerializationSequence + +A sequence that serializes lines by concatenating them using the JSON Sequence format. + +JSONSequenceEncoding.swift + +## Topics + +### Structures + +`struct Iterator` + +The iterator of `JSONSequenceSerializationSequence`. + +### Initializers + +`init(upstream: Upstream)` + +Creates a new sequence. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Sendable` +- `Swift.SendableMetatype` +- `_Concurrency.AsyncSequence` + +- JSONSequenceSerializationSequence +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serversentevent + +- OpenAPIRuntime +- ServerSentEvent + +Structure + +# ServerSentEvent + +An event sent by the server. + +struct ServerSentEvent + +ServerSentEvents.swift + +## Overview + +Https://Html.Spec.Whatwg.Org/Multipage/Server-Sent-Events.Html#Event-Stream-Interpretation + +## Topics + +### Initializers + +`init(id: String?, event: String?, data: String?, retry: Int64?)` + +Creates a new event. + +### Instance Properties + +`var data: String?` + +The payload of the event. + +`var event: String?` + +A type of the event, helps inform how to interpret the data. + +`var id: String?` + +A unique identifier of the event, can be used to resume an interrupted stream by making a new request with the `Last-Event-ID` header field set to this value. + +`var retry: Int64?` + +The amount of time, in milliseconds, the client should wait before reconnecting in case of an interruption. + +## Relationships + +### Conforms To + +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +- ServerSentEvent +- Overview +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serversenteventwithjsondata + +- OpenAPIRuntime +- ServerSentEventWithJSONData + +Structure + +# ServerSentEventWithJSONData + +An event sent by the server that has a JSON payload in the data field. + +ServerSentEvents.swift + +## Overview + +Https://Html.Spec.Whatwg.Org/Multipage/Server-Sent-Events.Html#Event-Stream-Interpretation + +## Topics + +### Initializers + +`init(event: String?, data: JSONDataType?, id: String?, retry: Int64?)` + +Creates a new event. + +### Instance Properties + +`var data: JSONDataType?` + +The payload of the event. + +`var event: String?` + +A type of the event, helps inform how to interpret the data. + +`var id: String?` + +A unique identifier of the event, can be used to resume an interrupted stream by making a new request with the `Last-Event-ID` header field set to this value. + +`var retry: Int64?` + +The amount of time, in milliseconds, the client should wait before reconnecting in case of an interruption. + +## Relationships + +### Conforms To + +- `Swift.Equatable` +- `Swift.Hashable` +- `Swift.Sendable` +- `Swift.SendableMetatype` + +- ServerSentEventWithJSONData +- Overview +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serversenteventsdeserializationsequence + +- OpenAPIRuntime +- ServerSentEventsDeserializationSequence + +Structure + +# ServerSentEventsDeserializationSequence + +A sequence that parses arbitrary byte chunks into events using the Server-sent Events format. + +ServerSentEventsDecoding.swift + +## Overview + +Https://Html.Spec.Whatwg.Org/Multipage/Server-Sent-Events.Html#Server-Sent-Events + +## Topics + +### Structures + +`struct Iterator` + +The iterator of `ServerSentEventsDeserializationSequence`. + +### Initializers + +`init(upstream: Upstream)` + +Creates a new sequence. + +Deprecated + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Sendable` +- `Swift.SendableMetatype` +- `_Concurrency.AsyncSequence` + +- ServerSentEventsDeserializationSequence +- Overview +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serversenteventslinedeserializationsequence + +- OpenAPIRuntime +- ServerSentEventsLineDeserializationSequence + +Structure + +# ServerSentEventsLineDeserializationSequence + +A sequence that parses arbitrary byte chunks into lines using the Server-sent Events format. + +ServerSentEventsDecoding.swift + +## Topics + +### Structures + +`struct Iterator` + +The iterator of `ServerSentEventsLineDeserializationSequence`. + +### Initializers + +`init(upstream: Upstream)` + +Creates a new sequence. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Sendable` +- `Swift.SendableMetatype` +- `_Concurrency.AsyncSequence` + +- ServerSentEventsLineDeserializationSequence +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serversenteventsserializationsequence + +- OpenAPIRuntime +- ServerSentEventsSerializationSequence + +Structure + +# ServerSentEventsSerializationSequence + +A sequence that serializes Server-sent Events. + +ServerSentEventsEncoding.swift + +## Topics + +### Structures + +`struct Iterator` + +The iterator of `ServerSentEventsSerializationSequence`. + +### Initializers + +`init(upstream: Upstream)` + +Creates a new sequence. + +## Relationships + +### Conforms To + +- `Swift.Copyable` +- `Swift.Sendable` +- `Swift.SendableMetatype` +- `_Concurrency.AsyncSequence` + +- ServerSentEventsSerializationSequence +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/foundation + +- OpenAPIRuntime +- Foundation + +Extended Module + +# Foundation + +## Topics + +### Extended Structures + +`extension Data` + +`extension URL` + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/swift + +- OpenAPIRuntime +- Swift + +Extended Module + +# Swift + +## Topics + +### Extended Structures + +`extension Array` + +`extension ArraySlice` + +`extension String` + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/_concurrency + +- OpenAPIRuntime +- \_Concurrency + +Extended Module + +# \_Concurrency + +## Topics + +### Extended Protocols + +`extension AsyncSequence` + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clienttransport), + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servertransport), + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clientmiddleware), + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servermiddleware). + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clienttransport) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clientmiddleware) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servertransport) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servermiddleware) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/datetranscoder) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartboundarygenerator) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/constantmultipartboundarygenerator) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iterationbehavior) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/httpbody) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/base64encodeddata) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartrawpart) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartpart) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartdynamicallynamedpart) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clienterror) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servererror) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/undocumentedpayload) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serverrequestmetadata) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/acceptableprotocol) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/acceptheadercontenttype) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/qualityvalue) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/openapivaluecontainer) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/openapiobjectcontainer) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/openapiarraycontainer) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/customcoder) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/httpresponseconvertible) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/errorhandlingmiddleware) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/jsonencodingoptions) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/jsonlinesdeserializationsequence) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/jsonlinesserializationsequence) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/jsonsequencedeserializationsequence) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/jsonsequenceserializationsequence) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serversentevent) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serversenteventwithjsondata) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serversenteventsdeserializationsequence) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serversenteventslinedeserializationsequence) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/serversenteventsserializationsequence) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/foundation) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/swift) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/_concurrency) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder/init(options:) + +#app-main) + +- OpenAPIRuntime +- ISO8601DateTranscoder +- init(options:) + +Initializer + +# init(options:) + +Creates a new transcoder with the provided options. + +init(options: ISO8601DateFormatter.Options? = nil) + +Configuration.swift + +## Parameters + +`options` + +Options to override the default ones. If you provide nil here, the default options are used. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder/decode(_:) + +#app-main) + +- OpenAPIRuntime +- ISO8601DateTranscoder +- decode(\_:) + +Instance Method + +# decode(\_:) + +Creates and returns a date object from the specified ISO 8601 formatted string representation. + +Configuration.swift + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder/encode(_:) + +#app-main) + +- OpenAPIRuntime +- ISO8601DateTranscoder +- encode(\_:) + +Instance Method + +# encode(\_:) + +Creates and returns an ISO 8601 formatted string representation of the specified date. + +Configuration.swift + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder/datetranscoder-implementations + +- OpenAPIRuntime +- ISO8601DateTranscoder +- DateTranscoder Implementations + +API Collection + +# DateTranscoder Implementations + +## Topics + +### Type Properties + +`static var iso8601: ISO8601DateTranscoder` + +A transcoder that transcodes dates as ISO-8601–formatted string (in RFC 3339 format). + +`static var iso8601WithFractionalSeconds: ISO8601DateTranscoder` + +A transcoder that transcodes dates as ISO-8601–formatted string (in RFC 3339 format) with fractional seconds. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder/init(options:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder/decode(_:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder/encode(_:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder/datetranscoder-implementations) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/init(datetranscoder:jsonencodingoptions:multipartboundarygenerator:xmlcoder:) + +#app-main) + +- OpenAPIRuntime +- Configuration +- init(dateTranscoder:jsonEncodingOptions:multipartBoundaryGenerator:xmlCoder:) + +Initializer + +# init(dateTranscoder:jsonEncodingOptions:multipartBoundaryGenerator:xmlCoder:) + +Creates a new configuration with the specified values. + +init( +dateTranscoder: any DateTranscoder = .iso8601, +jsonEncodingOptions: JSONEncodingOptions = [.sortedKeys, .prettyPrinted], +multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random, +xmlCoder: (any CustomCoder)? = nil +) + +Configuration.swift + +## Parameters + +`dateTranscoder` + +The transcoder to use when converting between date and string values. + +`jsonEncodingOptions` + +The options for the underlying JSON encoder. + +`multipartBoundaryGenerator` + +The generator to use when creating mutlipart bodies. + +`xmlCoder` + +Custom XML coder for encoding and decoding xml bodies. Only required when using XML body payloads. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/init(datetranscoder:multipartboundarygenerator:) + +#app-main) + +- OpenAPIRuntime +- Configuration +- init(dateTranscoder:multipartBoundaryGenerator:) + +Initializer + +# init(dateTranscoder:multipartBoundaryGenerator:) + +Creates a new configuration with the specified values. + +init( +dateTranscoder: any DateTranscoder = .iso8601, +multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random +) + +Deprecated.swift + +## Parameters + +`dateTranscoder` + +The transcoder to use when converting between date and string values. + +`multipartBoundaryGenerator` + +The generator to use when creating mutlipart bodies. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/init(datetranscoder:multipartboundarygenerator:xmlcoder:) + +#app-main) + +- OpenAPIRuntime +- Configuration +- init(dateTranscoder:multipartBoundaryGenerator:xmlCoder:) + +Initializer + +# init(dateTranscoder:multipartBoundaryGenerator:xmlCoder:) + +Creates a new configuration with the specified values. + +init( +dateTranscoder: any DateTranscoder = .iso8601, +multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random, +xmlCoder: (any CustomCoder)? = nil +) + +Deprecated.swift + +## Parameters + +`dateTranscoder` + +The transcoder to use when converting between date and string values. + +`multipartBoundaryGenerator` + +The generator to use when creating mutlipart bodies. + +`xmlCoder` + +Custom XML coder for encoding and decoding xml bodies. Only required when using XML body payloads. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/datetranscoder + +- OpenAPIRuntime +- Configuration +- dateTranscoder + +Instance Property + +# dateTranscoder + +The transcoder used when converting between date and string values. + +var dateTranscoder: any DateTranscoder + +Configuration.swift + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/jsonencodingoptions + +- OpenAPIRuntime +- Configuration +- jsonEncodingOptions + +Instance Property + +# jsonEncodingOptions + +The options for the underlying JSON encoder. + +var jsonEncodingOptions: JSONEncodingOptions + +Configuration.swift + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/multipartboundarygenerator + +- OpenAPIRuntime +- Configuration +- multipartBoundaryGenerator + +Instance Property + +# multipartBoundaryGenerator + +The generator to use when creating mutlipart bodies. + +var multipartBoundaryGenerator: any MultipartBoundaryGenerator + +Configuration.swift + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/xmlcoder + +- OpenAPIRuntime +- Configuration +- xmlCoder + +Instance Property + +# xmlCoder + +Custom XML coder for encoding and decoding xml bodies. + +var xmlCoder: (any CustomCoder)? + +Configuration.swift + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/init(datetranscoder:jsonencodingoptions:multipartboundarygenerator:xmlcoder:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/init(datetranscoder:multipartboundarygenerator:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/init(datetranscoder:multipartboundarygenerator:xmlcoder:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/datetranscoder) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/jsonencodingoptions) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/multipartboundarygenerator) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/configuration/xmlcoder) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clienttransport/send(_:body:baseurl:operationid:) + +#app-main) + +- OpenAPIRuntime +- ClientTransport +- send(\_:body:baseURL:operationID:) + +Instance Method + +# send(\_:body:baseURL:operationID:) + +Sends the underlying HTTP request and returns the received HTTP response. + +func send( +_ request: HTTPRequest, +body: HTTPBody?, +baseURL: URL, +operationID: String + +ClientTransport.swift + +**Required** + +## Parameters + +`request` + +An HTTP request. + +`body` + +An HTTP request body. + +`baseURL` + +A server base URL. + +`operationID` + +The identifier of the OpenAPI operation. + +## Return Value + +An HTTP response and its body. + +## Discussion + +- send(\_:body:baseURL:operationID:) +- Parameters +- Return Value +- Discussion + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clientmiddleware). + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clienttransport/send(_:body:baseurl:operationid:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartboundarygenerator/makeboundary() + +#app-main) + +- OpenAPIRuntime +- MultipartBoundaryGenerator +- makeBoundary() + +Instance Method + +# makeBoundary() + +Generates a boundary string for a multipart message. + +MultipartBoundaryGenerator.swift + +**Required** + +## Return Value + +A boundary string. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartboundarygenerator/constant + +- OpenAPIRuntime +- MultipartBoundaryGenerator +- constant + +Type Property + +# constant + +A generator that always returns the same boundary string. + +static var constant: ConstantMultipartBoundaryGenerator { get } + +MultipartBoundaryGenerator.swift + +Available when `Self` is `ConstantMultipartBoundaryGenerator`. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartboundarygenerator/random + +- OpenAPIRuntime +- MultipartBoundaryGenerator +- random + +Type Property + +# random + +A generator that produces a random boundary every time. + +static var random: RandomMultipartBoundaryGenerator { get } + +MultipartBoundaryGenerator.swift + +Available when `Self` is `RandomMultipartBoundaryGenerator`. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartboundarygenerator/makeboundary()) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartboundarygenerator/constant) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartboundarygenerator/random) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clientmiddleware/intercept(_:body:baseurl:operationid:next:) + + + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clienttransport). + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/clientmiddleware/intercept(_:body:baseurl:operationid:next:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator/init(boundaryprefix:randomnumbersuffixlength:) + +#app-main) + +- OpenAPIRuntime +- RandomMultipartBoundaryGenerator +- init(boundaryPrefix:randomNumberSuffixLength:) + +Initializer + +# init(boundaryPrefix:randomNumberSuffixLength:) + +Create a new generator. + +init( +boundaryPrefix: String = "__X_SWIFT_OPENAPI_", +randomNumberSuffixLength: Int = 20 +) + +MultipartBoundaryGenerator.swift + +## Parameters + +`boundaryPrefix` + +The constant prefix of each boundary. + +`randomNumberSuffixLength` + +The length, in bytes, of the random boundary suffix. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator/boundaryprefix + +- OpenAPIRuntime +- RandomMultipartBoundaryGenerator +- boundaryPrefix + +Instance Property + +# boundaryPrefix + +The constant prefix of each boundary. + +let boundaryPrefix: String + +MultipartBoundaryGenerator.swift + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator/randomnumbersuffixlength + +- OpenAPIRuntime +- RandomMultipartBoundaryGenerator +- randomNumberSuffixLength + +Instance Property + +# randomNumberSuffixLength + +The length, in bytes, of the random boundary suffix. + +let randomNumberSuffixLength: Int + +MultipartBoundaryGenerator.swift + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator/makeboundary() + +#app-main) + +- OpenAPIRuntime +- RandomMultipartBoundaryGenerator +- makeBoundary() + +Instance Method + +# makeBoundary() + +Generates a boundary string for a multipart message. + +MultipartBoundaryGenerator.swift + +## Return Value + +A boundary string. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator/multipartboundarygenerator-implementations + +- OpenAPIRuntime +- RandomMultipartBoundaryGenerator +- MultipartBoundaryGenerator Implementations + +API Collection + +# MultipartBoundaryGenerator Implementations + +## Topics + +### Type Properties + +`static var random: RandomMultipartBoundaryGenerator` + +A generator that produces a random boundary every time. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator/init(boundaryprefix:randomnumbersuffixlength:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator/boundaryprefix) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator/randomnumbersuffixlength) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator/makeboundary()) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/randommultipartboundarygenerator/multipartboundarygenerator-implementations) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/datetranscoder/decode(_:) + +#app-main) + +- OpenAPIRuntime +- DateTranscoder +- decode(\_:) + +Instance Method + +# decode(\_:) + +Decodes a `String` as a `Date`. + +Configuration.swift + +**Required** + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/datetranscoder/encode(_:) + +#app-main) + +- OpenAPIRuntime +- DateTranscoder +- encode(\_:) + +Instance Method + +# encode(\_:) + +Encodes the `Date` as a `String`. + +Configuration.swift + +**Required** + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/datetranscoder/iso8601 + + + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/datetranscoder/iso8601withfractionalseconds + +- OpenAPIRuntime +- DateTranscoder +- iso8601WithFractionalSeconds + +Type Property + +# iso8601WithFractionalSeconds + +A transcoder that transcodes dates as ISO-8601–formatted string (in RFC 3339 format) with fractional seconds. + +static var iso8601WithFractionalSeconds: ISO8601DateTranscoder { get } + +Configuration.swift + +Available when `Self` is `ISO8601DateTranscoder`. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/iso8601datetranscoder). + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/datetranscoder/decode(_:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/datetranscoder/encode(_:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/datetranscoder/iso8601) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/datetranscoder/iso8601withfractionalseconds) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/constantmultipartboundarygenerator/init(boundary:) + +#app-main) + +- OpenAPIRuntime +- ConstantMultipartBoundaryGenerator +- init(boundary:) + +Initializer + +# init(boundary:) + +Creates a new generator. + +init(boundary: String = "__X_SWIFT_OPENAPI_GENERATOR_BOUNDARY__") + +MultipartBoundaryGenerator.swift + +## Parameters + +`boundary` + +The boundary string to return every time. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/constantmultipartboundarygenerator/boundary + +- OpenAPIRuntime +- ConstantMultipartBoundaryGenerator +- boundary + +Instance Property + +# boundary + +The boundary string to return. + +let boundary: String + +MultipartBoundaryGenerator.swift + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/constantmultipartboundarygenerator/makeboundary() + +#app-main) + +- OpenAPIRuntime +- ConstantMultipartBoundaryGenerator +- makeBoundary() + +Instance Method + +# makeBoundary() + +Generates a boundary string for a multipart message. + +MultipartBoundaryGenerator.swift + +## Return Value + +A boundary string. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/constantmultipartboundarygenerator/multipartboundarygenerator-implementations + +- OpenAPIRuntime +- ConstantMultipartBoundaryGenerator +- MultipartBoundaryGenerator Implementations + +API Collection + +# MultipartBoundaryGenerator Implementations + +## Topics + +### Type Properties + +`static var constant: ConstantMultipartBoundaryGenerator` + +A generator that always returns the same boundary string. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/constantmultipartboundarygenerator/init(boundary:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/constantmultipartboundarygenerator/boundary) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/constantmultipartboundarygenerator/makeboundary()) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/constantmultipartboundarygenerator/multipartboundarygenerator-implementations) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servertransport/register(_:method:path:) + +#app-main) + +- OpenAPIRuntime +- ServerTransport +- register(\_:method:path:) + +Instance Method + +# register(\_:method:path:) + +Registers an HTTP operation handler at the provided path and method. + +func register( + +method: HTTPRequest.Method, +path: String +) throws + +ServerTransport.swift + +**Required** + +## Parameters + +`handler` + +A handler to be invoked when an HTTP request is received. + +`method` + +An HTTP request method. + +`path` + +A URL template for the path, for example `/pets/{petId}`. + +## Discussion + +- register(\_:method:path:) +- Parameters +- Discussion + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servertransport/register(_:method:path:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servermiddleware/intercept(_:body:metadata:operationid:next:) + +#app-main) + +- OpenAPIRuntime +- ServerMiddleware +- intercept(\_:body:metadata:operationID:next:) + +Instance Method + +# intercept(\_:body:metadata:operationID:next:) + +Intercepts an incoming HTTP request and an outgoing HTTP response. + +func intercept( +_ request: HTTPRequest, +body: HTTPBody?, +metadata: ServerRequestMetadata, +operationID: String, + +ServerTransport.swift + +**Required** + +## Parameters + +`request` + +An HTTP request. + +`body` + +An HTTP request body. + +`metadata` + +The metadata parsed from the HTTP request, including path parameters. + +`operationID` + +The identifier of the OpenAPI operation. + +`next` + +A closure that calls the next middleware, or the transport. + +## Return Value + +An HTTP response and its body. + +## Discussion + +- intercept(\_:body:metadata:operationID:next:) +- Parameters +- Return Value +- Discussion + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servertransport). + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/servermiddleware/intercept(_:body:metadata:operationid:next:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/collecting:%20myCaseBValue.body,%20upTo:%201024 + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/iterator + +- OpenAPIRuntime +- MultipartBody +- MultipartBody.Iterator + +Structure + +# MultipartBody.Iterator + +An async iterator of both input async sequences and of the sequence itself. + +struct Iterator + +MultipartPublicTypes.swift + +Available when `Part` conforms to `Sendable`. + +## Topics + +### Instance Methods + +Advances the iterator to the next element and returns it asynchronously. + +## Relationships + +### Conforms To + +- `_Concurrency.AsyncIteratorProtocol` + +- MultipartBody.Iterator +- Topics +- Relationships + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/init(_:)-23wfb + +-23wfb#app-main) + +- OpenAPIRuntime +- MultipartBody +- init(\_:) + +Initializer + +# init(\_:) + +Creates a new sequence with the provided async throwing stream. + +MultipartPublicTypes.swift + +Available when `Part` conforms to `Sendable`. + +## Parameters + +`stream` + +An async throwing stream that provides the parts. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/init(_:)-4nr4c + +-4nr4c#app-main) + +- OpenAPIRuntime +- MultipartBody +- init(\_:) + +Initializer + +# init(\_:) + +Creates a new sequence with the provided collection of parts. + +MultipartPublicTypes.swift + +Available when `Part` conforms to `Sendable`. + +## Parameters + +`elements` + +A collection of parts. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/init(_:)-5mnyc + +-5mnyc#app-main) + +- OpenAPIRuntime +- MultipartBody +- init(\_:) + +Initializer + +# init(\_:) + +Creates a new sequence with the provided async stream. + +MultipartPublicTypes.swift + +Available when `Part` conforms to `Sendable`. + +## Parameters + +`stream` + +An async stream that provides the parts. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/init(_:iterationbehavior:) + +#app-main) + +- OpenAPIRuntime +- MultipartBody +- init(\_:iterationBehavior:) + +Initializer + +# init(\_:iterationBehavior:) + +Creates a new sequence with the provided async sequence of parts. + +_ sequence: Input, +iterationBehavior: IterationBehavior +) where Part == Input.Element, Input : Sendable, Input : AsyncSequence + +MultipartPublicTypes.swift + +Available when `Part` conforms to `Sendable`. + +## Parameters + +`sequence` + +An async sequence that provides the parts. + +`iterationBehavior` + +The iteration behavior of the sequence, which indicates whether it can be iterated multiple times. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/iterationbehavior + +- OpenAPIRuntime +- MultipartBody +- iterationBehavior + +Instance Property + +# iterationBehavior + +The iteration behavior, which controls how many times the input sequence can be iterated. + +let iterationBehavior: IterationBehavior + +MultipartPublicTypes.swift + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/asyncsequence-implementations + +- OpenAPIRuntime +- MultipartBody +- AsyncSequence Implementations + +API Collection + +# AsyncSequence Implementations + +## Topics + +### Instance Methods + +Returns another sequence that decodes each JSON Lines event as the provided type using the provided decoder. + +Returns another sequence that decodes each JSON Sequence event as the provided type using the provided decoder. + +`func asDecodedServerSentEvents() -> ServerSentEventsDeserializationSequence>` + +Returns another sequence that decodes each event’s data as the provided type using the provided decoder. + +Deprecated + +`func asDecodedServerSentEvents(while: (ArraySlice) -> Bool) -> ServerSentEventsDeserializationSequence>` + +`func asDecodedServerSentEventsWithJSONData(of: JSONDataType.Type, decoder: JSONDecoder) -> AsyncThrowingMapSequence>, ServerSentEventWithJSONData>` + +`func asDecodedServerSentEventsWithJSONData(of: JSONDataType.Type, decoder: JSONDecoder, while: (ArraySlice) -> Bool) -> AsyncThrowingMapSequence>, ServerSentEventWithJSONData>` + +`func asEncodedJSONLines(encoder: JSONEncoder) -> JSONLinesSerializationSequence>>` + +Returns another sequence that encodes the events using the provided encoder into JSON Lines. + +`func asEncodedJSONSequence(encoder: JSONEncoder) -> JSONSequenceSerializationSequence>>` + +Returns another sequence that encodes the events using the provided encoder into a JSON Sequence. + +Returns another sequence that encodes Server-sent Events with generic data in the data field. + +`func asEncodedServerSentEventsWithJSONData(encoder: JSONEncoder) -> ServerSentEventsSerializationSequence>` + +Returns another sequence that encodes Server-sent Events that have a JSON value in the data field. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/equatable-implementations + +- OpenAPIRuntime +- MultipartBody +- Equatable Implementations + +API Collection + +# Equatable Implementations + +## Topics + +### Operators + +Compares two OpenAPISequence instances for equality by comparing their object identifiers. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/expressiblebyarrayliteral-implementations + +- OpenAPIRuntime +- MultipartBody +- ExpressibleByArrayLiteral Implementations + +API Collection + +# ExpressibleByArrayLiteral Implementations + +## Topics + +### Initializers + +Creates an instance initialized with the given elements. + +### Type Aliases + +`typealias ArrayLiteralElement` + +The type of the elements of an array literal. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/hashable-implementations + +- OpenAPIRuntime +- MultipartBody +- Hashable Implementations + +API Collection + +# Hashable Implementations + +## Topics + +### Instance Methods + +`func hash(into: inout Hasher)` + +Hashes the OpenAPISequence instance by combining its object identifier into the provided hasher. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/iterator) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/init(_:)-23wfb) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/init(_:)-4nr4c) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/init(_:)-5mnyc) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/init(_:iterationbehavior:)) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/iterationbehavior) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/asyncsequence-implementations) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/equatable-implementations) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/expressiblebyarrayliteral-implementations) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + +# https://swiftpackageindex.com/apple/swift-openapi-runtime/1.9.0/documentation/openapiruntime/multipartbody/hashable-implementations) + +#### 404 - Not Found + +If you were expecting to find a page here, please raise an issue. + +From here, you'll want to go to the home page or search for a package. + +| +| + +--- + diff --git a/.devcontainer/swift-6.2-nightly/devcontainer.json b/.devcontainer/swift-6.2-nightly/devcontainer.json deleted file mode 100644 index b5bd73c4..00000000 --- a/.devcontainer/swift-6.2-nightly/devcontainer.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "Swift 6.2 Nightly", - "image": "swiftlang/swift:nightly-6.2-noble", - "features": { - "ghcr.io/devcontainers/features/common-utils:2": { - "installZsh": "false", - "username": "vscode", - "upgradePackages": "false" - }, - "ghcr.io/devcontainers/features/git:1": { - "version": "os-provided", - "ppa": "false" - } - }, - "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", - "runArgs": [ - "--cap-add=SYS_PTRACE", - "--security-opt", - "seccomp=unconfined" - ], - // Configure tool-specific properties. - "customizations": { - // Configure properties specific to VS Code. - "vscode": { - // Set *default* container specific settings.json values on container create. - "settings": { - "lldb.library": "/usr/lib/liblldb.so" - }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "sswg.swift-lang" - ] - } - }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "root" -} \ No newline at end of file diff --git a/.devcontainer/swift-6.3-nightly/devcontainer.json b/.devcontainer/swift-6.3-nightly/devcontainer.json new file mode 100644 index 00000000..57c29fee --- /dev/null +++ b/.devcontainer/swift-6.3-nightly/devcontainer.json @@ -0,0 +1,15 @@ +{ + "name": "Swift 6.3 Nightly Development Container", + "image": "swift:6.3-nightly-jammy", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "sswg.swift-lang" + ] + } + }, + "postCreateCommand": "swift --version" +} diff --git a/CLAUDE.md b/CLAUDE.md index b082c896..f50490c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,6 +97,32 @@ swift run mistdemo --config-file ~/.mistdemo/config.json query ## Architecture Considerations +### FieldValue Type Architecture + +MistKit uses separate types for requests and responses at the OpenAPI schema level to accurately model CloudKit's asymmetric API behavior: + +**Type Layers:** +1. **Domain Layer**: `FieldValue` enum - Pure Swift types, no API metadata (Sources/MistKit/FieldValue.swift) +2. **API Request Layer**: `FieldValueRequest` - No type field, CloudKit infers type from value structure +3. **API Response Layer**: `FieldValueResponse` - Optional type field for explicit type information + +**Why Separate Request/Response Types?** +- CloudKit API has asymmetric behavior: requests omit type field, responses may include it +- OpenAPI schema accurately models this asymmetry (openapi.yaml:867-920) +- Swift code generation produces type-safe request/response types +- Compiler prevents accidentally using response types in requests +- Cleaner architecture without nil type values in conversion code + +**Generated Types:** +- `Components.Schemas.FieldValueRequest` - Used for modify, create, filter operations +- `Components.Schemas.FieldValueResponse` - Used for query, lookup, changes responses +- `Components.Schemas.RecordRequest` - Records in request bodies +- `Components.Schemas.RecordResponse` - Records in response bodies + +**Conversion:** +- Request conversion: `Extensions/OpenAPI/Components+FieldValue.swift` converts domain `FieldValue` → `FieldValueRequest` +- Response conversion: `Service/FieldValue+Components.swift` converts `FieldValueResponse` → domain `FieldValue` + ### Modern Swift Features to Utilize - Swift Concurrency (async/await) for all network operations - Structured concurrency with TaskGroup for parallel operations @@ -154,6 +180,47 @@ MistKitLogger.logDebug(_:logger:shouldRedact:) // Debug level - Set `MISTKIT_DISABLE_LOG_REDACTION=1` to disable redaction for debugging - Tokens, keys, and secrets are automatically masked in logged messages +### Asset Upload Transport Design + +**⚠️ CRITICAL WARNING: Transport Separation** + +When providing a custom `AssetUploader` implementation: +- **NEVER** use the CloudKit API transport (`ClientTransport`) for asset uploads +- **MUST** use a separate URLSession instance, NOT shared with api.apple-cloudkit.com +- **MUST NOT** share HTTP/2 connections between CloudKit API and CDN hosts +- Custom uploaders should **ONLY** be used for testing or specialized CDN configurations +- Production code should use the default implementation (`URLSession.shared`) + +**Why URLSession instead of ClientTransport?** + +Asset uploads use `URLSession.shared` directly rather than the injected `ClientTransport` to avoid HTTP/2 connection reuse issues: + +1. **Problem:** CloudKit API (api.apple-cloudkit.com) and CDN (cvws.icloud-content.com) are different hosts +2. **HTTP/2 Issue:** Reusing the same HTTP/2 connection for both hosts causes 421 Misdirected Request errors +3. **Solution:** Use separate URLSession for CDN uploads, maintaining distinct connection pools + +**Design:** +- `AssetUploader` closure type allows dependency injection for testing +- Default implementation uses `URLSession.shared.upload(_:to:)` with separate connection pool +- Tests provide mock uploader closures without network calls +- Platform-specific: WASI compilation excludes URLSession code via `#if !os(WASI)` +- **CRITICAL:** Custom uploaders must maintain connection pool separation from CloudKit API + +**Implementation Details:** +- AssetUploader type: `(Data, URL) async throws -> (statusCode: Int?, data: Data)` +- Defined in: `Sources/MistKit/Core/AssetUploader.swift` +- URLSession extension: `Sources/MistKit/Extensions/URLSession+AssetUpload.swift` +- Upload orchestration: `Sources/MistKit/Service/CloudKitService+WriteOperations.swift` + - `uploadAssets()` - Complete two-step upload workflow + - `requestAssetUploadURL()` - Step 1: Get CDN upload URL + - `uploadAssetData()` - Step 2: Upload binary data to CDN + +**Future Consideration:** +A `ClientTransport` extension could provide a generic upload method, but would need to: +- Handle connection pooling separately for different hosts +- Provide platform-specific implementations (URLSession, custom transports) +- Maintain the same testability via dependency injection + ### CloudKit Web Services Integration - Base URL: `https://api.apple-cloudkit.com` - Authentication: API Token + Web Auth Token or Server-to-Server Key Authentication @@ -171,6 +238,18 @@ MistKitLogger.logDebug(_:logger:shouldRedact:) // Debug level - Parameterized tests for testing multiple scenarios - See `testing-enablinganddisabling.md` for Swift Testing patterns +### Asset Upload Testing + +**Integration Test Requirements:** +- Verify connection pool separation between CloudKit API and CDN +- Test HTTP/2 connection reuse prevention +- Validate 421 Misdirected Request error handling +- Mock uploaders should simulate realistic HTTP responses + +**Test Files:** +- `Tests/MistKitTests/Service/CloudKitServiceUploadTests+*.swift` +- `Tests/MistKitTests/Service/AssetUploadTokenTests.swift` + ## Important Implementation Notes 1. **Async/Await First**: All network operations should use async/await, not completion handlers diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/VirtualBuddyFetcherTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/VirtualBuddyFetcherTests.swift index 6a265a2d..34732c91 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/VirtualBuddyFetcherTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/VirtualBuddyFetcherTests.swift @@ -160,7 +160,7 @@ internal struct VirtualBuddyFetcherTests { httpVersion: nil, headerFields: nil )! - let data = TestFixtures.virtualBuddySonoma1421Response.data(using: .utf8)! + let data = Data(TestFixtures.virtualBuddySonoma1421Response.utf8) return (response, data) } @@ -198,7 +198,7 @@ internal struct VirtualBuddyFetcherTests { httpVersion: nil, headerFields: nil )! - let data = TestFixtures.virtualBuddyUnsignedResponse.data(using: .utf8)! + let data = Data(TestFixtures.virtualBuddyUnsignedResponse.utf8) return (response, data) } @@ -287,7 +287,7 @@ internal struct VirtualBuddyFetcherTests { httpVersion: nil, headerFields: nil )! - let data = TestFixtures.virtualBuddySonoma1421Response.data(using: .utf8)! + let data = Data(TestFixtures.virtualBuddySonoma1421Response.utf8) return (response, data) } @@ -339,7 +339,7 @@ internal struct VirtualBuddyFetcherTests { httpVersion: nil, headerFields: nil )! - let data = TestFixtures.virtualBuddyBuildMismatchResponse.data(using: .utf8)! + let data = Data(TestFixtures.virtualBuddyBuildMismatchResponse.utf8) return (response, data) } @@ -483,7 +483,7 @@ internal struct VirtualBuddyFetcherTests { httpVersion: nil, headerFields: nil )! - let invalidJSON = "{ invalid json }".data(using: .utf8)! + let invalidJSON = Data("{ invalid json }".utf8) return (response, invalidJSON) } @@ -517,7 +517,7 @@ internal struct VirtualBuddyFetcherTests { httpVersion: nil, headerFields: nil )! - let data = TestFixtures.virtualBuddySonoma1421Response.data(using: .utf8)! + let data = Data(TestFixtures.virtualBuddySonoma1421Response.utf8) return (response, data) } @@ -547,7 +547,7 @@ internal struct VirtualBuddyFetcherTests { httpVersion: nil, headerFields: nil )! - let data = TestFixtures.virtualBuddySonoma1421Response.data(using: .utf8)! + let data = Data(TestFixtures.virtualBuddySonoma1421Response.utf8) return (response, data) } @@ -641,7 +641,7 @@ internal struct VirtualBuddyFetcherTests { httpVersion: nil, headerFields: nil )! - let data = TestFixtures.virtualBuddySignedResponse.data(using: .utf8)! + let data = Data(TestFixtures.virtualBuddySignedResponse.utf8) return (response, data) } @@ -685,7 +685,7 @@ internal struct VirtualBuddyFetcherTests { httpVersion: nil, headerFields: nil )! - let data = TestFixtures.virtualBuddyUnsignedResponse.data(using: .utf8)! + let data = Data(TestFixtures.virtualBuddyUnsignedResponse.utf8) return (response, data) } @@ -731,7 +731,7 @@ internal struct VirtualBuddyFetcherTests { httpVersion: nil, headerFields: nil )! - let data = TestFixtures.virtualBuddySonoma1421Response.data(using: .utf8)! + let data = Data(TestFixtures.virtualBuddySonoma1421Response.utf8) return (response, data) } diff --git a/Examples/MistDemo/Package.resolved b/Examples/MistDemo/Package.resolved index 3d5c4fa9..c23cdde8 100644 --- a/Examples/MistDemo/Package.resolved +++ b/Examples/MistDemo/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "02f43a37425a793d28e0bcfae294deea6c8825bdb1fc10c7e1b170f38730c508", + "originHash" : "33fd915476a7cdcedb724c6d792f6b5a583243f1ac2482c608d8de3f342a8328", "pins" : [ { "identity" : "async-http-client", @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-openapi-runtime", "state" : { - "revision" : "7722cf8eac05c1f1b5b05895b04cfcc29576d9be", - "version" : "1.8.3" + "revision" : "7cdf33371bf89b23b9cf4fd3ce8d3c825c28fbe8", + "version" : "1.9.0" } }, { diff --git a/Examples/MistDemo/Sources/MistDemo/Commands/CreateCommand.swift b/Examples/MistDemo/Sources/MistDemo/Commands/CreateCommand.swift index fe1c7ec2..6fe72fbe 100644 --- a/Examples/MistDemo/Sources/MistDemo/Commands/CreateCommand.swift +++ b/Examples/MistDemo/Sources/MistDemo/Commands/CreateCommand.swift @@ -65,6 +65,7 @@ public struct CreateCommand: MistDemoCommand, OutputFormatting { int64 Integer numbers double Decimal numbers timestamp Dates (ISO 8601 or Unix timestamp) + asset Asset URL (from upload-asset command) EXAMPLES: @@ -93,10 +94,13 @@ public struct CreateCommand: MistDemoCommand, OutputFormatting { 6. Table output format: mistdemo create --field "title:string:Test" --output-format table + 7. With asset (after upload-asset): + mistdemo create --field "title:string:My Photo, image:asset:https://cws.icloud-content.com:443/..." + NOTES: • Record name is auto-generated if not provided • JSON files auto-detect field types from values - • Use environment variables CLOUDKIT_API_TOKEN and CLOUDKIT_WEBAUTH_TOKEN + • Use environment variables CLOUDKIT_API_TOKEN and CLOUDKIT_WEB_AUTH_TOKEN to avoid repeating tokens """ @@ -115,8 +119,8 @@ public struct CreateCommand: MistDemoCommand, OutputFormatting { let recordName = config.recordName ?? generateRecordName() // Convert fields to CloudKit format - let cloudKitFields = try convertFieldsToCloudKit(config.fields) - + let cloudKitFields = try config.fields.toCloudKitFields() + // Create the record // NOTE: Zone support requires enhancements to CloudKitService.createRecord method let recordInfo = try await client.createRecord( @@ -140,36 +144,6 @@ public struct CreateCommand: MistDemoCommand, OutputFormatting { let randomSuffix = String(Int.random(in: MistDemoConstants.Limits.randomSuffixMin...MistDemoConstants.Limits.randomSuffixMax)) return "\(config.recordType.lowercased())-\(timestamp)-\(randomSuffix)" } - - /// Convert Field array to CloudKit fields dictionary - private func convertFieldsToCloudKit(_ fields: [Field]) throws -> [String: FieldValue] { - var cloudKitFields: [String: FieldValue] = [:] - - for field in fields { - do { - let convertedValue = try field.type.convertValue(field.value) - let fieldValue = try convertToFieldValue(convertedValue, type: field.type) - cloudKitFields[field.name] = fieldValue - } catch { - throw CreateError.fieldConversionError(field.name, field.type, field.value, error.localizedDescription) - } - } - - return cloudKitFields - } - - /// Convert a value to the appropriate FieldValue enum case using the FieldValue extension - private func convertToFieldValue(_ value: Any, type: FieldType) throws -> FieldValue { - guard let fieldValue = FieldValue(value: value, fieldType: type) else { - throw CreateError.fieldConversionError( - "", - type, - String(describing: value), - "Unable to convert value to FieldValue" - ) - } - return fieldValue - } } // CreateError is now defined in Errors/CreateError.swift \ No newline at end of file diff --git a/Examples/MistDemo/Sources/MistDemo/Commands/UpdateCommand.swift b/Examples/MistDemo/Sources/MistDemo/Commands/UpdateCommand.swift new file mode 100644 index 00000000..4ba17776 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemo/Commands/UpdateCommand.swift @@ -0,0 +1,140 @@ +// +// UpdateCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +import MistKit + +/// Command to update an existing record in CloudKit +public struct UpdateCommand: MistDemoCommand, OutputFormatting { + public typealias Config = UpdateConfig + public static let commandName = "update" + public static let abstract = "Update an existing record in CloudKit" + public static let helpText = """ + UPDATE - Update an existing record in CloudKit + + USAGE: + mistdemo update --record-name [options] + + REQUIRED: + --api-token CloudKit API token + --web-auth-token Web authentication token + --record-name Record name to update (REQUIRED) + + OPTIONS: + --record-type Record type (default: Note) + --zone Zone name (default: _defaultZone) + --record-change-tag Change tag for optimistic locking + --output-format Output format: json, table, csv, yaml + + FIELD DEFINITION (choose one method): + --field Inline field definition + --json-file Load fields from JSON file + --stdin Read fields from stdin as JSON + + FIELD FORMAT: + Format: name:type:value + Multiple fields: separate with commas + + FIELD TYPES: + string Text values + int64 Integer numbers + double Decimal numbers + timestamp Dates (ISO 8601 or Unix timestamp) + asset Asset URL (from upload-asset command) + + EXAMPLES: + + 1. Update single field: + mistdemo update --record-name my-note-123 --field "title:string:Updated Title" + + 2. Update multiple fields (comma-separated): + mistdemo update --record-name my-note-123 --field "title:string:New Title, priority:int64:8" + + 3. With optimistic locking: + mistdemo update --record-name my-note-123 \\ + --record-change-tag abc123 --field "title:string:Safe Update" + + 4. From JSON file: + mistdemo update --record-name my-note-123 --json-file updates.json + + Example updates.json: + { + "title": "Updated Project Plan", + "priority": 9, + "progress": 0.75 + } + + 5. From stdin: + echo '{"title":"Quick Update"}' | mistdemo update --record-name my-note-123 --stdin + + 6. Table output format: + mistdemo update --record-name my-note-123 --field "title:string:Test" --output-format table + + 7. Update asset field (after upload-asset): + mistdemo update --record-name my-note-123 \\ + --field "image:asset:https://cws.icloud-content.com:443/..." + + NOTES: + • Record name is REQUIRED for updates + • Only specified fields will be updated, others remain unchanged + • Use record-change-tag for safe concurrent updates + • Use environment variables CLOUDKIT_API_TOKEN and CLOUDKIT_WEB_AUTH_TOKEN + to avoid repeating tokens + """ + + private let config: UpdateConfig + + public init(config: UpdateConfig) { + self.config = config + } + + public func execute() async throws { + do { + // Create CloudKit client + let client = try MistKitClientFactory.create(from: config.base) + + // Convert fields to CloudKit format + let cloudKitFields = try config.fields.toCloudKitFields() + + // Update the record + let recordInfo = try await client.updateRecord( + recordType: config.recordType, + recordName: config.recordName, + fields: cloudKitFields, + recordChangeTag: config.recordChangeTag + ) + + // Format and output result + try await outputResult(recordInfo, format: config.output) + + } catch { + throw UpdateError.operationFailed(error.localizedDescription) + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemo/Commands/UploadAssetCommand.swift b/Examples/MistDemo/Sources/MistDemo/Commands/UploadAssetCommand.swift new file mode 100644 index 00000000..b893f526 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemo/Commands/UploadAssetCommand.swift @@ -0,0 +1,246 @@ +// +// UploadAssetCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import MistKit + +/// Command to upload binary assets to CloudKit +public struct UploadAssetCommand: MistDemoCommand, OutputFormatting { + public typealias Config = UploadAssetConfig + public static let commandName = "upload-asset" + public static let abstract = "Upload binary assets to CloudKit" + public static let helpText = """ + UPLOAD-ASSET - Upload binary assets to CloudKit + + USAGE: + mistdemo upload-asset --file [options] + + REQUIRED OPTIONS: + --file Path to the file to upload + + OPTIONAL: + --record-type Record type name (default: "Note") + --field-name Asset field name (default: "image") + --record-name Unique record name (optional, auto-generated if omitted) + --api-token CloudKit API token + --output-format Output format: json, table, csv, yaml + + EXAMPLES: + # Upload with defaults (Note.image) + mistdemo upload-asset --file photo.jpg + + # Upload to custom record type and field + mistdemo upload-asset \\ + --file photo.jpg \\ + --record-type Photo \\ + --field-name thumbnail + + # Upload with specific record name + mistdemo upload-asset \\ + --file document.pdf \\ + --record-type Document \\ + --field-name file \\ + --record-name my-document-123 + + WORKFLOW: + 1. Upload the asset using this command + 2. Note the returned record name and asset details + 3. Use 'create' or 'update' command to associate the asset with a record + + NOTES: + - Maximum file size: 15 MB + - Upload URLs valid for 15 minutes + - With web authentication: uploads to private database + - With API-only authentication: uploads to public database + - Returns asset metadata (receipt, checksums) needed for record operations + - Defaults match MistDemo schema: Note record type, image field + """ + + private let config: UploadAssetConfig + + public init(config: UploadAssetConfig) { + self.config = config + } + + public func execute() async throws { + print("\n" + String(repeating: "=", count: 60)) + print("📤 Upload Asset to CloudKit") + print(String(repeating: "=", count: 60)) + + // Validate file exists + let fileURL = URL(fileURLWithPath: config.file) + guard FileManager.default.fileExists(atPath: config.file) else { + throw UploadAssetError.fileNotFound(config.file) + } + + do { + // Read file data + let data = try Data(contentsOf: fileURL) + let sizeInMB = Double(data.count) / 1024 / 1024 + print("\n📁 File: \(fileURL.lastPathComponent) (\(String(format: "%.2f", sizeInMB)) MB)") + print("📝 Record Type: \(config.recordType)") + print("🏷️ Field Name: \(config.fieldName)") + if let recordName = config.recordName { + print("🆔 Record Name: \(recordName)") + } + + // Check file size (15 MB limit) + let maxSize: Int64 = 15 * 1024 * 1024 + if data.count > maxSize { + throw UploadAssetError.fileTooLarge(Int64(data.count), maximum: maxSize) + } + + // Create CloudKit service (will use appropriate database based on authentication) + // With web-auth: private database, with API-only: public database + let service = try MistKitClientFactory.create(from: config.base) + + // Upload asset + print("\n⬆️ Uploading...") + let result = try await service.uploadAssets( + data: data, + recordType: config.recordType, + fieldName: config.fieldName, + recordName: config.recordName + ) + + print("\n✅ Asset uploaded successfully!") + print(" Record Name: \(result.recordName)") + print(" Field Name: \(result.fieldName)") + if let receipt = result.asset.receipt { + print(" Receipt: \(receipt.prefix(40))...") + } + + // Now create/update the record with the asset + print("\n📝 Creating record with asset...") + do { + let recordInfo = try await createOrUpdateRecordWithAsset( + result: result, + service: service + ) + + if config.recordName != nil { + print("✅ Record updated with asset!") + } else { + print("✅ New record created with asset!") + } + + print(" Record Name: \(recordInfo.recordName)") + print(" Record Type: \(recordInfo.recordType)") + if let changeTag = recordInfo.recordChangeTag { + print(" Change Tag: \(changeTag)") + } + + // Output in requested format + try await outputResult(recordInfo, format: config.output) + + } catch { + print("\n⚠️ Asset uploaded but record operation failed:") + print(" \(error.localizedDescription)") + print("\n The asset is uploaded but not associated with a record.") + print(" Asset details:") + print(" - Record Name: \(result.recordName)") + print(" - Field Name: \(result.fieldName)") + // Don't throw - asset upload succeeded + } + + } catch let error as CloudKitError { + print("\n❌ CloudKit Error: \(error)") + throw UploadAssetError.operationFailed(error.localizedDescription) + } catch let error as UploadAssetError { + print("\n❌ \(error.localizedDescription)") + throw error + } catch { + print("\n❌ Error: \(error)") + throw UploadAssetError.operationFailed(error.localizedDescription) + } + + print("\n" + String(repeating: "=", count: 60)) + print("✅ Upload completed!") + print(String(repeating: "=", count: 60)) + } + + /// Create or update a record with the uploaded asset + /// The asset metadata (receipt, checksums) from CloudKit must be used in the record + private func createOrUpdateRecordWithAsset( + result: AssetUploadReceipt, + service: CloudKitService + ) async throws -> RecordInfo { + // Use the complete asset data from the upload result + // This contains the receipt and checksums returned by CloudKit + var fields: [String: FieldValue] = [ + config.fieldName: .asset(result.asset) + ] + + // Debug: Print asset details + print(" Asset details:") + print(" - Receipt: \(result.asset.receipt ?? "nil")") + print(" - File checksum: \(result.asset.fileChecksum ?? "nil")") + print(" - Size: \(result.asset.size.map(String.init) ?? "nil")") + print(" - Wrapping key: \(result.asset.wrappingKey ?? "nil")") + print(" - Reference checksum: \(result.asset.referenceChecksum ?? "nil")") + + if let recordName = config.recordName { + // User provided recordName → UPDATE existing record's asset field + // First fetch the existing record to get its current recordChangeTag + print(" Fetching existing record to get change tag...") + let existingRecords = try await service.lookupRecords( + recordNames: [recordName] + ) + + guard let existingRecord = existingRecords.first else { + throw UploadAssetError.operationFailed("Record '\(recordName)' not found") + } + + print(" Updating record with change tag: \(existingRecord.recordChangeTag ?? "nil")") + return try await service.updateRecord( + recordType: config.recordType, + recordName: recordName, + fields: fields, + recordChangeTag: existingRecord.recordChangeTag + ) + } else { + // No recordName → CREATE new record with the asset field + // For Note records, add a default title to ensure validity + if config.recordType == "Note" { + fields["title"] = .string("Uploaded Image - \(Date().formatted())") + } + + // Generate a NEW recordName for the record (don't reuse the upload token's recordName) + // The upload recordName is just for the asset upload, not the actual record + let newRecordName = UUID().uuidString.lowercased() + print(" Creating record with new name: \(newRecordName)") + + return try await service.createRecord( + recordType: config.recordType, + recordName: newRecordName, + fields: fields + ) + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemo/Configuration/FieldType.swift b/Examples/MistDemo/Sources/MistDemo/Configuration/FieldType.swift index 4242decd..0c3d0b7d 100644 --- a/Examples/MistDemo/Sources/MistDemo/Configuration/FieldType.swift +++ b/Examples/MistDemo/Sources/MistDemo/Configuration/FieldType.swift @@ -64,7 +64,10 @@ public enum FieldType: String, CaseIterable, Sendable { } else { throw FieldParsingError.invalidValueForType(stringValue, type: self) } - case .asset, .location, .reference, .bytes: + case .asset: + // stringValue should be the URL from the upload token + return stringValue // Will be converted to FieldValue.Asset later + case .location, .reference, .bytes: // These require more complex parsing - implement later throw FieldParsingError.unsupportedFieldType(self) } diff --git a/Examples/MistDemo/Sources/MistDemo/Configuration/UpdateConfig.swift b/Examples/MistDemo/Sources/MistDemo/Configuration/UpdateConfig.swift new file mode 100644 index 00000000..29414239 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemo/Configuration/UpdateConfig.swift @@ -0,0 +1,159 @@ +// +// UpdateConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +public import MistKit +public import ConfigKeyKit + +/// Configuration for update command +public struct UpdateConfig: Sendable, ConfigurationParseable { + public typealias ConfigReader = MistDemoConfiguration + public typealias BaseConfig = MistDemoConfig + + public let base: MistDemoConfig + public let zone: String + public let recordType: String + public let recordName: String + public let recordChangeTag: String? + public let fields: [Field] + public let output: OutputFormat + + public init( + base: MistDemoConfig, + zone: String = "_defaultZone", + recordType: String = "Note", + recordName: String, + recordChangeTag: String? = nil, + fields: [Field] = [], + output: OutputFormat = .json + ) { + self.base = base + self.zone = zone + self.recordType = recordType + self.recordName = recordName + self.recordChangeTag = recordChangeTag + self.fields = fields + self.output = output + } + + /// Parse configuration from command line arguments + public init(configuration: MistDemoConfiguration, base: MistDemoConfig?) async throws { + let configReader = configuration + let baseConfig: MistDemoConfig + if let base = base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig(configuration: configuration, base: nil) + } + + // Parse update-specific options + let zone = configReader.string(forKey: MistDemoConstants.ConfigKeys.zone, default: MistDemoConstants.Defaults.zone) ?? MistDemoConstants.Defaults.zone + let recordType = configReader.string(forKey: MistDemoConstants.ConfigKeys.recordType, default: MistDemoConstants.Defaults.recordType) ?? MistDemoConstants.Defaults.recordType + + // Validate recordName is provided (REQUIRED for update) + guard let recordName = configReader.string(forKey: MistDemoConstants.ConfigKeys.recordName) else { + throw UpdateError.recordNameRequired + } + + let recordChangeTag = configReader.string(forKey: MistDemoConstants.ConfigKeys.recordChangeTag) + + // Parse fields from various sources + let fields = try Self.parseFieldsFromSources(configReader) + + // Parse output format + let outputString = configReader.string(forKey: MistDemoConstants.ConfigKeys.outputFormat, default: MistDemoConstants.Defaults.outputFormat) ?? MistDemoConstants.Defaults.outputFormat + let output = OutputFormat(rawValue: outputString) ?? .json + + self.init( + base: baseConfig, + zone: zone, + recordType: recordType, + recordName: recordName, + recordChangeTag: recordChangeTag, + fields: fields, + output: output + ) + } + + private static func parseFieldsFromSources(_ configReader: MistDemoConfiguration) throws -> [Field] { + var fields: [Field] = [] + + // 1. Parse inline field definitions + if let fieldString = configReader.string(forKey: "field") { + let fieldDefinitions = fieldString.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) } + let inlineFields = try Field.parseFields(fieldDefinitions) + fields.append(contentsOf: inlineFields) + } + + // 2. Parse from JSON file + if let jsonFile = configReader.string(forKey: MistDemoConstants.ConfigKeys.jsonFile) { + let jsonFields = try parseFieldsFromJSONFile(jsonFile) + fields.append(contentsOf: jsonFields) + } + + // 3. Parse from stdin (check if data is available) + if configReader.bool(forKey: MistDemoConstants.ConfigKeys.stdin, default: false) { + let stdinFields = try parseFieldsFromStdin() + fields.append(contentsOf: stdinFields) + } + + guard !fields.isEmpty else { + throw UpdateError.noFieldsProvided + } + + return fields + } + + /// Parse fields from JSON file + private static func parseFieldsFromJSONFile(_ filePath: String) throws -> [Field] { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: filePath)) + let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) + return try fieldsInput.toFields() + } catch { + throw UpdateError.jsonFileError(filePath, error.localizedDescription) + } + } + + /// Parse fields from stdin + private static func parseFieldsFromStdin() throws -> [Field] { + let stdinData = FileHandle.standardInput.readDataToEndOfFile() + + guard !stdinData.isEmpty else { + throw UpdateError.emptyStdin + } + + do { + let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: stdinData) + return try fieldsInput.toFields() + } catch { + throw UpdateError.stdinError(error.localizedDescription) + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemo/Configuration/UploadAssetConfig.swift b/Examples/MistDemo/Sources/MistDemo/Configuration/UploadAssetConfig.swift new file mode 100644 index 00000000..35030b62 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemo/Configuration/UploadAssetConfig.swift @@ -0,0 +1,99 @@ +// +// UploadAssetConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +public import MistKit +public import ConfigKeyKit + +/// Configuration for upload-asset command +public struct UploadAssetConfig: Sendable, ConfigurationParseable { + public typealias ConfigReader = MistDemoConfiguration + public typealias BaseConfig = MistDemoConfig + + public let base: MistDemoConfig + public let file: String + public let recordType: String + public let fieldName: String + public let recordName: String? + public let output: OutputFormat + + public init( + base: MistDemoConfig, + file: String, + recordType: String, + fieldName: String, + recordName: String? = nil, + output: OutputFormat = .json + ) { + self.base = base + self.file = file + self.recordType = recordType + self.fieldName = fieldName + self.recordName = recordName + self.output = output + } + + /// Parse configuration from command line arguments + public init(configuration: MistDemoConfiguration, base: MistDemoConfig?) async throws { + let configReader = configuration + let baseConfig: MistDemoConfig + if let base = base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig(configuration: configuration, base: nil) + } + + // Get file path from configuration + guard let filePath = configReader.string(forKey: "file") else { + throw UploadAssetError.filePathRequired + } + + // Get record type (defaults to "Note") + let recordType = configReader.string(forKey: "record-type") ?? "Note" + + // Get field name (defaults to "image") + let fieldName = configReader.string(forKey: "field-name") ?? "image" + + // Parse optional record name + let recordName = configReader.string(forKey: "record-name") + + // Parse output format + let outputString = configReader.string(forKey: "output.format", default: "json") ?? "json" + let output = OutputFormat(rawValue: outputString) ?? .json + + self.init( + base: baseConfig, + file: filePath, + recordType: recordType, + fieldName: fieldName, + recordName: recordName, + output: output + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemo/Constants/MistDemoConstants.swift b/Examples/MistDemo/Sources/MistDemo/Constants/MistDemoConstants.swift index 1ce3a299..8bd98b56 100644 --- a/Examples/MistDemo/Sources/MistDemo/Constants/MistDemoConstants.swift +++ b/Examples/MistDemo/Sources/MistDemo/Constants/MistDemoConstants.swift @@ -49,6 +49,7 @@ public enum MistDemoConstants { public static let port = "port" public static let jsonFile = "json.file" public static let stdin = "stdin" + public static let recordChangeTag = "record.change.tag" } // MARK: - Default Values @@ -180,6 +181,7 @@ public enum MistDemoConstants { public enum Commands { public static let query = "query" public static let create = "create" + public static let update = "update" public static let currentUser = "current-user" public static let authToken = "auth-token" } diff --git a/Examples/MistDemo/Sources/MistDemo/Errors/FieldConversionError.swift b/Examples/MistDemo/Sources/MistDemo/Errors/FieldConversionError.swift new file mode 100644 index 00000000..60ab799f --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemo/Errors/FieldConversionError.swift @@ -0,0 +1,46 @@ +// +// FieldConversionError.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +public import MistKit + +/// Errors that can occur during field conversion +public enum FieldConversionError: Error, LocalizedError { + case conversionFailed(fieldName: String, fieldType: FieldType, value: String, reason: String) + case invalidFieldValue(fieldType: FieldType, value: String) + + public var errorDescription: String? { + switch self { + case .conversionFailed(let fieldName, let fieldType, let value, let reason): + return "Failed to convert field '\(fieldName)' of type '\(fieldType.rawValue)' with value '\(value)': \(reason)" + case .invalidFieldValue(let fieldType, let value): + return "Unable to convert value '\(value)' to FieldValue for type '\(fieldType.rawValue)'" + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemo/Errors/UpdateError.swift b/Examples/MistDemo/Sources/MistDemo/Errors/UpdateError.swift new file mode 100644 index 00000000..565abb6c --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemo/Errors/UpdateError.swift @@ -0,0 +1,77 @@ +// +// UpdateError.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Errors that can occur during update command execution +public enum UpdateError: Error, LocalizedError { + case recordNameRequired + case noFieldsProvided + case fieldConversionError(String, FieldType, String, String) + case jsonFileError(String, String) + case emptyStdin + case stdinError(String) + case operationFailed(String) + + public var errorDescription: String? { + switch self { + case .recordNameRequired: + return "Record name is required for update operations. Use --record-name " + case .noFieldsProvided: + return "No fields provided. Use --field, --json-file, or --stdin to specify fields to update" + case .fieldConversionError(let fieldName, let fieldType, let value, let reason): + return "Failed to convert field '\(fieldName)' of type '\(fieldType.rawValue)' with value '\(value)': \(reason)" + case .jsonFileError(let filename, let reason): + return "Failed to read JSON file '\(filename)': \(reason)" + case .emptyStdin: + return "Empty stdin. Provide JSON data when using --stdin" + case .stdinError(let reason): + return "Failed to read from stdin: \(reason)" + case .operationFailed(let reason): + return "Update operation failed: \(reason)" + } + } + + public var recoverySuggestion: String? { + switch self { + case .recordNameRequired: + return "Specify a record name: mistdemo update --record-name my-record-123 --field \"title:string:Updated\"" + case .noFieldsProvided: + return "Provide at least one field to update using --field, --json-file, or --stdin" + case .fieldConversionError: + return "Check that the field value matches the expected type. Use --help for field type information" + case .jsonFileError: + return "Ensure the JSON file exists and contains valid JSON" + case .emptyStdin: + return "Pipe JSON data to stdin: echo '{\"title\":\"Updated\"}' | mistdemo update --record-name my-record --stdin" + case .stdinError, .operationFailed: + return nil + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemo/Errors/UploadAssetError.swift b/Examples/MistDemo/Sources/MistDemo/Errors/UploadAssetError.swift new file mode 100644 index 00000000..13d1f9ae --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemo/Errors/UploadAssetError.swift @@ -0,0 +1,62 @@ +// +// UploadAssetError.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Errors that can occur during asset upload operations +public enum UploadAssetError: Error, LocalizedError { + case filePathRequired + case recordTypeRequired + case fieldNameRequired + case fileNotFound(String) + case fileTooLarge(Int64, maximum: Int64) + case invalidRecordType(String) + case operationFailed(String) + + public var errorDescription: String? { + switch self { + case .filePathRequired: + return "File path is required. Usage: mistdemo upload-asset --file --record-type --field-name " + case .recordTypeRequired: + return "Record type is required. Specify with --record-type " + case .fieldNameRequired: + return "Field name is required. Specify with --field-name " + case .fileNotFound(let path): + return "File not found at path: \(path)" + case .fileTooLarge(let size, let maximum): + let sizeMB = Double(size) / 1024 / 1024 + let maxMB = Double(maximum) / 1024 / 1024 + return "File size (\(String(format: "%.2f", sizeMB)) MB) exceeds maximum (\(String(format: "%.2f", maxMB)) MB)" + case .invalidRecordType(let type): + return "Invalid record type: \(type)" + case .operationFailed(let message): + return "Upload operation failed: \(message)" + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemo/Extensions/Array+Field.swift b/Examples/MistDemo/Sources/MistDemo/Extensions/Array+Field.swift new file mode 100644 index 00000000..bc46a08d --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemo/Extensions/Array+Field.swift @@ -0,0 +1,58 @@ +// +// Array+Field.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +public import MistKit + +extension Array where Element == Field { + /// Convert Field array to CloudKit fields dictionary + /// - Returns: Dictionary of field names to FieldValue enums + /// - Throws: FieldConversionError if conversion fails + public func toCloudKitFields() throws -> [String: FieldValue] { + try reduce(into: [:]) { result, field in + do { + let convertedValue = try field.type.convertValue(field.value) + guard let fieldValue = FieldValue(value: convertedValue, fieldType: field.type) else { + throw FieldConversionError.invalidFieldValue( + fieldType: field.type, + value: String(describing: convertedValue) + ) + } + result[field.name] = fieldValue + } catch { + throw FieldConversionError.conversionFailed( + fieldName: field.name, + fieldType: field.type, + value: field.value, + reason: error.localizedDescription + ) + } + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemo/Extensions/FieldValue+FieldType.swift b/Examples/MistDemo/Sources/MistDemo/Extensions/FieldValue+FieldType.swift index 8a017fc8..d4f30801 100644 --- a/Examples/MistDemo/Sources/MistDemo/Extensions/FieldValue+FieldType.swift +++ b/Examples/MistDemo/Sources/MistDemo/Extensions/FieldValue+FieldType.swift @@ -76,7 +76,20 @@ extension FieldValue { guard let stringValue = value as? String else { return nil } self = .bytes(stringValue) - case .asset, .location, .reference: + case .asset: + // Value should be the URL from upload token + guard let urlString = value as? String else { return nil } + let asset = FieldValue.Asset( + fileChecksum: nil, + size: nil, + referenceChecksum: nil, + wrappingKey: nil, + receipt: nil, + downloadURL: urlString + ) + self = .asset(asset) + + case .location, .reference: // These complex types require specialized handling // For now, return nil to indicate they're not supported via simple conversion return nil diff --git a/Examples/MistDemo/Sources/MistDemo/MistDemo.swift b/Examples/MistDemo/Sources/MistDemo/MistDemo.swift index 1c6ef047..f277f738 100644 --- a/Examples/MistDemo/Sources/MistDemo/MistDemo.swift +++ b/Examples/MistDemo/Sources/MistDemo/MistDemo.swift @@ -49,6 +49,8 @@ struct MistDemo { await registry.register(CurrentUserCommand.self) await registry.register(QueryCommand.self) await registry.register(CreateCommand.self) + await registry.register(UpdateCommand.self) + await registry.register(UploadAssetCommand.self) // Parse command line arguments let parser = CommandLineParser() diff --git a/Examples/MistDemo/Sources/MistDemo/Types/FieldInputValue.swift b/Examples/MistDemo/Sources/MistDemo/Types/FieldInputValue.swift index 11617bae..a65c62ba 100644 --- a/Examples/MistDemo/Sources/MistDemo/Types/FieldInputValue.swift +++ b/Examples/MistDemo/Sources/MistDemo/Types/FieldInputValue.swift @@ -35,7 +35,8 @@ public enum FieldInputValue { case int(Int) case double(Double) case bool(Bool) - + case asset(String) // Asset URL from upload token + /// Convert to FieldType and string value for Field creation func toFieldComponents() throws -> (FieldType, String) { switch self { @@ -47,6 +48,8 @@ public enum FieldInputValue { return (.double, String(value)) case .bool(let value): return (.string, value ? "true" : "false") + case .asset(let url): + return (.asset, url) } } } \ No newline at end of file diff --git a/Examples/MistDemo/Sources/MistDemo/Types/FieldsInput.swift b/Examples/MistDemo/Sources/MistDemo/Types/FieldsInput.swift index 3d91fa0d..6caf3d80 100644 --- a/Examples/MistDemo/Sources/MistDemo/Types/FieldsInput.swift +++ b/Examples/MistDemo/Sources/MistDemo/Types/FieldsInput.swift @@ -70,6 +70,8 @@ public struct FieldsInput: Codable { try container.encode(doubleValue, forKey: dynamicKey) case .bool(let boolValue): try container.encode(boolValue, forKey: dynamicKey) + case .asset(let url): + try container.encode(url, forKey: dynamicKey) } } } diff --git a/Examples/MistDemo/Sources/MistDemo/Utilities/AuthenticationHelper.swift b/Examples/MistDemo/Sources/MistDemo/Utilities/AuthenticationHelper.swift index 640a3e5a..3253fa76 100644 --- a/Examples/MistDemo/Sources/MistDemo/Utilities/AuthenticationHelper.swift +++ b/Examples/MistDemo/Sources/MistDemo/Utilities/AuthenticationHelper.swift @@ -193,7 +193,7 @@ enum AuthenticationHelper { /// Resolves web auth token from option or environment variable static func resolveWebAuthToken(_ webAuthToken: String) -> String? { let token = webAuthToken.isEmpty ? - ProcessInfo.processInfo.environment["CLOUDKIT_WEBAUTH_TOKEN"] ?? "" : + EnvironmentConfig.getOptional(MistDemoConstants.EnvironmentVars.cloudKitWebAuthToken) ?? "" : webAuthToken return token.isEmpty ? nil : token } diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/ErrorOutputTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/ErrorOutputTests.swift index a1a5f4f7..69e2e609 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/ErrorOutputTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/ErrorOutputTests.swift @@ -111,7 +111,7 @@ struct ErrorOutputTests { ) let jsonString = try errorOutput.toJSON(pretty: false) - let jsonData = jsonString.data(using: .utf8)! + let jsonData = Data(jsonString.utf8) let decoded = try JSONDecoder().decode(ErrorOutput.self, from: jsonData) #expect(decoded.error.code == "TEST") diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodableTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodableTests.swift index 71adf36f..1c86faa9 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodableTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodableTests.swift @@ -39,7 +39,7 @@ struct AnyCodableTests { @Test("Decode string value") func decodeString() throws { let json = "\"hello world\"" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value as? String == "hello world") } @@ -47,7 +47,7 @@ struct AnyCodableTests { @Test("Decode empty string") func decodeEmptyString() throws { let json = "\"\"" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value as? String == "") } @@ -57,7 +57,7 @@ struct AnyCodableTests { @Test("Decode positive integer") func decodePositiveInt() throws { let json = "42" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value as? Int == 42) } @@ -65,7 +65,7 @@ struct AnyCodableTests { @Test("Decode negative integer") func decodeNegativeInt() throws { let json = "-123" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value as? Int == -123) } @@ -73,7 +73,7 @@ struct AnyCodableTests { @Test("Decode zero") func decodeZero() throws { let json = "0" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value as? Int == 0) } @@ -83,7 +83,7 @@ struct AnyCodableTests { @Test("Decode positive double") func decodePositiveDouble() throws { let json = "3.14" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value as? Double == 3.14) } @@ -91,7 +91,7 @@ struct AnyCodableTests { @Test("Decode negative double") func decodeNegativeDouble() throws { let json = "-2.5" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value as? Double == -2.5) } @@ -99,7 +99,7 @@ struct AnyCodableTests { @Test("Decode double with scientific notation") func decodeScientificNotation() throws { let json = "1.23e-4" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value as? Double == 1.23e-4) } @@ -109,7 +109,7 @@ struct AnyCodableTests { @Test("Decode true") func decodeTrue() throws { let json = "true" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value as? Bool == true) } @@ -117,7 +117,7 @@ struct AnyCodableTests { @Test("Decode false") func decodeFalse() throws { let json = "false" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value as? Bool == false) } @@ -127,7 +127,7 @@ struct AnyCodableTests { @Test("Decode null value") func decodeNull() throws { let json = "null" - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let decoded = try JSONDecoder().decode(AnyCodable.self, from: data) #expect(decoded.value is NSNull) } @@ -179,7 +179,7 @@ struct AnyCodableTests { @Test("Decode invalid value throws error") func decodeInvalidValue() throws { let json = "[1, 2, 3]" // Arrays not supported - let data = json.data(using: .utf8)! + let data = Data(json.utf8) #expect(throws: DecodingError.self) { try JSONDecoder().decode(AnyCodable.self, from: data) } diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKeyTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKeyTests.swift index 949d3fc0..71bf442a 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKeyTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKeyTests.swift @@ -103,7 +103,7 @@ struct DynamicKeyTests { "anotherField": 123 } """ - let data = json.data(using: .utf8)! + let data = Data(json.utf8) struct TestWrapper: Decodable { let fields: [String: String] diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInputTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInputTests.swift index 8c0d53b0..c4f5f90a 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInputTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInputTests.swift @@ -43,7 +43,7 @@ struct FieldsInputTests { "title": "Hello World" } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -60,7 +60,7 @@ struct FieldsInputTests { "description": "" } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -79,7 +79,7 @@ struct FieldsInputTests { "count": 42 } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -96,7 +96,7 @@ struct FieldsInputTests { "temperature": -10 } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -113,7 +113,7 @@ struct FieldsInputTests { "balance": 0 } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -132,7 +132,7 @@ struct FieldsInputTests { "price": 19.99 } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -149,7 +149,7 @@ struct FieldsInputTests { "latitude": -33.8688 } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -168,7 +168,7 @@ struct FieldsInputTests { "isActive": true } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -185,7 +185,7 @@ struct FieldsInputTests { "isEnabled": false } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -207,7 +207,7 @@ struct FieldsInputTests { "active": true } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -231,7 +231,7 @@ struct FieldsInputTests { @Test("Decode empty object") func decodeEmptyObject() throws { let json = "{}" - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -247,7 +247,7 @@ struct FieldsInputTests { "name": "Test" } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let encoded = try JSONEncoder().encode(fieldsInput) @@ -266,7 +266,7 @@ struct FieldsInputTests { "count": 100 } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let encoded = try JSONEncoder().encode(fieldsInput) @@ -288,7 +288,7 @@ struct FieldsInputTests { "price": 15.50 } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let encoded = try JSONEncoder().encode(fieldsInput) @@ -307,7 +307,7 @@ struct FieldsInputTests { "field_name": "value" } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -322,7 +322,7 @@ struct FieldsInputTests { "firstName": "John" } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -339,7 +339,7 @@ struct FieldsInputTests { "description": " spaced text " } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() @@ -354,7 +354,7 @@ struct FieldsInputTests { "emoji": "🎉" } """ - let data = json.data(using: .utf8)! + let data = jsonData(json.utf8) let fieldsInput = try JSONDecoder().decode(FieldsInput.self, from: data) let fields = try fieldsInput.toFields() diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelperTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelperTests.swift index dcc7e83c..40c7d529 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelperTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelperTests.swift @@ -316,7 +316,7 @@ struct AuthenticationHelperTests { func resolveWebAuthTokenReturnsNilForEmpty() { let resolved = AuthenticationHelper.resolveWebAuthToken("") // Should return nil if environment variable not set - if ProcessInfo.processInfo.environment["CLOUDKIT_WEBAUTH_TOKEN"] == nil { + if ProcessInfo.processInfo.environment["CLOUDKIT_WEB_AUTH_TOKEN"] == nil { #expect(resolved == nil) } } @@ -324,8 +324,8 @@ struct AuthenticationHelperTests { @Test("resolveWebAuthToken checks environment variable") func resolveWebAuthTokenChecksEnvironment() { // Set environment variable temporarily - setenv("CLOUDKIT_WEBAUTH_TOKEN", "env-token", 1) - defer { unsetenv("CLOUDKIT_WEBAUTH_TOKEN") } + setenv("CLOUDKIT_WEB_AUTH_TOKEN", "env-token", 1) + defer { unsetenv("CLOUDKIT_WEB_AUTH_TOKEN") } let resolved = AuthenticationHelper.resolveWebAuthToken("") #expect(resolved == "env-token") diff --git a/Examples/MistDemo/examples/README.md b/Examples/MistDemo/examples/README.md index 971144ed..bafaaba8 100644 --- a/Examples/MistDemo/examples/README.md +++ b/Examples/MistDemo/examples/README.md @@ -19,6 +19,11 @@ This directory contains example scripts demonstrating how to use MistDemo's esse ./query-records.sh ``` +4. **Upload assets**: + ```bash + ./upload-asset.sh + ``` + ## Example Scripts ### 🔐 auth-flow.sh @@ -95,6 +100,35 @@ swift run mistdemo query --sort "createdAt:desc" --limit 5 swift run mistdemo query --fields "title,createdAt,priority" ``` +### 📤 upload-asset.sh +**Asset upload workflow examples** + +Shows how to upload binary assets to CloudKit: +- Upload image files to default Note.image field +- Upload to custom record types and fields +- Complete workflow: upload then create record +- Complete workflow: upload then update existing record +- Error handling for file size limits and invalid paths + +**Examples**: +```bash +# Simple upload to Note.image +swift run mistdemo upload-asset --file-path image.png + +# Upload to custom record type +swift run mistdemo upload-asset --file-path photo.jpg --record-type Photo --field-name thumbnail + +# Upload and get JSON output for record creation +swift run mistdemo upload-asset --file-path document.pdf --output json +``` + +**What it demonstrates**: +1. Binary asset upload to CloudKit CDN +2. AssetUploadReceipt containing receipt and checksums +3. Two-step workflow: upload asset, then associate with record +4. Error handling for missing files and size limits +5. Using asset metadata in subsequent record operations + ## Field Types MistDemo supports four CloudKit field types: @@ -134,7 +168,7 @@ All commands support multiple output formats: ### Environment Variables ```bash export CLOUDKIT_API_TOKEN=your_api_token -export CLOUDKIT_WEBAUTH_TOKEN=your_web_auth_token +export CLOUDKIT_WEB_AUTH_TOKEN=your_web_auth_token ``` ### Configuration File diff --git a/Examples/MistDemo/examples/auth-flow.sh b/Examples/MistDemo/examples/auth-flow.sh index 6888ac5f..2a6633cc 100755 --- a/Examples/MistDemo/examples/auth-flow.sh +++ b/Examples/MistDemo/examples/auth-flow.sh @@ -121,6 +121,6 @@ echo "Next steps:" echo "1. Use the saved configuration: swift run mistdemo --config-file $CONFIG_FILE " echo "2. Or set environment variables:" echo " export CLOUDKIT_API_TOKEN=$API_TOKEN" -echo " export CLOUDKIT_WEBAUTH_TOKEN=$WEB_AUTH_TOKEN" +echo " export CLOUDKIT_WEB_AUTH_TOKEN=$WEB_AUTH_TOKEN" echo "3. Create your first record: ./examples/create-record.sh" echo "4. Query records: ./examples/query-records.sh" \ No newline at end of file diff --git a/Examples/MistDemo/examples/create-record.sh b/Examples/MistDemo/examples/create-record.sh index 3bfcb81c..9ccb2b12 100755 --- a/Examples/MistDemo/examples/create-record.sh +++ b/Examples/MistDemo/examples/create-record.sh @@ -21,7 +21,7 @@ NC='\033[0m' # Configuration API_TOKEN="${CLOUDKIT_API_TOKEN}" -WEB_AUTH_TOKEN="${CLOUDKIT_WEBAUTH_TOKEN}" +WEB_AUTH_TOKEN="${CLOUDKIT_WEB_AUTH_TOKEN}" CONFIG_FILE="$HOME/.mistdemo/config.json" echo -e "${GREEN}📝 MistDemo Create Record Examples${NC}" @@ -37,7 +37,7 @@ if [ -z "$API_TOKEN" ] || [ -z "$WEB_AUTH_TOKEN" ]; then echo "❌ No authentication tokens found." echo "Run ./examples/auth-flow.sh first or set environment variables:" echo " export CLOUDKIT_API_TOKEN=your_api_token" - echo " export CLOUDKIT_WEBAUTH_TOKEN=your_web_auth_token" + echo " export CLOUDKIT_WEB_AUTH_TOKEN=your_web_auth_token" exit 1 fi fi diff --git a/Examples/MistDemo/examples/query-records.sh b/Examples/MistDemo/examples/query-records.sh index 45b8ed4f..b38a7356 100755 --- a/Examples/MistDemo/examples/query-records.sh +++ b/Examples/MistDemo/examples/query-records.sh @@ -20,7 +20,7 @@ NC='\033[0m' # Configuration API_TOKEN="${CLOUDKIT_API_TOKEN}" -WEB_AUTH_TOKEN="${CLOUDKIT_WEBAUTH_TOKEN}" +WEB_AUTH_TOKEN="${CLOUDKIT_WEB_AUTH_TOKEN}" CONFIG_FILE="$HOME/.mistdemo/config.json" echo -e "${GREEN}🔍 MistDemo Query Examples${NC}" @@ -36,7 +36,7 @@ if [ -z "$API_TOKEN" ] || [ -z "$WEB_AUTH_TOKEN" ]; then echo "❌ No authentication tokens found." echo "Run ./examples/auth-flow.sh first or set environment variables:" echo " export CLOUDKIT_API_TOKEN=your_api_token" - echo " export CLOUDKIT_WEBAUTH_TOKEN=your_web_auth_token" + echo " export CLOUDKIT_WEB_AUTH_TOKEN=your_web_auth_token" exit 1 fi fi diff --git a/Examples/MistDemo/examples/upload-asset.sh b/Examples/MistDemo/examples/upload-asset.sh new file mode 100755 index 00000000..95b9d02a --- /dev/null +++ b/Examples/MistDemo/examples/upload-asset.sh @@ -0,0 +1,157 @@ +#!/bin/bash +# +# upload-asset.sh +# Asset upload example for MistDemo +# +# This script demonstrates various asset upload workflows: +# 1. Upload an image asset +# 2. Upload with custom record type +# 3. Complete workflow: upload then create record +# 4. Complete workflow: upload then update existing record +# 5. Error handling scenarios +# + +set -e + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' + +# Configuration +API_TOKEN="${CLOUDKIT_API_TOKEN}" +WEB_AUTH_TOKEN="${CLOUDKIT_WEB_AUTH_TOKEN}" +CONFIG_FILE="$HOME/.mistdemo/config.json" + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}MistDemo Asset Upload Examples${NC}" +echo -e "${BLUE}========================================${NC}\n" + +# Load configuration if tokens not provided +if [ -z "$API_TOKEN" ] || [ -z "$WEB_AUTH_TOKEN" ]; then + if [ -f "$CONFIG_FILE" ]; then + echo -e "${YELLOW}📋 Loading configuration from $CONFIG_FILE${NC}" + API_TOKEN=$(cat "$CONFIG_FILE" | jq -r '.api_token') + WEB_AUTH_TOKEN=$(cat "$CONFIG_FILE" | jq -r '.web_auth_token') + else + echo -e "${RED}❌ No authentication tokens found.${NC}" + echo "Run ./examples/auth-flow.sh first or set environment variables:" + echo " export CLOUDKIT_API_TOKEN=your_api_token" + echo " export CLOUDKIT_WEB_AUTH_TOKEN=your_web_auth_token" + exit 1 + fi +fi + +# Common parameters +COMMON_ARGS="--api-token $API_TOKEN --web-auth-token $WEB_AUTH_TOKEN" + +# Create temporary test files +TEMP_DIR=$(mktemp -d) +TEST_IMAGE="$TEMP_DIR/test-image.png" +TEST_LARGE="$TEMP_DIR/test-large.bin" + +echo -e "${YELLOW}📁 Creating test files in $TEMP_DIR${NC}\n" + +# Create a small test image (1x1 PNG - 67 bytes) +echo -n "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" | base64 -d > "$TEST_IMAGE" + +echo "" +echo -e "${GREEN}Example 1: Upload image to default Note.image field${NC}" +echo "Command: swift run mistdemo upload-asset $COMMON_ARGS --file-path \"$TEST_IMAGE\"" +echo "" +swift run mistdemo upload-asset $COMMON_ARGS --file-path "$TEST_IMAGE" + +echo "" +echo -e "${GREEN}Example 2: Upload to custom record type and field${NC}" +echo "Command: swift run mistdemo upload-asset $COMMON_ARGS --file-path \"$TEST_IMAGE\" --record-type Photo --field-name thumbnail" +echo "" +swift run mistdemo upload-asset $COMMON_ARGS --file-path "$TEST_IMAGE" --record-type Photo --field-name thumbnail + +echo "" +echo -e "${GREEN}Example 3: Complete workflow - Upload asset then create record${NC}" +echo -e "${YELLOW}Step 1: Upload asset and capture output${NC}" +echo "Command: swift run mistdemo upload-asset $COMMON_ARGS --file-path \"$TEST_IMAGE\" --output json" +echo "" + +UPLOAD_OUTPUT=$(swift run mistdemo upload-asset $COMMON_ARGS --file-path "$TEST_IMAGE" --output json) +echo "$UPLOAD_OUTPUT" | jq . + +# Extract asset data from upload result +ASSET_RECEIPT=$(echo "$UPLOAD_OUTPUT" | jq -r '.asset.receipt') +ASSET_CHECKSUM=$(echo "$UPLOAD_OUTPUT" | jq -r '.asset.fileChecksum') + +echo "" +echo -e "${YELLOW}Step 2: Create record with asset field${NC}" +echo "Note: The upload-asset command returns an AssetUploadReceipt containing the complete asset dictionary" +echo " Use this asset data when creating or updating records with asset fields" +echo "" + +# For demonstration purposes, show how you would use the asset in a record creation +echo "Example create command (requires asset field support in create command):" +echo "swift run mistdemo create $COMMON_ARGS \\" +echo " --record-type Note \\" +echo " --field \"title:string:Photo Note\" \\" +echo " --asset-field \"image:asset:receipt=$ASSET_RECEIPT,checksum=$ASSET_CHECKSUM\"" + +echo "" +echo -e "${GREEN}Example 4: Update existing record with asset${NC}" +echo -e "${YELLOW}Step 1: Create a record first${NC}" +echo "" + +CREATE_OUTPUT=$(swift run mistdemo create $COMMON_ARGS --field "title:string:Asset Test" --output json) +RECORD_NAME=$(echo "$CREATE_OUTPUT" | jq -r '.recordName') +RECORD_CHANGE_TAG=$(echo "$CREATE_OUTPUT" | jq -r '.recordChangeTag') + +echo "Created record: $RECORD_NAME" +echo "Change tag: $RECORD_CHANGE_TAG" + +echo "" +echo -e "${YELLOW}Step 2: Upload asset for this record${NC}" +echo "" +swift run mistdemo upload-asset $COMMON_ARGS --file-path "$TEST_IMAGE" --record-name "$RECORD_NAME" + +echo "" +echo -e "${YELLOW}Step 3: Update record with asset field${NC}" +echo "Note: Similar to create, you would use the asset data from the upload result" +echo "" + +echo "" +echo -e "${GREEN}Example 5: Error handling scenarios${NC}" +echo "" + +# Test file size validation +echo -e "${YELLOW}Testing file size validation (create 300MB file - should fail)...${NC}" +echo "CloudKit asset upload has size limits. Large files will be rejected." +echo "" + +# Create a 1KB file instead of 300MB for demo purposes (300MB would take too long) +dd if=/dev/zero of="$TEST_LARGE" bs=1024 count=1 2>/dev/null +echo "Created 1KB test file (would be 300MB in real scenario)" +echo "" + +echo "Command: swift run mistdemo upload-asset $COMMON_ARGS --file-path \"$TEST_LARGE\"" +swift run mistdemo upload-asset $COMMON_ARGS --file-path "$TEST_LARGE" || echo -e "${RED}Expected: Large file upload might fail with CloudKit limits${NC}" + +echo "" +echo -e "${YELLOW}Testing invalid file path...${NC}" +swift run mistdemo upload-asset $COMMON_ARGS --file-path "/nonexistent/file.png" 2>&1 || echo -e "${RED}Expected: File not found error${NC}" + +echo "" +echo -e "\n${BLUE}Cleaning up temporary files...${NC}" +rm -rf "$TEMP_DIR" +echo -e "${GREEN}✓ Cleanup complete${NC}" + +echo "" +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}Asset Upload Examples Complete${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" +echo -e "${YELLOW}💡 Key Takeaways:${NC}" +echo " • upload-asset returns AssetUploadReceipt with complete asset metadata" +echo " • Asset includes receipt, checksums, and download URL" +echo " • Use this asset data when creating/updating records with asset fields" +echo " • CloudKit enforces file size limits on uploads" +echo " • Assets must be associated with record fields via subsequent operations" +echo "" diff --git a/Sources/MistKit/Authentication/AdaptiveTokenManager+Transitions.swift b/Sources/MistKit/Authentication/AdaptiveTokenManager+Transitions.swift index 1a23083f..6c8ee0b2 100644 --- a/Sources/MistKit/Authentication/AdaptiveTokenManager+Transitions.swift +++ b/Sources/MistKit/Authentication/AdaptiveTokenManager+Transitions.swift @@ -81,7 +81,10 @@ extension AdaptiveTokenManager { } catch { // Don't fail silently - log the storage error but continue with the upgrade // This ensures the authentication upgrade succeeds even if storage fails - print("Warning: Failed to store credentials after upgrade: \(error.localizedDescription)") + MistKitLogger.logWarning( + "Failed to store credentials after upgrade: \(error.localizedDescription)", + logger: MistKitLogger.auth + ) // Could also throw here if storage failure should be fatal: // throw TokenManagerError.internalError( // reason: "Failed to store credentials: \(error.localizedDescription)" diff --git a/Sources/MistKit/Core/AssetUploader.swift b/Sources/MistKit/Core/AssetUploader.swift new file mode 100644 index 00000000..2571c8ad --- /dev/null +++ b/Sources/MistKit/Core/AssetUploader.swift @@ -0,0 +1,38 @@ +// +// AssetUploader.swift +// MistKit +// +// Created by Claude on 2026-02-03. +// + +public import Foundation + +/// Closure for uploading binary asset data to a CloudKit CDN URL. +/// +/// **⚠️ CRITICAL: Transport Separation Required** +/// +/// Custom implementations MUST maintain connection pool separation from the CloudKit API: +/// - Use a separate URLSession instance, NOT the CloudKit API transport +/// - Do NOT share HTTP/2 connections with api.apple-cloudkit.com +/// - The default implementation uses `URLSession.shared.upload(_:to:)` +/// +/// **Why Separate Connection Pools?** +/// +/// CloudKit asset uploads target the CDN (cvws.icloud-content.com) rather than the +/// API host (api.apple-cloudkit.com). Reusing the same HTTP/2 connection for both +/// hosts causes 421 Misdirected Request errors due to HTTP/2's host validation rules. +/// +/// **When to Provide Custom Implementation:** +/// - Unit testing (mock responses without network calls) +/// - Specialized CDN configurations +/// - **NOT for production use** - use the default URLSession.shared implementation +/// +/// Returns the raw HTTP response (status code and data) without decoding. +/// CloudKitService handles JSON decoding of the response data. +/// +/// - Parameters: +/// - data: Binary asset data to upload +/// - url: CloudKit CDN upload URL +/// - Returns: Tuple containing optional HTTP status code and response data +/// - Throws: Any error that occurs during upload +public typealias AssetUploader = (Data, URL) async throws -> (statusCode: Int?, data: Data) diff --git a/Sources/MistKit/EnvironmentConfig.swift b/Sources/MistKit/EnvironmentConfig.swift index 3e2c794b..f6a54fb9 100644 --- a/Sources/MistKit/EnvironmentConfig.swift +++ b/Sources/MistKit/EnvironmentConfig.swift @@ -35,6 +35,9 @@ public enum EnvironmentConfig { public enum Keys { /// CloudKit API token environment variable key public static let cloudKitAPIToken = "CLOUDKIT_API_TOKEN" + + /// CloudKit Web Auth token environment variable key + public static let cloudKitWebAuthToken = "CLOUDKIT_WEB_AUTH_TOKEN" } /// CloudKit-specific environment utilities @@ -47,6 +50,7 @@ public enum EnvironmentConfig { // Check for CloudKit-related environment variables let cloudKitKeys = [ "CLOUDKIT_API_TOKEN", + "CLOUDKIT_WEB_AUTH_TOKEN", "CLOUDKIT_CONTAINER_ID", "CLOUDKIT_ENVIRONMENT", "CLOUDKIT_DATABASE", diff --git a/Sources/MistKit/Extensions/OpenAPI/Components+FieldValue.swift b/Sources/MistKit/Extensions/OpenAPI/Components+FieldValue.swift index a69bd33a..cfd8be98 100644 --- a/Sources/MistKit/Extensions/OpenAPI/Components+FieldValue.swift +++ b/Sources/MistKit/Extensions/OpenAPI/Components+FieldValue.swift @@ -29,23 +29,25 @@ internal import Foundation -/// Extension to convert MistKit FieldValue to OpenAPI Components.Schemas.FieldValue +/// Extension to convert MistKit FieldValue to OpenAPI FieldValueRequest for API requests @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) -extension Components.Schemas.FieldValue { - /// Initialize from MistKit FieldValue +extension Components.Schemas.FieldValueRequest { + /// Initialize from MistKit FieldValue for CloudKit API requests. + /// + /// CloudKit infers field types from the value structure, so no type field is sent. internal init(from fieldValue: FieldValue) { switch fieldValue { case .string(let value): - self.init(value: .stringValue(value), type: .string) + self.init(value: .StringValue(value)) case .int64(let value): - self.init(value: .int64Value(value), type: .int64) + self.init(value: .Int64Value(Int64(value))) case .double(let value): - self.init(value: .doubleValue(value), type: .double) + self.init(value: .DoubleValue(value)) case .bytes(let value): - self.init(value: .bytesValue(value), type: .bytes) + self.init(value: .BytesValue(value)) case .date(let value): - let milliseconds = Int64(value.timeIntervalSince1970 * 1_000) - self.init(value: .dateValue(Double(milliseconds)), type: .timestamp) + let milliseconds = value.timeIntervalSince1970 * 1_000 + self.init(value: .DateValue(milliseconds)) case .location(let location): self.init(location: location) case .reference(let reference): @@ -69,7 +71,7 @@ extension Components.Schemas.FieldValue { course: location.course, timestamp: location.timestamp.map { $0.timeIntervalSince1970 * 1_000 } ) - self.init(value: .locationValue(locationValue), type: .location) + self.init(value: .LocationValue(locationValue)) } /// Initialize from Reference to Components ReferenceValue @@ -87,7 +89,7 @@ extension Components.Schemas.FieldValue { recordName: reference.recordName, action: action ) - self.init(value: .referenceValue(referenceValue), type: .reference) + self.init(value: .ReferenceValue(referenceValue)) } /// Initialize from Asset to Components AssetValue @@ -100,12 +102,12 @@ extension Components.Schemas.FieldValue { receipt: asset.receipt, downloadURL: asset.downloadURL ) - self.init(value: .assetValue(assetValue), type: .asset) + self.init(value: .AssetValue(assetValue)) } /// Initialize from List to Components list value private init(list: [FieldValue]) { - let listValues = list.map { CustomFieldValue.CustomFieldValuePayload($0) } - self.init(value: .listValue(listValues), type: .list) + let listValues = list.map { Components.Schemas.ListValuePayload(from: $0) } + self.init(value: .ListValue(listValues)) } } diff --git a/Sources/MistKit/Extensions/OpenAPI/Components+ListValuePayload.swift b/Sources/MistKit/Extensions/OpenAPI/Components+ListValuePayload.swift new file mode 100644 index 00000000..3f4b4512 --- /dev/null +++ b/Sources/MistKit/Extensions/OpenAPI/Components+ListValuePayload.swift @@ -0,0 +1,90 @@ +// +// Components+ListValuePayload.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +extension Components.Schemas.ListValuePayload { + /// Initialize from MistKit FieldValue for list elements + internal init(from fieldValue: FieldValue) { + switch fieldValue { + case .string(let value): + self = .StringValue(value) + case .int64(let value): + self = .Int64Value(Int64(value)) + case .double(let value): + self = .DoubleValue(value) + case .bytes(let value): + self = .BytesValue(value) + case .date(let value): + let milliseconds = value.timeIntervalSince1970 * 1_000 + self = .DateValue(milliseconds) + case .location(let location): + let locationValue = Components.Schemas.LocationValue( + latitude: location.latitude, + longitude: location.longitude, + horizontalAccuracy: location.horizontalAccuracy, + verticalAccuracy: location.verticalAccuracy, + altitude: location.altitude, + speed: location.speed, + course: location.course, + timestamp: location.timestamp.map { $0.timeIntervalSince1970 * 1_000 } + ) + self = .LocationValue(locationValue) + case .reference(let reference): + let action: Components.Schemas.ReferenceValue.actionPayload? + switch reference.action { + case .some(.deleteSelf): + action = .DELETE_SELF + case .some(.none): + action = .NONE + case nil: + action = nil + } + let referenceValue = Components.Schemas.ReferenceValue( + recordName: reference.recordName, + action: action + ) + self = .ReferenceValue(referenceValue) + case .asset(let asset): + let assetValue = Components.Schemas.AssetValue( + fileChecksum: asset.fileChecksum, + size: asset.size, + referenceChecksum: asset.referenceChecksum, + wrappingKey: asset.wrappingKey, + receipt: asset.receipt, + downloadURL: asset.downloadURL + ) + self = .AssetValue(assetValue) + case .list(let nestedList): + // Recursively convert nested lists + let nestedPayloads = nestedList.map { Self(from: $0) } + self = .ListValue(nestedPayloads) + } + } +} diff --git a/Sources/MistKit/Extensions/OpenAPI/Components+RecordOperation.swift b/Sources/MistKit/Extensions/OpenAPI/Components+RecordOperation.swift index 83ee73a0..8b38cd68 100644 --- a/Sources/MistKit/Extensions/OpenAPI/Components+RecordOperation.swift +++ b/Sources/MistKit/Extensions/OpenAPI/Components+RecordOperation.swift @@ -51,10 +51,10 @@ extension Components.Schemas.RecordOperation { fatalError("Unknown operation type: \(recordOperation.operationType)") } - // Convert fields to OpenAPI FieldValue format + // Convert fields to OpenAPI FieldValueRequest format (for requests) let apiFields = recordOperation.fields.mapValues { - fieldValue -> Components.Schemas.FieldValue in - Components.Schemas.FieldValue(from: fieldValue) + fieldValue -> Components.Schemas.FieldValueRequest in + Components.Schemas.FieldValueRequest(from: fieldValue) } // Build the OpenAPI record operation diff --git a/Sources/MistKit/Extensions/URLRequest+AssetUpload.swift b/Sources/MistKit/Extensions/URLRequest+AssetUpload.swift new file mode 100644 index 00000000..fda8e5e7 --- /dev/null +++ b/Sources/MistKit/Extensions/URLRequest+AssetUpload.swift @@ -0,0 +1,48 @@ +// +// URLRequest+AssetUpload.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +#if canImport(FoundationNetworking) +public import FoundationNetworking +#endif + +#if !os(WASI) +extension URLRequest { + /// Initialize URLRequest for CloudKit asset upload + /// - Parameters: + /// - data: Binary asset data to upload + /// - url: CloudKit CDN upload URL + internal init(forAssetUpload data: Data, to url: URL) { + self.init(url: url) + self.httpMethod = "POST" + self.httpBody = data + self.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") + } +} +#endif diff --git a/Sources/MistKit/Extensions/URLSession+AssetUpload.swift b/Sources/MistKit/Extensions/URLSession+AssetUpload.swift new file mode 100644 index 00000000..b95ccd74 --- /dev/null +++ b/Sources/MistKit/Extensions/URLSession+AssetUpload.swift @@ -0,0 +1,57 @@ +// +// URLSession+AssetUpload.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +#if canImport(FoundationNetworking) +public import FoundationNetworking +#endif + +#if !os(WASI) +extension URLSession { + /// Upload asset data directly to CloudKit CDN + /// + /// Returns the raw HTTP response without decoding. CloudKitService handles JSON decoding. + /// + /// - Parameters: + /// - data: Binary data to upload + /// - url: CloudKit CDN upload URL + /// - Returns: Tuple containing optional HTTP status code and response data + /// - Throws: Error if upload fails + public func upload(_ data: Data, to url: URL) async throws -> (statusCode: Int?, data: Data) { + // Create URLRequest for direct upload to CDN + let request = URLRequest(forAssetUpload: data, to: url) + + // Upload directly via URLSession + let (responseData, response) = try await self.data(for: request) + + let statusCode = (response as? HTTPURLResponse)?.statusCode + return (statusCode, responseData) + } +} +#endif diff --git a/Sources/MistKit/Generated/Client.swift b/Sources/MistKit/Generated/Client.swift index 3b7fdf81..d441a573 100644 --- a/Sources/MistKit/Generated/Client.swift +++ b/Sources/MistKit/Generated/Client.swift @@ -2898,9 +2898,14 @@ internal struct Client: APIProtocol { } ) } - /// Upload Assets + /// Request Asset Upload URLs + /// + /// Request upload URLs for asset fields. This is the first step in a two-step process: + /// 1. Request upload URLs by specifying the record type and field name + /// 2. Upload the actual binary data to the returned URL (separate HTTP request) + /// + /// Upload URLs are valid for 15 minutes. Maximum file size is 15 MB. /// - /// Upload binary assets to CloudKit /// /// - Remark: HTTP `POST /database/{version}/{container}/{environment}/{database}/assets/upload`. /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/upload/post(uploadAssets)`. @@ -2929,38 +2934,11 @@ internal struct Client: APIProtocol { ) let body: OpenAPIRuntime.HTTPBody? switch input.body { - case let .multipartForm(value): - body = try converter.setRequiredRequestBodyAsMultipart( + case let .json(value): + body = try converter.setRequiredRequestBodyAsJSON( value, headerFields: &request.headerFields, - contentType: "multipart/form-data", - allowsUnknownParts: true, - requiredExactlyOncePartNames: [], - requiredAtLeastOncePartNames: [], - atMostOncePartNames: [ - "file" - ], - zeroOrMoreTimesPartNames: [], - encoding: { part in - switch part { - case let .file(wrapped): - var headerFields: HTTPTypes.HTTPFields = .init() - let value = wrapped.payload - let body = try converter.setRequiredRequestBodyAsBinary( - value.body, - headerFields: &headerFields, - contentType: "application/octet-stream" - ) - return .init( - name: "file", - filename: wrapped.filename, - headerFields: headerFields, - body: body - ) - case let .undocumented(value): - return value - } - } + contentType: "application/json; charset=utf-8" ) } return (request, body) diff --git a/Sources/MistKit/Generated/Types.swift b/Sources/MistKit/Generated/Types.swift index ea8bd6ce..716d7901 100644 --- a/Sources/MistKit/Generated/Types.swift +++ b/Sources/MistKit/Generated/Types.swift @@ -112,9 +112,14 @@ internal protocol APIProtocol: Sendable { /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/users/lookup/contacts/post(lookupContacts)`. @available(*, deprecated) func lookupContacts(_ input: Operations.lookupContacts.Input) async throws -> Operations.lookupContacts.Output - /// Upload Assets + /// Request Asset Upload URLs + /// + /// Request upload URLs for asset fields. This is the first step in a two-step process: + /// 1. Request upload URLs by specifying the record type and field name + /// 2. Upload the actual binary data to the returned URL (separate HTTP request) + /// + /// Upload URLs are valid for 15 minutes. Maximum file size is 15 MB. /// - /// Upload binary assets to CloudKit /// /// - Remark: HTTP `POST /database/{version}/{container}/{environment}/{database}/assets/upload`. /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/upload/post(uploadAssets)`. @@ -370,9 +375,14 @@ extension APIProtocol { body: body )) } - /// Upload Assets + /// Request Asset Upload URLs + /// + /// Request upload URLs for asset fields. This is the first step in a two-step process: + /// 1. Request upload URLs by specifying the record type and field name + /// 2. Upload the actual binary data to the returned URL (separate HTTP request) + /// + /// Upload URLs are valid for 15 minutes. Maximum file size is 15 MB. /// - /// Upload binary assets to CloudKit /// /// - Remark: HTTP `POST /database/{version}/{container}/{environment}/{database}/assets/upload`. /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/upload/post(uploadAssets)`. @@ -499,7 +509,7 @@ internal enum Components { /// - Remark: Generated from `#/components/schemas/Filter/fieldName`. internal var fieldName: Swift.String? /// - Remark: Generated from `#/components/schemas/Filter/fieldValue`. - internal var fieldValue: Components.Schemas.FieldValue? + internal var fieldValue: Components.Schemas.FieldValueRequest? /// Creates a new `Filter`. /// /// - Parameters: @@ -509,7 +519,7 @@ internal enum Components { internal init( comparator: Components.Schemas.Filter.comparatorPayload? = nil, fieldName: Swift.String? = nil, - fieldValue: Components.Schemas.FieldValue? = nil + fieldValue: Components.Schemas.FieldValueRequest? = nil ) { self.comparator = comparator self.fieldName = fieldName @@ -559,7 +569,7 @@ internal enum Components { /// - Remark: Generated from `#/components/schemas/RecordOperation/operationType`. internal var operationType: Components.Schemas.RecordOperation.operationTypePayload? /// - Remark: Generated from `#/components/schemas/RecordOperation/record`. - internal var record: Components.Schemas.Record? + internal var record: Components.Schemas.RecordRequest? /// Creates a new `RecordOperation`. /// /// - Parameters: @@ -567,7 +577,7 @@ internal enum Components { /// - record: internal init( operationType: Components.Schemas.RecordOperation.operationTypePayload? = nil, - record: Components.Schemas.Record? = nil + record: Components.Schemas.RecordRequest? = nil ) { self.operationType = operationType self.record = record @@ -577,31 +587,33 @@ internal enum Components { case record } } - /// - Remark: Generated from `#/components/schemas/Record`. - internal struct Record: Codable, Hashable, Sendable { + /// Record schema for API requests (fields use FieldValueRequest) + /// + /// - Remark: Generated from `#/components/schemas/RecordRequest`. + internal struct RecordRequest: Codable, Hashable, Sendable { /// The unique identifier for the record /// - /// - Remark: Generated from `#/components/schemas/Record/recordName`. + /// - Remark: Generated from `#/components/schemas/RecordRequest/recordName`. internal var recordName: Swift.String? /// The record type (schema name) /// - /// - Remark: Generated from `#/components/schemas/Record/recordType`. + /// - Remark: Generated from `#/components/schemas/RecordRequest/recordType`. internal var recordType: Swift.String? /// Change tag for optimistic concurrency control /// - /// - Remark: Generated from `#/components/schemas/Record/recordChangeTag`. + /// - Remark: Generated from `#/components/schemas/RecordRequest/recordChangeTag`. internal var recordChangeTag: Swift.String? - /// Record fields with their values and types + /// Record fields with their values (no type metadata) /// - /// - Remark: Generated from `#/components/schemas/Record/fields`. + /// - Remark: Generated from `#/components/schemas/RecordRequest/fields`. internal struct fieldsPayload: Codable, Hashable, Sendable { /// A container of undocumented properties. - internal var additionalProperties: [String: Components.Schemas.FieldValue] + internal var additionalProperties: [String: Components.Schemas.FieldValueRequest] /// Creates a new `fieldsPayload`. /// /// - Parameters: /// - additionalProperties: A container of undocumented properties. - internal init(additionalProperties: [String: Components.Schemas.FieldValue] = .init()) { + internal init(additionalProperties: [String: Components.Schemas.FieldValueRequest] = .init()) { self.additionalProperties = additionalProperties } internal init(from decoder: any Decoder) throws { @@ -611,22 +623,22 @@ internal enum Components { try encoder.encodeAdditionalProperties(additionalProperties) } } - /// Record fields with their values and types + /// Record fields with their values (no type metadata) /// - /// - Remark: Generated from `#/components/schemas/Record/fields`. - internal var fields: Components.Schemas.Record.fieldsPayload? - /// Creates a new `Record`. + /// - Remark: Generated from `#/components/schemas/RecordRequest/fields`. + internal var fields: Components.Schemas.RecordRequest.fieldsPayload? + /// Creates a new `RecordRequest`. /// /// - Parameters: /// - recordName: The unique identifier for the record /// - recordType: The record type (schema name) /// - recordChangeTag: Change tag for optimistic concurrency control - /// - fields: Record fields with their values and types + /// - fields: Record fields with their values (no type metadata) internal init( recordName: Swift.String? = nil, recordType: Swift.String? = nil, recordChangeTag: Swift.String? = nil, - fields: Components.Schemas.Record.fieldsPayload? = nil + fields: Components.Schemas.RecordRequest.fieldsPayload? = nil ) { self.recordName = recordName self.recordType = recordType @@ -640,10 +652,344 @@ internal enum Components { case fields } } - /// A CloudKit field value with its type information + /// Record schema for API responses (fields use FieldValueResponse) + /// + /// - Remark: Generated from `#/components/schemas/RecordResponse`. + internal struct RecordResponse: Codable, Hashable, Sendable { + /// The unique identifier for the record + /// + /// - Remark: Generated from `#/components/schemas/RecordResponse/recordName`. + internal var recordName: Swift.String? + /// The record type (schema name) + /// + /// - Remark: Generated from `#/components/schemas/RecordResponse/recordType`. + internal var recordType: Swift.String? + /// Change tag for optimistic concurrency control + /// + /// - Remark: Generated from `#/components/schemas/RecordResponse/recordChangeTag`. + internal var recordChangeTag: Swift.String? + /// Record fields with their values and optional type information + /// + /// - Remark: Generated from `#/components/schemas/RecordResponse/fields`. + internal struct fieldsPayload: Codable, Hashable, Sendable { + /// A container of undocumented properties. + internal var additionalProperties: [String: Components.Schemas.FieldValueResponse] + /// Creates a new `fieldsPayload`. + /// + /// - Parameters: + /// - additionalProperties: A container of undocumented properties. + internal init(additionalProperties: [String: Components.Schemas.FieldValueResponse] = .init()) { + self.additionalProperties = additionalProperties + } + internal init(from decoder: any Decoder) throws { + additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: []) + } + internal func encode(to encoder: any Encoder) throws { + try encoder.encodeAdditionalProperties(additionalProperties) + } + } + /// Record fields with their values and optional type information + /// + /// - Remark: Generated from `#/components/schemas/RecordResponse/fields`. + internal var fields: Components.Schemas.RecordResponse.fieldsPayload? + /// Creates a new `RecordResponse`. + /// + /// - Parameters: + /// - recordName: The unique identifier for the record + /// - recordType: The record type (schema name) + /// - recordChangeTag: Change tag for optimistic concurrency control + /// - fields: Record fields with their values and optional type information + internal init( + recordName: Swift.String? = nil, + recordType: Swift.String? = nil, + recordChangeTag: Swift.String? = nil, + fields: Components.Schemas.RecordResponse.fieldsPayload? = nil + ) { + self.recordName = recordName + self.recordType = recordType + self.recordChangeTag = recordChangeTag + self.fields = fields + } + internal enum CodingKeys: String, CodingKey { + case recordName + case recordType + case recordChangeTag + case fields + } + } + /// A CloudKit field value for API requests. + /// The type field is omitted as CloudKit infers types from the value structure. + /// + /// + /// - Remark: Generated from `#/components/schemas/FieldValueRequest`. + internal struct FieldValueRequest: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value`. + internal enum valuePayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value/case1`. + case StringValue(Components.Schemas.StringValue) + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value/case2`. + case Int64Value(Components.Schemas.Int64Value) + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value/case3`. + case DoubleValue(Components.Schemas.DoubleValue) + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value/case4`. + case BytesValue(Components.Schemas.BytesValue) + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value/case5`. + case DateValue(Components.Schemas.DateValue) + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value/case6`. + case LocationValue(Components.Schemas.LocationValue) + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value/case7`. + case ReferenceValue(Components.Schemas.ReferenceValue) + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value/case8`. + case AssetValue(Components.Schemas.AssetValue) + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value/case9`. + case ListValue(Components.Schemas.ListValue) + internal init(from decoder: any Decoder) throws { + var errors: [any Error] = [] + do { + self = .StringValue(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + do { + self = .Int64Value(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + do { + self = .DoubleValue(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + do { + self = .BytesValue(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + do { + self = .DateValue(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + do { + self = .LocationValue(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + do { + self = .ReferenceValue(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + do { + self = .AssetValue(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + do { + self = .ListValue(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + throw Swift.DecodingError.failedToDecodeOneOfSchema( + type: Self.self, + codingPath: decoder.codingPath, + errors: errors + ) + } + internal func encode(to encoder: any Encoder) throws { + switch self { + case let .StringValue(value): + try encoder.encodeToSingleValueContainer(value) + case let .Int64Value(value): + try encoder.encodeToSingleValueContainer(value) + case let .DoubleValue(value): + try encoder.encodeToSingleValueContainer(value) + case let .BytesValue(value): + try encoder.encodeToSingleValueContainer(value) + case let .DateValue(value): + try encoder.encodeToSingleValueContainer(value) + case let .LocationValue(value): + try value.encode(to: encoder) + case let .ReferenceValue(value): + try value.encode(to: encoder) + case let .AssetValue(value): + try value.encode(to: encoder) + case let .ListValue(value): + try encoder.encodeToSingleValueContainer(value) + } + } + } + /// - Remark: Generated from `#/components/schemas/FieldValueRequest/value`. + internal var value: Components.Schemas.FieldValueRequest.valuePayload + /// Creates a new `FieldValueRequest`. + /// + /// - Parameters: + /// - value: + internal init(value: Components.Schemas.FieldValueRequest.valuePayload) { + self.value = value + } + internal enum CodingKeys: String, CodingKey { + case value + } + } + /// A CloudKit field value from API responses. + /// May include optional type field for explicit type information. /// - /// - Remark: Generated from `#/components/schemas/FieldValue`. - internal typealias FieldValue = CustomFieldValue + /// + /// - Remark: Generated from `#/components/schemas/FieldValueResponse`. + internal struct FieldValueResponse: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value`. + internal enum valuePayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value/case1`. + case StringValue(Components.Schemas.StringValue) + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value/case2`. + case Int64Value(Components.Schemas.Int64Value) + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value/case3`. + case DoubleValue(Components.Schemas.DoubleValue) + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value/case4`. + case BytesValue(Components.Schemas.BytesValue) + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value/case5`. + case DateValue(Components.Schemas.DateValue) + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value/case6`. + case LocationValue(Components.Schemas.LocationValue) + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value/case7`. + case ReferenceValue(Components.Schemas.ReferenceValue) + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value/case8`. + case AssetValue(Components.Schemas.AssetValue) + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value/case9`. + case ListValue(Components.Schemas.ListValue) + internal init(from decoder: any Decoder) throws { + var errors: [any Error] = [] + do { + self = .StringValue(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + do { + self = .Int64Value(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + do { + self = .DoubleValue(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + do { + self = .BytesValue(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + do { + self = .DateValue(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + do { + self = .LocationValue(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + do { + self = .ReferenceValue(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + do { + self = .AssetValue(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + do { + self = .ListValue(try decoder.decodeFromSingleValueContainer()) + return + } catch { + errors.append(error) + } + throw Swift.DecodingError.failedToDecodeOneOfSchema( + type: Self.self, + codingPath: decoder.codingPath, + errors: errors + ) + } + internal func encode(to encoder: any Encoder) throws { + switch self { + case let .StringValue(value): + try encoder.encodeToSingleValueContainer(value) + case let .Int64Value(value): + try encoder.encodeToSingleValueContainer(value) + case let .DoubleValue(value): + try encoder.encodeToSingleValueContainer(value) + case let .BytesValue(value): + try encoder.encodeToSingleValueContainer(value) + case let .DateValue(value): + try encoder.encodeToSingleValueContainer(value) + case let .LocationValue(value): + try value.encode(to: encoder) + case let .ReferenceValue(value): + try value.encode(to: encoder) + case let .AssetValue(value): + try value.encode(to: encoder) + case let .ListValue(value): + try encoder.encodeToSingleValueContainer(value) + } + } + } + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/value`. + internal var value: Components.Schemas.FieldValueResponse.valuePayload + /// The CloudKit field type (optional, may be inferred from value) + /// + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/type`. + internal enum _typePayload: String, Codable, Hashable, Sendable, CaseIterable { + case STRING = "STRING" + case INT64 = "INT64" + case DOUBLE = "DOUBLE" + case BYTES = "BYTES" + case REFERENCE = "REFERENCE" + case ASSET = "ASSET" + case ASSETID = "ASSETID" + case LOCATION = "LOCATION" + case TIMESTAMP = "TIMESTAMP" + case LIST = "LIST" + } + /// The CloudKit field type (optional, may be inferred from value) + /// + /// - Remark: Generated from `#/components/schemas/FieldValueResponse/type`. + internal var _type: Components.Schemas.FieldValueResponse._typePayload? + /// Creates a new `FieldValueResponse`. + /// + /// - Parameters: + /// - value: + /// - _type: The CloudKit field type (optional, may be inferred from value) + internal init( + value: Components.Schemas.FieldValueResponse.valuePayload, + _type: Components.Schemas.FieldValueResponse._typePayload? = nil + ) { + self.value = value + self._type = _type + } + internal enum CodingKeys: String, CodingKey { + case value + case _type = "type" + } + } /// A text string value /// /// - Remark: Generated from `#/components/schemas/StringValue`. @@ -1076,7 +1422,7 @@ internal enum Components { /// - Remark: Generated from `#/components/schemas/QueryResponse`. internal struct QueryResponse: Codable, Hashable, Sendable { /// - Remark: Generated from `#/components/schemas/QueryResponse/records`. - internal var records: [Components.Schemas.Record]? + internal var records: [Components.Schemas.RecordResponse]? /// - Remark: Generated from `#/components/schemas/QueryResponse/continuationMarker`. internal var continuationMarker: Swift.String? /// Creates a new `QueryResponse`. @@ -1085,7 +1431,7 @@ internal enum Components { /// - records: /// - continuationMarker: internal init( - records: [Components.Schemas.Record]? = nil, + records: [Components.Schemas.RecordResponse]? = nil, continuationMarker: Swift.String? = nil ) { self.records = records @@ -1099,12 +1445,12 @@ internal enum Components { /// - Remark: Generated from `#/components/schemas/ModifyResponse`. internal struct ModifyResponse: Codable, Hashable, Sendable { /// - Remark: Generated from `#/components/schemas/ModifyResponse/records`. - internal var records: [Components.Schemas.Record]? + internal var records: [Components.Schemas.RecordResponse]? /// Creates a new `ModifyResponse`. /// /// - Parameters: /// - records: - internal init(records: [Components.Schemas.Record]? = nil) { + internal init(records: [Components.Schemas.RecordResponse]? = nil) { self.records = records } internal enum CodingKeys: String, CodingKey { @@ -1114,12 +1460,12 @@ internal enum Components { /// - Remark: Generated from `#/components/schemas/LookupResponse`. internal struct LookupResponse: Codable, Hashable, Sendable { /// - Remark: Generated from `#/components/schemas/LookupResponse/records`. - internal var records: [Components.Schemas.Record]? + internal var records: [Components.Schemas.RecordResponse]? /// Creates a new `LookupResponse`. /// /// - Parameters: /// - records: - internal init(records: [Components.Schemas.Record]? = nil) { + internal init(records: [Components.Schemas.RecordResponse]? = nil) { self.records = records } internal enum CodingKeys: String, CodingKey { @@ -1129,7 +1475,7 @@ internal enum Components { /// - Remark: Generated from `#/components/schemas/ChangesResponse`. internal struct ChangesResponse: Codable, Hashable, Sendable { /// - Remark: Generated from `#/components/schemas/ChangesResponse/records`. - internal var records: [Components.Schemas.Record]? + internal var records: [Components.Schemas.RecordResponse]? /// - Remark: Generated from `#/components/schemas/ChangesResponse/syncToken`. internal var syncToken: Swift.String? /// - Remark: Generated from `#/components/schemas/ChangesResponse/moreComing`. @@ -1141,7 +1487,7 @@ internal enum Components { /// - syncToken: /// - moreComing: internal init( - records: [Components.Schemas.Record]? = nil, + records: [Components.Schemas.RecordResponse]? = nil, syncToken: Swift.String? = nil, moreComing: Swift.Bool? = nil ) { @@ -6504,9 +6850,14 @@ internal enum Operations { } } } - /// Upload Assets + /// Request Asset Upload URLs + /// + /// Request upload URLs for asset fields. This is the first step in a two-step process: + /// 1. Request upload URLs by specifying the record type and field name + /// 2. Upload the actual binary data to the returned URL (separate HTTP request) + /// + /// Upload URLs are valid for 15 minutes. Maximum file size is 15 MB. /// - /// Upload binary assets to CloudKit /// /// - Remark: HTTP `POST /database/{version}/{container}/{environment}/{database}/assets/upload`. /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/upload/post(uploadAssets)`. @@ -6572,24 +6923,72 @@ internal enum Operations { internal var headers: Operations.uploadAssets.Input.Headers /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody`. internal enum Body: Sendable, Hashable { - /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/multipartForm`. - internal enum multipartFormPayload: Sendable, Hashable { - /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/multipartForm/file`. - internal struct filePayload: Sendable, Hashable { - internal var body: OpenAPIRuntime.HTTPBody - /// Creates a new `filePayload`. + /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/json`. + internal struct jsonPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/json/zoneID`. + internal var zoneID: Components.Schemas.ZoneID? + /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/json/tokensPayload`. + internal struct tokensPayloadPayload: Codable, Hashable, Sendable { + /// Unique name to identify the record. Defaults to random UUID if not specified. + /// + /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/json/tokensPayload/recordName`. + internal var recordName: Swift.String? + /// Name of the record type + /// + /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/json/tokensPayload/recordType`. + internal var recordType: Swift.String + /// Name of the Asset or Asset list field + /// + /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/json/tokensPayload/fieldName`. + internal var fieldName: Swift.String + /// Creates a new `tokensPayloadPayload`. /// /// - Parameters: - /// - body: - internal init(body: OpenAPIRuntime.HTTPBody) { - self.body = body + /// - recordName: Unique name to identify the record. Defaults to random UUID if not specified. + /// - recordType: Name of the record type + /// - fieldName: Name of the Asset or Asset list field + internal init( + recordName: Swift.String? = nil, + recordType: Swift.String, + fieldName: Swift.String + ) { + self.recordName = recordName + self.recordType = recordType + self.fieldName = fieldName } + internal enum CodingKeys: String, CodingKey { + case recordName + case recordType + case fieldName + } + } + /// Array of asset fields to request upload URLs for + /// + /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/json/tokens`. + internal typealias tokensPayload = [Operations.uploadAssets.Input.Body.jsonPayload.tokensPayloadPayload] + /// Array of asset fields to request upload URLs for + /// + /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/json/tokens`. + internal var tokens: Operations.uploadAssets.Input.Body.jsonPayload.tokensPayload + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - zoneID: + /// - tokens: Array of asset fields to request upload URLs for + internal init( + zoneID: Components.Schemas.ZoneID? = nil, + tokens: Operations.uploadAssets.Input.Body.jsonPayload.tokensPayload + ) { + self.zoneID = zoneID + self.tokens = tokens + } + internal enum CodingKeys: String, CodingKey { + case zoneID + case tokens } - case file(OpenAPIRuntime.MultipartPart) - case undocumented(OpenAPIRuntime.MultipartRawPart) } - /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/content/multipart\/form-data`. - case multipartForm(OpenAPIRuntime.MultipartBody) + /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/upload/POST/requestBody/content/application\/json`. + case json(Operations.uploadAssets.Input.Body.jsonPayload) } internal var body: Operations.uploadAssets.Input.Body /// Creates a new `Input`. @@ -6637,7 +7036,7 @@ internal enum Operations { self.body = body } } - /// Asset uploaded successfully + /// Upload URLs returned successfully /// /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/upload/post(uploadAssets)/responses/200`. /// diff --git a/Sources/MistKit/Helpers/FilterBuilder.swift b/Sources/MistKit/Helpers/FilterBuilder.swift index fe919bfe..c3330a7d 100644 --- a/Sources/MistKit/Helpers/FilterBuilder.swift +++ b/Sources/MistKit/Helpers/FilterBuilder.swift @@ -141,7 +141,7 @@ internal struct FilterBuilder { .init( comparator: .BEGINS_WITH, fieldName: field, - fieldValue: .init(value: .stringValue(value), type: .string) + fieldValue: .init(value: .StringValue(value)) ) } @@ -155,7 +155,7 @@ internal struct FilterBuilder { .init( comparator: .NOT_BEGINS_WITH, fieldName: field, - fieldValue: .init(value: .stringValue(value), type: .string) + fieldValue: .init(value: .StringValue(value)) ) } @@ -171,7 +171,7 @@ internal struct FilterBuilder { .init( comparator: .CONTAINS_ALL_TOKENS, fieldName: field, - fieldValue: .init(value: .stringValue(tokens), type: .string) + fieldValue: .init(value: .StringValue(tokens)) ) } @@ -185,8 +185,7 @@ internal struct FilterBuilder { comparator: .IN, fieldName: field, fieldValue: .init( - value: .listValue(values.map { Components.Schemas.FieldValue(from: $0).value }), - type: .list + value: .ListValue(values.map { Components.Schemas.ListValuePayload(from: $0) }) ) ) } @@ -201,8 +200,7 @@ internal struct FilterBuilder { comparator: .NOT_IN, fieldName: field, fieldValue: .init( - value: .listValue(values.map { Components.Schemas.FieldValue(from: $0).value }), - type: .list + value: .ListValue(values.map { Components.Schemas.ListValuePayload(from: $0) }) ) ) } @@ -253,7 +251,7 @@ internal struct FilterBuilder { .init( comparator: .LIST_MEMBER_BEGINS_WITH, fieldName: field, - fieldValue: .init(value: .stringValue(prefix), type: .string) + fieldValue: .init(value: .StringValue(prefix)) ) } @@ -269,7 +267,7 @@ internal struct FilterBuilder { .init( comparator: .NOT_LIST_MEMBER_BEGINS_WITH, fieldName: field, - fieldValue: .init(value: .stringValue(prefix), type: .string) + fieldValue: .init(value: .StringValue(prefix)) ) } } diff --git a/Sources/MistKit/LoggingMiddleware.swift b/Sources/MistKit/LoggingMiddleware.swift index dc4731e6..b6fed4c4 100644 --- a/Sources/MistKit/LoggingMiddleware.swift +++ b/Sources/MistKit/LoggingMiddleware.swift @@ -114,7 +114,11 @@ internal struct LoggingMiddleware: ClientMiddleware { "⚠️ 421 Misdirected Request - The server cannot produce a response for this request") } - return await logResponseBody(body) + #if !os(WASI) + return await logResponseBody(body) + #else + return body + #endif } /// Log response body content diff --git a/Sources/MistKit/MistKitClient.swift b/Sources/MistKit/MistKitClient.swift index 3a413c99..c607807a 100644 --- a/Sources/MistKit/MistKitClient.swift +++ b/Sources/MistKit/MistKitClient.swift @@ -29,6 +29,9 @@ import Crypto import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif import HTTPTypes import OpenAPIRuntime @@ -49,7 +52,10 @@ internal struct MistKitClient { /// - transport: Custom transport for network requests /// - Throws: ClientError if initialization fails @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) - internal init(configuration: MistKitConfiguration, transport: any ClientTransport) throws { + internal init( + configuration: MistKitConfiguration, + transport: any ClientTransport + ) throws { // Create appropriate TokenManager from configuration let tokenManager = try configuration.createTokenManager() @@ -130,19 +136,29 @@ internal struct MistKitClient { privateKeyData: privateKeyData ) - try self.init(configuration: configuration, tokenManager: tokenManager, transport: transport) + try self.init( + configuration: configuration, + tokenManager: tokenManager, + transport: transport + ) } // MARK: - Convenience Initializers #if !os(WASI) /// Initialize a new MistKit client with default URLSessionTransport - /// - Parameter configuration: The CloudKit configuration including container, - /// environment, and authentication + /// - Parameters: + /// - configuration: The CloudKit configuration including container, + /// environment, and authentication /// - Throws: ClientError if initialization fails @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) - internal init(configuration: MistKitConfiguration) throws { - try self.init(configuration: configuration, transport: URLSessionTransport()) + internal init( + configuration: MistKitConfiguration + ) throws { + try self.init( + configuration: configuration, + transport: URLSessionTransport() + ) } /// Initialize a new MistKit client with a custom TokenManager and individual parameters diff --git a/Sources/MistKit/Service/AssetUploadReceipt.swift b/Sources/MistKit/Service/AssetUploadReceipt.swift new file mode 100644 index 00000000..cc9969ac --- /dev/null +++ b/Sources/MistKit/Service/AssetUploadReceipt.swift @@ -0,0 +1,54 @@ +// +// AssetUploadReceipt.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Receipt from uploading an asset to CloudKit +/// +/// After uploading binary data to CloudKit, you receive an asset dictionary containing +/// the receipt, checksums, and other metadata needed to associate the asset with a record. +/// This type contains that complete asset information along with the target record and field. +public struct AssetUploadReceipt: Sendable { + /// The complete asset data including receipt and checksums + /// Use this when creating or updating records + public let asset: FieldValue.Asset + + /// The record name this asset is associated with + public let recordName: String + + /// The field name this asset should be assigned to + public let fieldName: String + + /// Initialize an asset upload receipt + public init(asset: FieldValue.Asset, recordName: String, fieldName: String) { + self.asset = asset + self.recordName = recordName + self.fieldName = fieldName + } +} diff --git a/Sources/MistKit/Service/AssetUploadResponse.swift b/Sources/MistKit/Service/AssetUploadResponse.swift new file mode 100644 index 00000000..55231245 --- /dev/null +++ b/Sources/MistKit/Service/AssetUploadResponse.swift @@ -0,0 +1,78 @@ +// +// AssetUploadResponse.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Response structure for CloudKit CDN asset upload +/// +/// After uploading binary data to the CloudKit CDN, the server returns this structure +/// containing the asset metadata needed to associate the upload with a record field. +/// +/// This type is useful when implementing custom upload workflows or when you need +/// to perform the upload steps individually rather than using the combined `uploadAssets()` method. +/// +/// Response format: `{ "singleFile": { "wrappingKey": ..., "fileChecksum": ..., "receipt": ..., etc. } }` +public struct AssetUploadResponse: Codable, Sendable { + /// The uploaded asset data containing checksums and receipt + public let singleFile: AssetData + + /// Asset metadata returned from CloudKit CDN + public struct AssetData: Codable, Sendable { + /// Wrapping key for encrypted assets + public let wrappingKey: String? + /// SHA256 checksum of the uploaded file + public let fileChecksum: String? + /// Receipt token proving successful upload + public let receipt: String? + /// Reference checksum for asset verification + public let referenceChecksum: String? + /// Size of the uploaded asset in bytes + public let size: Int64? + + /// Initialize asset data + public init( + wrappingKey: String?, + fileChecksum: String?, + receipt: String?, + referenceChecksum: String?, + size: Int64? + ) { + self.wrappingKey = wrappingKey + self.fileChecksum = fileChecksum + self.receipt = receipt + self.referenceChecksum = referenceChecksum + self.size = size + } + } + + /// Initialize asset upload response + public init(singleFile: AssetData) { + self.singleFile = singleFile + } +} diff --git a/Sources/MistKit/Service/AssetUploadToken.swift b/Sources/MistKit/Service/AssetUploadToken.swift new file mode 100644 index 00000000..c23929aa --- /dev/null +++ b/Sources/MistKit/Service/AssetUploadToken.swift @@ -0,0 +1,56 @@ +// +// AssetUploadToken.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Token returned after uploading an asset +/// +/// After uploading binary data, CloudKit returns tokens that must be +/// associated with record fields using a subsequent modifyRecords operation. +public struct AssetUploadToken: Sendable, Equatable { + /// The upload URL (may be used for download reference) + public let url: String? + /// The record name this token is associated with + public let recordName: String? + /// The field name this token should be assigned to + public let fieldName: String? + + /// Initialize an asset upload token + public init(url: String?, recordName: String?, fieldName: String?) { + self.url = url + self.recordName = recordName + self.fieldName = fieldName + } + + internal init(from token: Components.Schemas.AssetUploadResponse.tokensPayloadPayload) { + self.url = token.url + self.recordName = token.recordName + self.fieldName = token.fieldName + } +} diff --git a/Sources/MistKit/Service/CloudKitError+OpenAPI.swift b/Sources/MistKit/Service/CloudKitError+OpenAPI.swift index ba141493..d550176b 100644 --- a/Sources/MistKit/Service/CloudKitError+OpenAPI.swift +++ b/Sources/MistKit/Service/CloudKitError+OpenAPI.swift @@ -205,13 +205,22 @@ extension CloudKitError { // Handle undocumented error if let statusCode = response.undocumentedStatusCode { - assertionFailure("Unhandled response status code: \(statusCode)") + // Log warning but don't crash - undocumented status codes can occur + MistKitLogger.logWarning( + "Unhandled response status code: \(statusCode) - treating as generic HTTP error", + logger: MistKitLogger.api, + shouldRedact: false + ) self = .httpError(statusCode: statusCode) return } - // Should never reach here - assertionFailure("Unhandled response case: \(response)") + // Should never reach here - log and return generic error + MistKitLogger.logWarning( + "Unhandled response case: \(response) - treating as invalid response", + logger: MistKitLogger.api, + shouldRedact: false + ) self = .invalidResponse } } diff --git a/Sources/MistKit/Service/CloudKitError.swift b/Sources/MistKit/Service/CloudKitError.swift index 7d0c4ffa..6f61a226 100644 --- a/Sources/MistKit/Service/CloudKitError.swift +++ b/Sources/MistKit/Service/CloudKitError.swift @@ -28,6 +28,9 @@ // public import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif import OpenAPIRuntime /// Represents errors that can occur when interacting with CloudKit Web Services diff --git a/Sources/MistKit/Service/CloudKitResponseProcessor.swift b/Sources/MistKit/Service/CloudKitResponseProcessor.swift index a6d1e56c..509a79c1 100644 --- a/Sources/MistKit/Service/CloudKitResponseProcessor.swift +++ b/Sources/MistKit/Service/CloudKitResponseProcessor.swift @@ -173,4 +173,82 @@ internal struct CloudKitResponseProcessor { throw CloudKitError.invalidResponse } } + + /// Process lookupZones response + /// - Parameter response: The response to process + /// - Returns: The extracted zones lookup data + /// - Throws: CloudKitError for various error conditions + internal func processLookupZonesResponse(_ response: Operations.lookupZones.Output) + async throws(CloudKitError) -> Components.Schemas.ZonesLookupResponse + { + // Check for errors first + if let error = CloudKitError(response) { + throw error + } + + // Must be .ok case - extract data + switch response { + case .ok(let okResponse): + switch okResponse.body { + case .json(let zonesData): + return zonesData + } + default: + // Should never reach here since all errors are handled above + assertionFailure("Unexpected response case after error handling") + throw CloudKitError.invalidResponse + } + } + + /// Process fetchRecordChanges response + /// - Parameter response: The response to process + /// - Returns: The extracted changes response data + /// - Throws: CloudKitError for various error conditions + internal func processFetchRecordChangesResponse(_ response: Operations.fetchRecordChanges.Output) + async throws(CloudKitError) -> Components.Schemas.ChangesResponse + { + // Check for errors first + if let error = CloudKitError(response) { + throw error + } + + // Must be .ok case - extract data + switch response { + case .ok(let okResponse): + switch okResponse.body { + case .json(let changesData): + return changesData + } + default: + // Should never reach here since all errors are handled above + assertionFailure("Unexpected response case after error handling") + throw CloudKitError.invalidResponse + } + } + + /// Process uploadAssets response + /// - Parameter response: The response to process + /// - Returns: The extracted asset upload response data + /// - Throws: CloudKitError for various error conditions + internal func processUploadAssetsResponse(_ response: Operations.uploadAssets.Output) + async throws(CloudKitError) -> Components.Schemas.AssetUploadResponse + { + // Check for errors first + if let error = CloudKitError(response) { + throw error + } + + // Must be .ok case - extract data + switch response { + case .ok(let okResponse): + switch okResponse.body { + case .json(let uploadData): + return uploadData + } + default: + // Should never reach here since all errors are handled above + assertionFailure("Unexpected response case after error handling") + throw CloudKitError.invalidResponse + } + } } diff --git a/Sources/MistKit/Service/CloudKitService+Initialization.swift b/Sources/MistKit/Service/CloudKitService+Initialization.swift index 357e7c71..69ce77e8 100644 --- a/Sources/MistKit/Service/CloudKitService+Initialization.swift +++ b/Sources/MistKit/Service/CloudKitService+Initialization.swift @@ -28,6 +28,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif public import OpenAPIRuntime // MARK: - Generic Initializers (All Platforms) @@ -54,7 +57,10 @@ extension CloudKitService { apiToken: apiToken, webAuthToken: webAuthToken ) - self.mistKitClient = try MistKitClient(configuration: config, transport: transport) + self.mistKitClient = try MistKitClient( + configuration: config, + transport: transport + ) } /// Initialize CloudKit service with API-only authentication @@ -78,7 +84,10 @@ extension CloudKitService { keyID: nil, privateKeyData: nil ) - self.mistKitClient = try MistKitClient(configuration: config, transport: transport) + self.mistKitClient = try MistKitClient( + configuration: config, + transport: transport + ) } /// Initialize CloudKit service with a custom TokenManager diff --git a/Sources/MistKit/Service/CloudKitService+Operations.swift b/Sources/MistKit/Service/CloudKitService+Operations.swift index e9d48034..7d5fac42 100644 --- a/Sources/MistKit/Service/CloudKitService+Operations.swift +++ b/Sources/MistKit/Service/CloudKitService+Operations.swift @@ -28,6 +28,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif import OpenAPIRuntime #if !os(WASI) @@ -121,6 +124,85 @@ extension CloudKitService { } } + /// Lookup specific zones by their IDs + /// + /// Fetches detailed information about multiple zones in a single request. + /// Unlike listZones which returns all zones, this operation retrieves + /// specific zones identified by their zone IDs. + /// + /// - Parameter zoneIDs: Array of zone identifiers to lookup + /// - Returns: Array of ZoneInfo objects for the requested zones + /// - Throws: CloudKitError if the lookup fails + /// + /// Example: + /// ```swift + /// let zones = try await service.lookupZones( + /// zoneIDs: [ + /// ZoneID(zoneName: "Articles", ownerName: nil), + /// ZoneID(zoneName: "Images", ownerName: nil) + /// ] + /// ) + /// ``` + public func lookupZones(zoneIDs: [ZoneID]) async throws(CloudKitError) -> [ZoneInfo] { + // Validation + guard !zoneIDs.isEmpty else { + throw CloudKitError.httpErrorWithRawResponse( + statusCode: 400, + rawResponse: "zoneIDs cannot be empty" + ) + } + + do { + let response = try await client.lookupZones( + .init( + path: createLookupZonesPath(containerIdentifier: containerIdentifier), + body: .json( + .init( + zones: zoneIDs.map { Components.Schemas.ZoneID(from: $0) } + ) + ) + ) + ) + + let zonesData: Components.Schemas.ZonesLookupResponse = + try await responseProcessor.processLookupZonesResponse(response) + + return zonesData.zones?.compactMap { zone in + guard let zoneID = zone.zoneID else { + return nil + } + return ZoneInfo( + zoneName: zoneID.zoneName ?? "Unknown", + ownerRecordName: zoneID.ownerName, + capabilities: [] + ) + } ?? [] + } catch let cloudKitError as CloudKitError { + throw cloudKitError + } catch let decodingError as DecodingError { + MistKitLogger.logError( + "JSON decoding failed in lookupZones: \(decodingError)", + logger: MistKitLogger.api, + shouldRedact: false + ) + throw CloudKitError.decodingError(decodingError) + } catch let urlError as URLError { + MistKitLogger.logError( + "Network error in lookupZones: \(urlError)", + logger: MistKitLogger.network, + shouldRedact: false + ) + throw CloudKitError.networkError(urlError) + } catch { + MistKitLogger.logError( + "Unexpected error in lookupZones: \(error)", + logger: MistKitLogger.api, + shouldRedact: false + ) + throw CloudKitError.underlyingError(error) + } + } + /// Query records from the default zone /// /// Queries CloudKit records with optional filtering and sorting. Supports all CloudKit @@ -326,6 +408,152 @@ extension CloudKitService { } } + /// Fetch record changes since a sync token + /// + /// Retrieves all records that have changed (created, updated, or deleted) + /// since the provided sync token. Use this for efficient incremental sync + /// operations rather than repeatedly querying all records. + /// + /// - Parameters: + /// - zoneID: Optional zone to fetch changes from (defaults to _defaultZone) + /// - syncToken: Optional token from previous fetch (nil = initial fetch) + /// - resultsLimit: Optional maximum number of records (1-200) + /// - Returns: RecordChangesResult containing changed records and new sync token + /// - Throws: CloudKitError if the fetch fails + /// + /// Example - Initial Sync: + /// ```swift + /// let result = try await service.fetchRecordChanges() + /// // Store result.syncToken for next fetch + /// processRecords(result.records) + /// ``` + /// + /// Example - Incremental Sync: + /// ```swift + /// let result = try await service.fetchRecordChanges( + /// syncToken: previousToken + /// ) + /// if result.moreComing { + /// // More changes available, fetch again with new token + /// let next = try await service.fetchRecordChanges( + /// syncToken: result.syncToken + /// ) + /// } + /// ``` + /// + /// - Note: If moreComing is true, call again with the returned syncToken + /// to fetch remaining changes + public func fetchRecordChanges( + zoneID: ZoneID? = nil, + syncToken: String? = nil, + resultsLimit: Int? = nil + ) async throws(CloudKitError) -> RecordChangesResult { + // Validate limit if provided + if let limit = resultsLimit { + guard limit > 0 && limit <= 200 else { + throw CloudKitError.httpErrorWithRawResponse( + statusCode: 400, + rawResponse: "resultsLimit must be between 1 and 200, got \(limit)" + ) + } + } + + // Use provided zoneID or default zone + let effectiveZoneID = zoneID ?? .defaultZone + + do { + let response = try await client.fetchRecordChanges( + .init( + path: createFetchRecordChangesPath(containerIdentifier: containerIdentifier), + body: .json( + .init( + zoneID: Components.Schemas.ZoneID(from: effectiveZoneID), + syncToken: syncToken, + resultsLimit: resultsLimit + ) + ) + ) + ) + + let changesData: Components.Schemas.ChangesResponse = + try await responseProcessor.processFetchRecordChangesResponse(response) + + return RecordChangesResult(from: changesData) + } catch let cloudKitError as CloudKitError { + throw cloudKitError + } catch let decodingError as DecodingError { + MistKitLogger.logError( + "JSON decoding failed in fetchRecordChanges: \(decodingError)", + logger: MistKitLogger.api, + shouldRedact: false + ) + throw CloudKitError.decodingError(decodingError) + } catch let urlError as URLError { + MistKitLogger.logError( + "Network error in fetchRecordChanges: \(urlError)", + logger: MistKitLogger.network, + shouldRedact: false + ) + throw CloudKitError.networkError(urlError) + } catch { + MistKitLogger.logError( + "Unexpected error in fetchRecordChanges: \(error)", + logger: MistKitLogger.api, + shouldRedact: false + ) + throw CloudKitError.underlyingError(error) + } + } + + /// Fetch all record changes, handling pagination automatically + /// + /// Convenience method that automatically fetches all available changes + /// by following the moreComing flag and making multiple requests if needed. + /// + /// - Parameters: + /// - zoneID: Optional zone to fetch changes from (defaults to _defaultZone) + /// - syncToken: Optional token from previous fetch (nil = initial fetch) + /// - resultsLimit: Optional maximum records per request (1-200) + /// - Returns: Array of all changed records and final sync token + /// - Throws: CloudKitError if any fetch fails + /// + /// Example: + /// ```swift + /// let (records, newToken) = try await service.fetchAllRecordChanges( + /// syncToken: lastSyncToken + /// ) + /// // Process all records + /// processRecords(records) + /// // Store newToken for next sync + /// ``` + /// + /// - Warning: For zones with many changes, this may make multiple requests + /// and return a large array. Consider using fetchRecordChanges() + /// with manual pagination for better memory control. + public func fetchAllRecordChanges( + zoneID: ZoneID? = nil, + syncToken: String? = nil, + resultsLimit: Int? = nil + ) async throws(CloudKitError) -> (records: [RecordInfo], syncToken: String?) { + var allRecords: [RecordInfo] = [] + var currentToken = syncToken + var moreComing = true + + while moreComing { + let result = try await fetchRecordChanges( + zoneID: zoneID, + syncToken: currentToken, + resultsLimit: resultsLimit + ) + + allRecords.append(contentsOf: result.records) + currentToken = result.syncToken + moreComing = result.moreComing + } + + return (allRecords, currentToken) + } + /// Modify (create, update, delete) records @available( *, deprecated, @@ -378,7 +606,7 @@ extension CloudKitService { } /// Lookup records by record names - internal func lookupRecords( + public func lookupRecords( recordNames: [String], desiredKeys: [String]? = nil ) async throws(CloudKitError) -> [RecordInfo] { diff --git a/Sources/MistKit/Service/CloudKitService+WriteOperations.swift b/Sources/MistKit/Service/CloudKitService+WriteOperations.swift index 7f5189f3..3cff6f34 100644 --- a/Sources/MistKit/Service/CloudKitService+WriteOperations.swift +++ b/Sources/MistKit/Service/CloudKitService+WriteOperations.swift @@ -27,7 +27,11 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +public import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import HTTPTypes import OpenAPIRuntime #if !os(WASI) @@ -151,4 +155,263 @@ extension CloudKitService { _ = try await modifyRecords([operation]) } + + /// Upload binary asset data to CloudKit + /// + /// This is a convenience method that performs a complete two-step asset upload: + /// 1. Requests an upload URL from CloudKit + /// 2. Uploads the binary data to that URL + /// + /// - Parameters: + /// - data: The binary data to upload + /// - recordType: The type of record that will use this asset (e.g., "Photo") + /// - fieldName: The name of the asset field (e.g., "image") + /// - recordName: Optional unique record name (defaults to CloudKit-generated UUID) + /// - Returns: AssetUploadToken containing the upload URL for record association + /// - Throws: CloudKitError if the upload fails + /// + /// Example: + /// ```swift + /// // Upload the asset + /// let imageData = try Data(contentsOf: imageURL) + /// let token = try await service.uploadAssets( + /// data: imageData, + /// recordType: "Photo", + /// fieldName: "image" + /// ) + /// + /// // Create a record with the asset + /// let asset = FieldValue.Asset( + /// fileChecksum: nil, + /// size: Int64(imageData.count), + /// referenceChecksum: nil, + /// wrappingKey: nil, + /// receipt: nil, + /// downloadURL: token.url + /// ) + /// + /// let record = try await service.createRecord( + /// recordType: "Photo", + /// fields: [ + /// "image": .asset(asset), + /// "title": .string("My Photo") + /// ] + /// ) + /// ``` + /// + /// - Note: Upload URLs are valid for 15 minutes + /// - Warning: Maximum upload size is 15 MB per asset + public func uploadAssets( + data: Data, + recordType: String, + fieldName: String, + recordName: String? = nil, + using uploader: AssetUploader? = nil + ) async throws(CloudKitError) -> AssetUploadReceipt { + // Validate data size (CloudKit limit is 15 MB) + let maxSize: Int = 15 * 1024 * 1024 // 15 MB + guard data.count <= maxSize else { + throw CloudKitError.httpErrorWithRawResponse( + statusCode: 413, + rawResponse: "Asset size \(data.count) exceeds maximum of \(maxSize) bytes" + ) + } + + guard !data.isEmpty else { + throw CloudKitError.httpErrorWithRawResponse( + statusCode: 400, + rawResponse: "Asset data cannot be empty" + ) + } + + do { + // Step 1: Request upload URL + let urlToken = try await requestAssetUploadURL( + recordType: recordType, + fieldName: fieldName, + recordName: recordName + ) + + guard let uploadURLString = urlToken.url, + let uploadURL = URL(string: uploadURLString) + else { + throw CloudKitError.invalidResponse + } + + // Step 2: Upload binary data to the URL and get asset dictionary + let asset = try await uploadAssetData(data, to: uploadURL, using: uploader) + + // Return complete result with asset data + return AssetUploadReceipt( + asset: asset, + recordName: urlToken.recordName ?? "unknown", + fieldName: urlToken.fieldName ?? fieldName + ) + } catch let cloudKitError as CloudKitError { + throw cloudKitError + } catch let decodingError as DecodingError { + MistKitLogger.logError( + "JSON decoding failed in uploadAssets: \(decodingError)", + logger: MistKitLogger.api, + shouldRedact: false + ) + throw CloudKitError.decodingError(decodingError) + } catch let urlError as URLError { + MistKitLogger.logError( + "Network error in uploadAssets: \(urlError)", + logger: MistKitLogger.network, + shouldRedact: false + ) + throw CloudKitError.networkError(urlError) + } catch { + MistKitLogger.logError( + "Unexpected error in uploadAssets: \(error)", + logger: MistKitLogger.api, + shouldRedact: false + ) + throw CloudKitError.underlyingError(error) + } + } + + /// Request an upload URL for an asset field + /// + /// This is step 1 of the two-step asset upload process. Use `uploadAssetData(_:to:)` + /// to complete step 2, or use the convenience method `uploadAssets(data:recordType:fieldName:)` + /// to perform both steps. + /// + /// - Parameters: + /// - recordType: The type of record that will use this asset + /// - fieldName: The name of the asset field + /// - recordName: Optional unique record name (defaults to CloudKit-generated UUID) + /// - zoneID: Optional zone ID (defaults to default zone) + /// - Returns: AssetUploadToken containing the upload URL + /// - Throws: CloudKitError if the request fails + public func requestAssetUploadURL( + recordType: String, + fieldName: String, + recordName: String? = nil, + zoneID: ZoneID? = nil + ) async throws(CloudKitError) -> AssetUploadToken { + do { + // Create token request + let tokenRequest = Operations.uploadAssets.Input.Body.jsonPayload.tokensPayloadPayload( + recordName: recordName, + recordType: recordType, + fieldName: fieldName + ) + + let requestBody = Operations.uploadAssets.Input.Body.jsonPayload( + zoneID: zoneID.map { Components.Schemas.ZoneID(from: $0) }, + tokens: [tokenRequest] + ) + + let response = try await client.uploadAssets( + path: createUploadAssetsPath(containerIdentifier: containerIdentifier), + body: .json(requestBody) + ) + + let uploadData: Components.Schemas.AssetUploadResponse = + try await responseProcessor.processUploadAssetsResponse(response) + + guard let token = uploadData.tokens?.first else { + throw CloudKitError.invalidResponse + } + + return AssetUploadToken(from: token) + } catch let cloudKitError as CloudKitError { + throw cloudKitError + } catch { + throw CloudKitError.underlyingError(error) + } + } + + /// Upload binary data to a CloudKit asset upload URL + /// + /// This is step 2 of the two-step asset upload process. Use `requestAssetUploadURL` + /// to get the upload URL first, or use the convenience method + /// `uploadAssets(data:recordType:fieldName:)` to perform both steps. + /// + /// - Parameters: + /// - data: The binary data to upload + /// - url: The upload URL from CloudKit + /// - uploader: Optional custom upload handler. If nil, uses URLSession.shared + /// - Returns: The asset dictionary returned by CloudKit containing receipt, checksums, etc. + /// - Throws: CloudKitError if the upload fails + /// - Note: Upload URLs are valid for 15 minutes + /// - Important: The returned asset dictionary must be used when creating/updating records with this asset + public func uploadAssetData( + _ data: Data, + to url: URL, + using uploader: AssetUploader? = nil + ) async throws(CloudKitError) -> FieldValue.Asset { + do { + // Use provided uploader or default to URLSession.shared + let uploadHandler = uploader ?? { data, url in + #if os(WASI) + throw CloudKitError.httpErrorWithRawResponse( + statusCode: 501, + rawResponse: "Asset uploads not supported on WASI" + ) + #else + return try await URLSession.shared.upload(data, to: url) + #endif + } + + // Perform the upload + let (statusCode, responseData) = try await uploadHandler(data, url) + + // Validate HTTP status code + guard let httpStatusCode = statusCode else { + throw CloudKitError.invalidResponse + } + guard (200...299).contains(httpStatusCode) else { + throw CloudKitError.httpError(statusCode: httpStatusCode) + } + + // Debug: log the raw response + if let responseString = String(data: responseData, encoding: .utf8) { + MistKitLogger.logDebug( + "Asset upload response: \(responseString)", + logger: MistKitLogger.api, + shouldRedact: false + ) + } + + // Decode the response + let uploadResponse = try JSONDecoder().decode(AssetUploadResponse.self, from: responseData) + + // Convert to FieldValue.Asset + return FieldValue.Asset( + fileChecksum: uploadResponse.singleFile.fileChecksum, + size: uploadResponse.singleFile.size, + referenceChecksum: uploadResponse.singleFile.referenceChecksum, + wrappingKey: uploadResponse.singleFile.wrappingKey, + receipt: uploadResponse.singleFile.receipt, + downloadURL: nil + ) + } catch let cloudKitError as CloudKitError { + throw cloudKitError + } catch let decodingError as DecodingError { + MistKitLogger.logError( + "Failed to decode asset upload response: \(decodingError)", + logger: MistKitLogger.api, + shouldRedact: false + ) + throw CloudKitError.decodingError(decodingError) + } catch let urlError as URLError { + MistKitLogger.logError( + "Network error uploading asset: \(urlError)", + logger: MistKitLogger.network, + shouldRedact: false + ) + throw CloudKitError.networkError(urlError) + } catch { + MistKitLogger.logError( + "Error uploading asset data: \(error)", + logger: MistKitLogger.network, + shouldRedact: false + ) + throw CloudKitError.underlyingError(error) + } + } } diff --git a/Sources/MistKit/Service/CloudKitService.swift b/Sources/MistKit/Service/CloudKitService.swift index 46e7f3b9..dac26a82 100644 --- a/Sources/MistKit/Service/CloudKitService.swift +++ b/Sources/MistKit/Service/CloudKitService.swift @@ -28,6 +28,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif import OpenAPIRuntime #if !os(WASI) @@ -129,4 +132,46 @@ extension CloudKitService { database: .init(from: database) ) } + + /// Create a standard path for lookupZones requests + /// - Parameter containerIdentifier: The container identifier + /// - Returns: A configured path for the request + internal func createLookupZonesPath( + containerIdentifier: String + ) -> Operations.lookupZones.Input.Path { + .init( + version: "1", + container: containerIdentifier, + environment: .init(from: environment), + database: .init(from: database) + ) + } + + /// Create a standard path for fetchRecordChanges requests + /// - Parameter containerIdentifier: The container identifier + /// - Returns: A configured path for the request + internal func createFetchRecordChangesPath( + containerIdentifier: String + ) -> Operations.fetchRecordChanges.Input.Path { + .init( + version: "1", + container: containerIdentifier, + environment: .init(from: environment), + database: .init(from: database) + ) + } + + /// Create a standard path for uploadAssets requests + /// - Parameter containerIdentifier: The container identifier + /// - Returns: A configured path for the request + internal func createUploadAssetsPath( + containerIdentifier: String + ) -> Operations.uploadAssets.Input.Path { + .init( + version: "1", + container: containerIdentifier, + environment: .init(from: environment), + database: .init(from: database) + ) + } } diff --git a/Sources/MistKit/Service/FieldValue+Components.swift b/Sources/MistKit/Service/FieldValue+Components.swift index d086f81e..41546837 100644 --- a/Sources/MistKit/Service/FieldValue+Components.swift +++ b/Sources/MistKit/Service/FieldValue+Components.swift @@ -29,43 +29,43 @@ internal import Foundation -/// Extension to convert OpenAPI Components.Schemas.FieldValue to MistKit FieldValue +/// Extension to convert OpenAPI Components.Schemas.FieldValueResponse to MistKit FieldValue extension FieldValue { - /// Initialize from OpenAPI Components.Schemas.FieldValue - internal init?(_ fieldData: Components.Schemas.FieldValue) { - self.init(value: fieldData.value, fieldType: fieldData.type) + /// Initialize from OpenAPI Components.Schemas.FieldValueResponse (from API responses) + internal init?(_ fieldData: Components.Schemas.FieldValueResponse) { + self.init(valuePayload: fieldData.value, typePayload: fieldData._type) } /// Initialize from field value and type private init?( - value: CustomFieldValue.CustomFieldValuePayload, - fieldType: CustomFieldValue.FieldTypePayload? + valuePayload: Components.Schemas.FieldValueResponse.valuePayload, + typePayload: Components.Schemas.FieldValueResponse._typePayload? ) { + let value = valuePayload + let fieldType = typePayload switch value { - case .stringValue(let stringValue): + case .StringValue(let stringValue): self = .string(stringValue) - case .int64Value(let intValue): - self = .int64(intValue) - case .doubleValue(let doubleValue): - if fieldType == .timestamp { + case .Int64Value(let intValue): + self = .int64(Int(intValue)) + case .DoubleValue(let doubleValue): + if fieldType == .TIMESTAMP { self = .date(Date(timeIntervalSince1970: doubleValue / 1_000)) } else { self = .double(doubleValue) } - case .booleanValue(let boolValue): - self = .int64(boolValue ? 1 : 0) - case .bytesValue(let bytesValue): + case .BytesValue(let bytesValue): self = .bytes(bytesValue) - case .dateValue(let dateValue): + case .DateValue(let dateValue): self = .date(Date(timeIntervalSince1970: dateValue / 1_000)) - case .locationValue(let locationValue): + case .LocationValue(let locationValue): guard let location = Self(locationValue: locationValue) else { return nil } self = location - case .referenceValue(let referenceValue): + case .ReferenceValue(let referenceValue): self.init(referenceValue: referenceValue) - case .assetValue(let assetValue): + case .AssetValue(let assetValue): self.init(assetValue: assetValue) - case .listValue(let listValue): + case .ListValue(let listValue): self.init(listValue: listValue) } } @@ -123,54 +123,52 @@ extension FieldValue { } /// Initialize from list field value - private init(listValue: [CustomFieldValue.CustomFieldValuePayload]) { + private init(listValue: [Components.Schemas.ListValuePayload]) { let convertedList = listValue.compactMap { Self(listItem: $0) } self = .list(convertedList) } /// Initialize from individual list item - private init?(listItem: CustomFieldValue.CustomFieldValuePayload) { + private init?(listItem: Components.Schemas.ListValuePayload) { switch listItem { - case .stringValue(let stringValue): + case .StringValue(let stringValue): self = .string(stringValue) - case .int64Value(let intValue): - self = .int64(intValue) - case .doubleValue(let doubleValue): + case .Int64Value(let intValue): + self = .int64(Int(intValue)) + case .DoubleValue(let doubleValue): self = .double(doubleValue) - case .booleanValue(let boolValue): - self = .int64(boolValue ? 1 : 0) - case .bytesValue(let bytesValue): + case .BytesValue(let bytesValue): self = .bytes(bytesValue) - case .dateValue(let dateValue): + case .DateValue(let dateValue): self = .date(Date(timeIntervalSince1970: dateValue / 1_000)) - case .locationValue(let locationValue): + case .LocationValue(let locationValue): guard let location = Self(locationValue: locationValue) else { return nil } self = location - case .referenceValue(let referenceValue): + case .ReferenceValue(let referenceValue): self.init(referenceValue: referenceValue) - case .assetValue(let assetValue): + case .AssetValue(let assetValue): self.init(assetValue: assetValue) - case .listValue(let nestedList): + case .ListValue(let nestedList): self.init(nestedListValue: nestedList) } } /// Initialize from nested list value (simplified for basic types) - private init(nestedListValue: [CustomFieldValue.CustomFieldValuePayload]) { + private init(nestedListValue: [Components.Schemas.ListValuePayload]) { let convertedNestedList = nestedListValue.compactMap { Self(basicListItem: $0) } self = .list(convertedNestedList) } /// Initialize from basic list item types only - private init?(basicListItem: CustomFieldValue.CustomFieldValuePayload) { + private init?(basicListItem: Components.Schemas.ListValuePayload) { switch basicListItem { - case .stringValue(let stringValue): + case .StringValue(let stringValue): self = .string(stringValue) - case .int64Value(let intValue): - self = .int64(intValue) - case .doubleValue(let doubleValue): + case .Int64Value(let intValue): + self = .int64(Int(intValue)) + case .DoubleValue(let doubleValue): self = .double(doubleValue) - case .bytesValue(let bytesValue): + case .BytesValue(let bytesValue): self = .bytes(bytesValue) default: return nil diff --git a/Sources/MistKit/Service/Operations.fetchRecordChanges.Output.swift b/Sources/MistKit/Service/Operations.fetchRecordChanges.Output.swift new file mode 100644 index 00000000..bc9a4a72 --- /dev/null +++ b/Sources/MistKit/Service/Operations.fetchRecordChanges.Output.swift @@ -0,0 +1,78 @@ +// +// Operations.fetchRecordChanges.Output.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +extension Operations.fetchRecordChanges.Output: CloudKitResponseType { + var badRequestResponse: Components.Responses.BadRequest? { + if case .badRequest(let response) = self { return response } else { return nil } + } + + var unauthorizedResponse: Components.Responses.Unauthorized? { + if case .unauthorized(let response) = self { return response } else { return nil } + } + + var forbiddenResponse: Components.Responses.Forbidden? { + if case .forbidden(let response) = self { return response } else { return nil } + } + + var notFoundResponse: Components.Responses.NotFound? { + if case .notFound(let response) = self { return response } else { return nil } + } + + var conflictResponse: Components.Responses.Conflict? { + if case .conflict(let response) = self { return response } else { return nil } + } + + var preconditionFailedResponse: Components.Responses.PreconditionFailed? { + if case .preconditionFailed(let response) = self { return response } else { return nil } + } + + var contentTooLargeResponse: Components.Responses.RequestEntityTooLarge? { + if case .contentTooLarge(let response) = self { return response } else { return nil } + } + + var misdirectedRequestResponse: Components.Responses.UnprocessableEntity? { + if case .misdirectedRequest(let response) = self { return response } else { return nil } + } + + var tooManyRequestsResponse: Components.Responses.TooManyRequests? { + if case .tooManyRequests(let response) = self { return response } else { return nil } + } + + var isOk: Bool { + if case .ok = self { return true } else { return false } + } + + var undocumentedStatusCode: Int? { + if case .undocumented(let statusCode, _) = self { return statusCode } else { return nil } + } + + // fetchRecordChanges has most error responses except 500/503 + var internalServerErrorResponse: Components.Responses.InternalServerError? { nil } + var serviceUnavailableResponse: Components.Responses.ServiceUnavailable? { nil } +} diff --git a/Sources/MistKit/Service/Operations.lookupZones.Output.swift b/Sources/MistKit/Service/Operations.lookupZones.Output.swift new file mode 100644 index 00000000..2ee22c14 --- /dev/null +++ b/Sources/MistKit/Service/Operations.lookupZones.Output.swift @@ -0,0 +1,57 @@ +// +// Operations.lookupZones.Output.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +extension Operations.lookupZones.Output: CloudKitResponseType { + var badRequestResponse: Components.Responses.BadRequest? { + if case .badRequest(let response) = self { return response } else { return nil } + } + + var unauthorizedResponse: Components.Responses.Unauthorized? { + if case .unauthorized(let response) = self { return response } else { return nil } + } + + var isOk: Bool { + if case .ok = self { return true } else { return false } + } + + var undocumentedStatusCode: Int? { + if case .undocumented(let statusCode, _) = self { return statusCode } else { return nil } + } + + // lookupZones only has 400/401 errors per OpenAPI spec + var forbiddenResponse: Components.Responses.Forbidden? { nil } + var notFoundResponse: Components.Responses.NotFound? { nil } + var conflictResponse: Components.Responses.Conflict? { nil } + var preconditionFailedResponse: Components.Responses.PreconditionFailed? { nil } + var contentTooLargeResponse: Components.Responses.RequestEntityTooLarge? { nil } + var misdirectedRequestResponse: Components.Responses.UnprocessableEntity? { nil } + var tooManyRequestsResponse: Components.Responses.TooManyRequests? { nil } + var internalServerErrorResponse: Components.Responses.InternalServerError? { nil } + var serviceUnavailableResponse: Components.Responses.ServiceUnavailable? { nil } +} diff --git a/Sources/MistKit/Service/Operations.uploadAssets.Output.swift b/Sources/MistKit/Service/Operations.uploadAssets.Output.swift new file mode 100644 index 00000000..2884477e --- /dev/null +++ b/Sources/MistKit/Service/Operations.uploadAssets.Output.swift @@ -0,0 +1,57 @@ +// +// Operations.uploadAssets.Output.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +extension Operations.uploadAssets.Output: CloudKitResponseType { + var badRequestResponse: Components.Responses.BadRequest? { + if case .badRequest(let response) = self { return response } else { return nil } + } + + var unauthorizedResponse: Components.Responses.Unauthorized? { + if case .unauthorized(let response) = self { return response } else { return nil } + } + + var isOk: Bool { + if case .ok = self { return true } else { return false } + } + + var undocumentedStatusCode: Int? { + if case .undocumented(let statusCode, _) = self { return statusCode } else { return nil } + } + + // uploadAssets only has 400/401 errors per OpenAPI spec + var forbiddenResponse: Components.Responses.Forbidden? { nil } + var notFoundResponse: Components.Responses.NotFound? { nil } + var conflictResponse: Components.Responses.Conflict? { nil } + var preconditionFailedResponse: Components.Responses.PreconditionFailed? { nil } + var contentTooLargeResponse: Components.Responses.RequestEntityTooLarge? { nil } + var misdirectedRequestResponse: Components.Responses.UnprocessableEntity? { nil } + var tooManyRequestsResponse: Components.Responses.TooManyRequests? { nil } + var internalServerErrorResponse: Components.Responses.InternalServerError? { nil } + var serviceUnavailableResponse: Components.Responses.ServiceUnavailable? { nil } +} diff --git a/Sources/MistKit/Service/RecordChangesResult.swift b/Sources/MistKit/Service/RecordChangesResult.swift new file mode 100644 index 00000000..2d483fd7 --- /dev/null +++ b/Sources/MistKit/Service/RecordChangesResult.swift @@ -0,0 +1,60 @@ +// +// RecordChangesResult.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Result from fetching record changes +/// +/// Contains records that have changed since the provided sync token, +/// along with a new sync token for subsequent fetches. +public struct RecordChangesResult: Sendable { + /// Records that have changed (created, updated, or deleted) + public let records: [RecordInfo] + /// Token to use for next fetch to get incremental changes + public let syncToken: String? + /// Whether more changes are available (for large change sets) + public let moreComing: Bool + + /// Initialize a record changes result + public init( + records: [RecordInfo], + syncToken: String?, + moreComing: Bool + ) { + self.records = records + self.syncToken = syncToken + self.moreComing = moreComing + } + + internal init(from response: Components.Schemas.ChangesResponse) { + self.records = response.records?.compactMap { RecordInfo(from: $0) } ?? [] + self.syncToken = response.syncToken + self.moreComing = response.moreComing ?? false + } +} diff --git a/Sources/MistKit/Service/RecordInfo.swift b/Sources/MistKit/Service/RecordInfo.swift index 342fc0a1..ffa34b6e 100644 --- a/Sources/MistKit/Service/RecordInfo.swift +++ b/Sources/MistKit/Service/RecordInfo.swift @@ -62,7 +62,7 @@ public struct RecordInfo: Encodable, Sendable { recordType == "Unknown" } - internal init(from record: Components.Schemas.Record) { + internal init(from record: Components.Schemas.RecordResponse) { self.recordName = record.recordName ?? "Unknown" self.recordType = record.recordType ?? "Unknown" self.recordChangeTag = record.recordChangeTag diff --git a/Sources/MistKit/Service/ZoneID.swift b/Sources/MistKit/Service/ZoneID.swift new file mode 100644 index 00000000..0d99aee5 --- /dev/null +++ b/Sources/MistKit/Service/ZoneID.swift @@ -0,0 +1,63 @@ +// +// ZoneID.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Identifies a specific CloudKit zone +/// +/// Zone IDs uniquely identify a record zone within a database. +/// The _defaultZone is automatically available in all databases. +public struct ZoneID: Sendable, Equatable, Hashable { + /// The zone name (e.g., "_defaultZone", "Articles") + public let zoneName: String + /// The owner's record name (optional, nil for current user) + public let ownerName: String? + + /// Initialize a zone identifier + /// - Parameters: + /// - zoneName: The zone name + /// - ownerName: Optional owner record name (nil = current user) + public init(zoneName: String, ownerName: String? = nil) { + self.zoneName = zoneName + self.ownerName = ownerName + } + + /// The default zone present in all databases + public static let defaultZone = ZoneID(zoneName: "_defaultZone", ownerName: nil) +} + +// MARK: - Internal Conversion +extension Components.Schemas.ZoneID { + internal init(from zoneID: ZoneID) { + self.init( + zoneName: zoneID.zoneName, + ownerName: zoneID.ownerName + ) + } +} diff --git a/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+BasicTypes.swift b/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+BasicTypes.swift index f926a9f4..02840029 100644 --- a/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+BasicTypes.swift +++ b/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+BasicTypes.swift @@ -13,10 +13,9 @@ extension FieldValueConversionTests { return } let fieldValue = FieldValue.string("test string") - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .string) - if case .stringValue(let value) = components.value { + if case .StringValue(let value) = components.value { #expect(value == "test string") } else { Issue.record("Expected stringValue") @@ -30,10 +29,9 @@ extension FieldValueConversionTests { return } let fieldValue = FieldValue.int64(42) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .int64) - if case .int64Value(let value) = components.value { + if case .Int64Value(let value) = components.value { #expect(value == 42) } else { Issue.record("Expected int64Value") @@ -47,10 +45,9 @@ extension FieldValueConversionTests { return } let fieldValue = FieldValue.double(3.14159) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .double) - if case .doubleValue(let value) = components.value { + if case .DoubleValue(let value) = components.value { #expect(value == 3.14159) } else { Issue.record("Expected doubleValue") @@ -64,19 +61,17 @@ extension FieldValueConversionTests { return } let trueValue = FieldValue(booleanValue: true) - let trueComponents = Components.Schemas.FieldValue(from: trueValue) - #expect(trueComponents.type == .int64) - if case .int64Value(let value) = trueComponents.value { + let trueComponents = Components.Schemas.FieldValueRequest(from: trueValue) + if case .Int64Value(let value) = trueComponents.value { #expect(value == 1) } else { Issue.record("Expected int64Value 1 for true") } let falseValue = FieldValue(booleanValue: false) - let falseComponents = Components.Schemas.FieldValue(from: falseValue) + let falseComponents = Components.Schemas.FieldValueRequest(from: falseValue) - #expect(falseComponents.type == .int64) - if case .int64Value(let value) = falseComponents.value { + if case .Int64Value(let value) = falseComponents.value { #expect(value == 0) } else { Issue.record("Expected int64Value 0 for false") @@ -90,10 +85,9 @@ extension FieldValueConversionTests { return } let fieldValue = FieldValue.bytes("base64encodedstring") - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .bytes) - if case .bytesValue(let value) = components.value { + if case .BytesValue(let value) = components.value { #expect(value == "base64encodedstring") } else { Issue.record("Expected bytesValue") @@ -108,10 +102,9 @@ extension FieldValueConversionTests { } let date = Date(timeIntervalSince1970: 1_000_000) let fieldValue = FieldValue.date(date) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .timestamp) - if case .dateValue(let value) = components.value { + if case .DateValue(let value) = components.value { #expect(value == date.timeIntervalSince1970 * 1_000) } else { Issue.record("Expected dateValue") diff --git a/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+ComplexTypes.swift b/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+ComplexTypes.swift index b53335f3..d58b0c2b 100644 --- a/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+ComplexTypes.swift +++ b/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+ComplexTypes.swift @@ -23,10 +23,9 @@ extension FieldValueConversionTests { timestamp: Date(timeIntervalSince1970: 1_000_000) ) let fieldValue = FieldValue.location(location) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .location) - if case .locationValue(let value) = components.value { + if case .LocationValue(let value) = components.value { #expect(value.latitude == 37.7749) #expect(value.longitude == -122.4194) #expect(value.horizontalAccuracy == 10.0) @@ -48,10 +47,9 @@ extension FieldValueConversionTests { } let location = FieldValue.Location(latitude: 0.0, longitude: 0.0) let fieldValue = FieldValue.location(location) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .location) - if case .locationValue(let value) = components.value { + if case .LocationValue(let value) = components.value { #expect(value.latitude == 0.0) #expect(value.longitude == 0.0) #expect(value.horizontalAccuracy == nil) @@ -73,10 +71,9 @@ extension FieldValueConversionTests { } let reference = FieldValue.Reference(recordName: "test-record-123") let fieldValue = FieldValue.reference(reference) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .reference) - if case .referenceValue(let value) = components.value { + if case .ReferenceValue(let value) = components.value { #expect(value.recordName == "test-record-123") #expect(value.action == nil) } else { @@ -92,10 +89,9 @@ extension FieldValueConversionTests { } let reference = FieldValue.Reference(recordName: "test-record-456", action: .deleteSelf) let fieldValue = FieldValue.reference(reference) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .reference) - if case .referenceValue(let value) = components.value { + if case .ReferenceValue(let value) = components.value { #expect(value.recordName == "test-record-456") #expect(value.action == .DELETE_SELF) } else { @@ -112,10 +108,9 @@ extension FieldValueConversionTests { let reference = FieldValue.Reference( recordName: "test-record-789", action: FieldValue.Reference.Action.none) let fieldValue = FieldValue.reference(reference) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .reference) - if case .referenceValue(let value) = components.value { + if case .ReferenceValue(let value) = components.value { #expect(value.recordName == "test-record-789") #expect(value.action == .NONE) } else { @@ -138,10 +133,9 @@ extension FieldValueConversionTests { downloadURL: "https://example.com/file.jpg" ) let fieldValue = FieldValue.asset(asset) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .asset) - if case .assetValue(let value) = components.value { + if case .AssetValue(let value) = components.value { #expect(value.fileChecksum == "abc123") #expect(value.size == 1_024) #expect(value.referenceChecksum == "def456") @@ -161,10 +155,9 @@ extension FieldValueConversionTests { } let asset = FieldValue.Asset() let fieldValue = FieldValue.asset(asset) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .asset) - if case .assetValue(let value) = components.value { + if case .AssetValue(let value) = components.value { #expect(value.fileChecksum == nil) #expect(value.size == nil) #expect(value.referenceChecksum == nil) diff --git a/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+EdgeCases.swift b/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+EdgeCases.swift index 643e4c34..ee12f18e 100644 --- a/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+EdgeCases.swift +++ b/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+EdgeCases.swift @@ -13,12 +13,12 @@ extension FieldValueConversionTests { return } let intZero = FieldValue.int64(0) - let intComponents = Components.Schemas.FieldValue(from: intZero) - #expect(intComponents.type == .int64) + let intComponents = Components.Schemas.FieldValueRequest(from: intZero) + // #expect(#expect(intComponents.type == .int64) let doubleZero = FieldValue.double(0.0) - let doubleComponents = Components.Schemas.FieldValue(from: doubleZero) - #expect(doubleComponents.type == .double) + let doubleComponents = Components.Schemas.FieldValueRequest(from: doubleZero) + // #expect(#expect(doubleComponents.type == .double) } @Test("Convert negative values") @@ -28,12 +28,12 @@ extension FieldValueConversionTests { return } let negativeInt = FieldValue.int64(-100) - let intComponents = Components.Schemas.FieldValue(from: negativeInt) - #expect(intComponents.type == .int64) + let intComponents = Components.Schemas.FieldValueRequest(from: negativeInt) + // #expect(#expect(intComponents.type == .int64) let negativeDouble = FieldValue.double(-3.14) - let doubleComponents = Components.Schemas.FieldValue(from: negativeDouble) - #expect(doubleComponents.type == .double) + let doubleComponents = Components.Schemas.FieldValueRequest(from: negativeDouble) + // #expect(#expect(doubleComponents.type == .double) } @Test("Convert large numbers") @@ -43,12 +43,12 @@ extension FieldValueConversionTests { return } let largeInt = FieldValue.int64(Int.max) - let intComponents = Components.Schemas.FieldValue(from: largeInt) - #expect(intComponents.type == .int64) + let intComponents = Components.Schemas.FieldValueRequest(from: largeInt) + // #expect(#expect(intComponents.type == .int64) let largeDouble = FieldValue.double(Double.greatestFiniteMagnitude) - let doubleComponents = Components.Schemas.FieldValue(from: largeDouble) - #expect(doubleComponents.type == .double) + let doubleComponents = Components.Schemas.FieldValueRequest(from: largeDouble) + // #expect(#expect(doubleComponents.type == .double) } @Test("Convert empty string") @@ -58,8 +58,7 @@ extension FieldValueConversionTests { return } let emptyString = FieldValue.string("") - let components = Components.Schemas.FieldValue(from: emptyString) - #expect(components.type == .string) + let components = Components.Schemas.FieldValueRequest(from: emptyString) } @Test("Convert string with special characters") @@ -69,8 +68,7 @@ extension FieldValueConversionTests { return } let specialString = FieldValue.string("Hello\nWorld\t🌍") - let components = Components.Schemas.FieldValue(from: specialString) - #expect(components.type == .string) + let components = Components.Schemas.FieldValueRequest(from: specialString) } } } diff --git a/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+Lists.swift b/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+Lists.swift index 5ad48370..ee712fdf 100644 --- a/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+Lists.swift +++ b/Tests/MistKitTests/Core/FieldValue/FieldValueConversionTests+Lists.swift @@ -14,10 +14,9 @@ extension FieldValueConversionTests { } let list: [FieldValue] = [.string("one"), .string("two"), .string("three")] let fieldValue = FieldValue.list(list) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .list) - if case .listValue(let values) = components.value { + if case .ListValue(let values) = components.value { #expect(values.count == 3) } else { Issue.record("Expected listValue") @@ -32,10 +31,9 @@ extension FieldValueConversionTests { } let list: [FieldValue] = [.int64(1), .int64(2), .int64(3)] let fieldValue = FieldValue.list(list) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .list) - if case .listValue(let values) = components.value { + if case .ListValue(let values) = components.value { #expect(values.count == 3) } else { Issue.record("Expected listValue") @@ -55,10 +53,9 @@ extension FieldValueConversionTests { FieldValue(booleanValue: true), ] let fieldValue = FieldValue.list(list) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .list) - if case .listValue(let values) = components.value { + if case .ListValue(let values) = components.value { #expect(values.count == 4) } else { Issue.record("Expected listValue") @@ -73,10 +70,9 @@ extension FieldValueConversionTests { } let list: [FieldValue] = [] let fieldValue = FieldValue.list(list) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .list) - if case .listValue(let values) = components.value { + if case .ListValue(let values) = components.value { #expect(values.isEmpty) } else { Issue.record("Expected listValue") @@ -92,13 +88,13 @@ extension FieldValueConversionTests { let innerList: [FieldValue] = [.string("a"), .string("b")] let outerList: [FieldValue] = [.list(innerList), .string("c")] let fieldValue = FieldValue.list(outerList) - let components = Components.Schemas.FieldValue(from: fieldValue) + let components = Components.Schemas.FieldValueRequest(from: fieldValue) - #expect(components.type == .list) - if case .listValue(let values) = components.value { + // FieldValueRequest does not have a type field - CloudKit infers type from structure + if case .ListValue(let values) = components.value { #expect(values.count == 2) } else { - Issue.record("Expected listValue") + Issue.record("Expected ListValue") } } } diff --git a/Tests/MistKitTests/Core/Platform.swift b/Tests/MistKitTests/Core/Platform.swift index 08bdacca..282535a4 100644 --- a/Tests/MistKitTests/Core/Platform.swift +++ b/Tests/MistKitTests/Core/Platform.swift @@ -11,4 +11,14 @@ internal enum Platform { } return false }() + + /// Returns true if running on WASM/WASI platform + /// WASM has limited memory (~65 MB linear), large allocations (15+ MB) will fail + internal static let isWasm: Bool = { + #if os(WASI) + return true + #else + return false + #endif + }() } diff --git a/Tests/MistKitTests/Core/RecordInfo/RecordInfoTests.swift b/Tests/MistKitTests/Core/RecordInfo/RecordInfoTests.swift index 05268a3f..a5811f5b 100644 --- a/Tests/MistKitTests/Core/RecordInfo/RecordInfoTests.swift +++ b/Tests/MistKitTests/Core/RecordInfo/RecordInfoTests.swift @@ -9,7 +9,7 @@ internal struct RecordInfoTests { /// Tests RecordInfo initialization with empty record data @Test("RecordInfo initialization with empty record data") internal func recordInfoWithUnknownRecord() { - let mockRecord = Components.Schemas.Record() + let mockRecord = Components.Schemas.RecordResponse() let recordInfo = RecordInfo(from: mockRecord) #expect(recordInfo.recordName == "Unknown") diff --git a/Tests/MistKitTests/Helpers/FilterBuilderTests.swift b/Tests/MistKitTests/Helpers/FilterBuilderTests.swift index 3810dd03..db2aa712 100644 --- a/Tests/MistKitTests/Helpers/FilterBuilderTests.swift +++ b/Tests/MistKitTests/Helpers/FilterBuilderTests.swift @@ -16,7 +16,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.equals("name", .string("John")) #expect(filter.comparator == .EQUALS) #expect(filter.fieldName == "name") - #expect(filter.fieldValue?.type == .string) } @Test("FilterBuilder creates NOT_EQUALS filter") @@ -28,7 +27,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.notEquals("age", .int64(25)) #expect(filter.comparator == .NOT_EQUALS) #expect(filter.fieldName == "age") - #expect(filter.fieldValue?.type == .int64) } // MARK: - Comparison Filters @@ -42,7 +40,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.lessThan("score", .double(100.0)) #expect(filter.comparator == .LESS_THAN) #expect(filter.fieldName == "score") - #expect(filter.fieldValue?.type == .double) } @Test("FilterBuilder creates LESS_THAN_OR_EQUALS filter") @@ -54,7 +51,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.lessThanOrEquals("count", .int64(50)) #expect(filter.comparator == .LESS_THAN_OR_EQUALS) #expect(filter.fieldName == "count") - #expect(filter.fieldValue?.type == .int64) } @Test("FilterBuilder creates GREATER_THAN filter") @@ -67,7 +63,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.greaterThan("createdAt", .date(date)) #expect(filter.comparator == .GREATER_THAN) #expect(filter.fieldName == "createdAt") - #expect(filter.fieldValue?.type == .timestamp) } @Test("FilterBuilder creates GREATER_THAN_OR_EQUALS filter") @@ -79,7 +74,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.greaterThanOrEquals("priority", .int64(3)) #expect(filter.comparator == .GREATER_THAN_OR_EQUALS) #expect(filter.fieldName == "priority") - #expect(filter.fieldValue?.type == .int64) } // MARK: - String Filters @@ -93,7 +87,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.beginsWith("title", "Hello") #expect(filter.comparator == .BEGINS_WITH) #expect(filter.fieldName == "title") - #expect(filter.fieldValue?.type == .string) } @Test("FilterBuilder creates NOT_BEGINS_WITH filter") @@ -105,7 +98,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.notBeginsWith("email", "spam") #expect(filter.comparator == .NOT_BEGINS_WITH) #expect(filter.fieldName == "email") - #expect(filter.fieldValue?.type == .string) } @Test("FilterBuilder creates CONTAINS_ALL_TOKENS filter") @@ -117,7 +109,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.containsAllTokens("description", "swift cloudkit") #expect(filter.comparator == .CONTAINS_ALL_TOKENS) #expect(filter.fieldName == "description") - #expect(filter.fieldValue?.type == .string) } // MARK: - List Filters @@ -132,7 +123,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.in("status", values) #expect(filter.comparator == .IN) #expect(filter.fieldName == "status") - #expect(filter.fieldValue?.type == .list) } @Test("FilterBuilder creates NOT_IN filter") @@ -145,7 +135,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.notIn("status", values) #expect(filter.comparator == .NOT_IN) #expect(filter.fieldName == "status") - #expect(filter.fieldValue?.type == .list) } @Test("FilterBuilder creates IN filter with numbers") @@ -158,7 +147,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.in("categoryId", values) #expect(filter.comparator == .IN) #expect(filter.fieldName == "categoryId") - #expect(filter.fieldValue?.type == .list) } // MARK: - List Member Filters @@ -172,7 +160,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.listContains("tags", .string("important")) #expect(filter.comparator == .LIST_CONTAINS) #expect(filter.fieldName == "tags") - #expect(filter.fieldValue?.type == .string) } @Test("FilterBuilder creates NOT_LIST_CONTAINS filter") @@ -184,7 +171,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.notListContains("tags", .string("spam")) #expect(filter.comparator == .NOT_LIST_CONTAINS) #expect(filter.fieldName == "tags") - #expect(filter.fieldValue?.type == .string) } @Test("FilterBuilder creates LIST_MEMBER_BEGINS_WITH filter") @@ -196,7 +182,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.listMemberBeginsWith("emails", "admin@") #expect(filter.comparator == .LIST_MEMBER_BEGINS_WITH) #expect(filter.fieldName == "emails") - #expect(filter.fieldValue?.type == .string) } @Test("FilterBuilder creates NOT_LIST_MEMBER_BEGINS_WITH filter") @@ -208,7 +193,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.notListMemberBeginsWith("domains", "spam") #expect(filter.comparator == .NOT_LIST_MEMBER_BEGINS_WITH) #expect(filter.fieldName == "domains") - #expect(filter.fieldValue?.type == .string) } // MARK: - Complex Value Tests @@ -234,7 +218,6 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.equals("owner", .reference(reference)) #expect(filter.comparator == .EQUALS) #expect(filter.fieldName == "owner") - #expect(filter.fieldValue?.type == .reference) } @Test("FilterBuilder handles location values") @@ -250,6 +233,5 @@ internal struct FilterBuilderTests { let filter = FilterBuilder.equals("location", .location(location)) #expect(filter.comparator == .EQUALS) #expect(filter.fieldName == "location") - #expect(filter.fieldValue?.type == .location) } } diff --git a/Tests/MistKitTests/Service/AssetUploadTokenTests.swift b/Tests/MistKitTests/Service/AssetUploadTokenTests.swift new file mode 100644 index 00000000..587a98d5 --- /dev/null +++ b/Tests/MistKitTests/Service/AssetUploadTokenTests.swift @@ -0,0 +1,126 @@ +// +// AssetUploadTokenTests.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Testing + +@testable import MistKit + +@Suite("AssetUploadToken Model Tests") +internal struct AssetUploadTokenTests { + @Test("AssetUploadToken initializes with all fields") + internal func assetUploadTokenInitializesWithAllFields() { + let token = AssetUploadToken( + url: "https://cvws.icloud-content.com/test-url", + recordName: "test-record", + fieldName: "testField" + ) + + #expect(token.url == "https://cvws.icloud-content.com/test-url") + #expect(token.recordName == "test-record") + #expect(token.fieldName == "testField") + } + + @Test("AssetUploadToken initializes with nil optional fields") + internal func assetUploadTokenInitializesWithNilOptionalFields() { + let token = AssetUploadToken( + url: nil, + recordName: nil, + fieldName: nil + ) + + #expect(token.url == nil) + #expect(token.recordName == nil) + #expect(token.fieldName == nil) + } + + @Test("AssetUploadToken supports equality comparison") + internal func assetUploadTokenSupportsEqualityComparison() { + let token1 = AssetUploadToken( + url: "https://example.com/test", + recordName: "record1", + fieldName: "field1" + ) + + let token2 = AssetUploadToken( + url: "https://example.com/test", + recordName: "record1", + fieldName: "field1" + ) + + let token3 = AssetUploadToken( + url: "https://example.com/different", + recordName: "record1", + fieldName: "field1" + ) + + #expect(token1 == token2, "Tokens with same values should be equal") + #expect(token1 != token3, "Tokens with different URLs should not be equal") + } + + @Test("AssetUploadReceipt initializes with all fields") + internal func assetUploadReceiptInitializesWithAllFields() { + let asset = FieldValue.Asset( + fileChecksum: "abc123", + size: 1024, + referenceChecksum: "ref456", + wrappingKey: "wrap789", + receipt: "receipt-token-xyz", + downloadURL: "https://cvws.icloud-content.com/download" + ) + + let result = AssetUploadReceipt( + asset: asset, + recordName: "test-record", + fieldName: "testField" + ) + + #expect(result.asset.fileChecksum == "abc123") + #expect(result.asset.size == 1024) + #expect(result.asset.receipt == "receipt-token-xyz") + #expect(result.recordName == "test-record") + #expect(result.fieldName == "testField") + } + + @Test("AssetUploadReceipt initializes with minimal asset data") + internal func assetUploadReceiptInitializesWithMinimalAssetData() { + let asset = FieldValue.Asset(receipt: "minimal-receipt") + + let result = AssetUploadReceipt( + asset: asset, + recordName: "record1", + fieldName: "field1" + ) + + #expect(result.asset.receipt == "minimal-receipt") + #expect(result.asset.fileChecksum == nil) + #expect(result.recordName == "record1") + #expect(result.fieldName == "field1") + } +} diff --git a/Tests/MistKitTests/Service/CloudKitServiceUploadTests+ErrorHandling.swift b/Tests/MistKitTests/Service/CloudKitServiceUploadTests+ErrorHandling.swift new file mode 100644 index 00000000..272dac64 --- /dev/null +++ b/Tests/MistKitTests/Service/CloudKitServiceUploadTests+ErrorHandling.swift @@ -0,0 +1,94 @@ +// +// CloudKitServiceUploadTests+ErrorHandling.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Testing + +@testable import MistKit + +extension CloudKitServiceUploadTests { + @Suite("Error Handling") + internal struct ErrorHandling { + @Test("uploadAssets() handles unauthorized error (401)") + internal func uploadAssetsHandlesUnauthorizedError() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceUploadTests.makeAuthErrorService() + let testData = Data(count: 1024) + + do { + _ = try await service.uploadAssets( + data: testData, + recordType: "Note", + fieldName: "image" + ) + Issue.record("Expected authentication error") + } catch let error as CloudKitError { + if case .httpErrorWithDetails(let statusCode, let serverErrorCode, let reason) = error { + #expect(statusCode == 401, "Should return 401 Unauthorized") + #expect(serverErrorCode == "AUTHENTICATION_FAILED") + #expect(reason == "Authentication failed") + } else { + Issue.record("Expected httpErrorWithDetails error, got \(error)") + } + } catch { + Issue.record("Expected CloudKitError, got \(type(of: error))") + } + } + + @Test("uploadAssets() handles bad request error (400)") + internal func uploadAssetsHandlesBadRequestError() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceUploadTests.makeUploadValidationErrorService(.emptyData) + let testData = Data() // Empty data triggers 400 + + do { + _ = try await service.uploadAssets( + data: testData, + recordType: "Note", + fieldName: "image" + ) + Issue.record("Expected bad request error") + } catch let error as CloudKitError { + if case .httpErrorWithRawResponse(let statusCode, _) = error { + #expect(statusCode == 400, "Should return 400 Bad Request") + } else { + Issue.record("Expected httpErrorWithRawResponse error, got \(error)") + } + } catch { + Issue.record("Expected CloudKitError, got \(type(of: error))") + } + } + } +} diff --git a/Tests/MistKitTests/Service/CloudKitServiceUploadTests+Helpers.swift b/Tests/MistKitTests/Service/CloudKitServiceUploadTests+Helpers.swift new file mode 100644 index 00000000..851667c0 --- /dev/null +++ b/Tests/MistKitTests/Service/CloudKitServiceUploadTests+Helpers.swift @@ -0,0 +1,216 @@ +// +// CloudKitServiceUploadTests+Helpers.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import HTTPTypes +import Testing + +@testable import MistKit + +extension CloudKitServiceUploadTests { + /// Create service for successful upload operations + /// Test API token in 64-character hexadecimal format as required by MistKit validation + private static let testAPIToken = "abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234" + + /// Create a mock asset uploader that returns a successful upload response + internal static func makeMockAssetUploader() -> AssetUploader { + { data, url in + let response = """ + { + "singleFile": { + "wrappingKey": "test-wrapping-key-abc123", + "fileChecksum": "test-checksum-def456", + "receipt": "test-receipt-token-xyz", + "referenceChecksum": "test-ref-checksum-789", + "size": \(data.count) + } + } + """ + return (200, Data(response.utf8)) + } + } + + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) + internal static func makeSuccessfulUploadService( + tokenCount: Int = 1 + ) async throws -> CloudKitService { + let responseProvider = ResponseProvider.successfulUpload(tokenCount: tokenCount) + + let transport = MockTransport(responseProvider: responseProvider) + return try CloudKitService( + containerIdentifier: "iCloud.com.example.test", + apiToken: testAPIToken, + transport: transport + ) + } + + /// Create service for validation error testing + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) + internal static func makeUploadValidationErrorService( + _ errorType: UploadValidationErrorType + ) async throws -> CloudKitService { + let responseProvider = ResponseProvider.uploadValidationError(errorType) + + let transport = MockTransport(responseProvider: responseProvider) + return try CloudKitService( + containerIdentifier: "iCloud.com.example.test", + apiToken: testAPIToken, + transport: transport + ) + } + + /// Create service for auth errors + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) + internal static func makeAuthErrorService() async throws -> CloudKitService { + let responseProvider = ResponseProvider.authenticationError() + + let transport = MockTransport(responseProvider: responseProvider) + return try CloudKitService( + containerIdentifier: "iCloud.com.example.test", + apiToken: testAPIToken, + transport: transport + ) + } + + /// Create service for asset data upload testing + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) + internal static func makeAssetDataUploadService(tokenCount: Int = 1) async throws -> CloudKitService { + let responseProvider = ResponseProvider.successfulUpload(tokenCount: tokenCount) + + let transport = MockTransport(responseProvider: responseProvider) + return try CloudKitService( + containerIdentifier: "iCloud.com.example.test", + apiToken: testAPIToken, + transport: transport + ) + } +} + +/// Types of upload validation errors that can occur +internal enum UploadValidationErrorType: Sendable { + case emptyData + case oversizedAsset(Int) +} + +// MARK: - Upload Response Builders + +extension ResponseProvider { + /// Response provider for successful upload operations + internal static func successfulUpload(tokenCount: Int = 1) -> ResponseProvider { + ResponseProvider(defaultResponse: .successfulUploadResponse(tokenCount: tokenCount)) + } + + /// Response provider for upload validation errors + internal static func uploadValidationError(_ type: UploadValidationErrorType) -> ResponseProvider { + ResponseProvider(defaultResponse: .uploadValidationError(type)) + } +} + +extension ResponseConfig { + /// Creates a successful asset upload response + /// + /// - Parameter tokenCount: Number of upload tokens to include in response + /// - Returns: ResponseConfig with successful upload response + internal static func successfulUploadResponse(tokenCount: Int = 1) -> ResponseConfig { + var tokens: [[String: Any]] = [] + for index in 0.. ResponseConfig { + let reason: String + switch type { + case .emptyData: + reason = "Asset data cannot be empty" + case .oversizedAsset(let size): + reason = "Asset size \(size) bytes exceeds maximum allowed size of 262144000 bytes (250 MB)" + } + + return cloudKitError( + statusCode: 400, + serverErrorCode: "BAD_REQUEST", + reason: reason + ) + } + + /// Creates a successful asset data upload response (binary upload to CDN) + /// + /// - Returns: ResponseConfig with CloudKit asset upload response + internal static func successfulAssetDataUpload() -> ResponseConfig { + let responseJSON = """ + { + "singleFile": { + "wrappingKey": "test-wrapping-key-abc123", + "fileChecksum": "test-checksum-def456", + "receipt": "test-receipt-token-xyz", + "referenceChecksum": "test-ref-checksum-789", + "size": 1024 + } + } + """ + + var headers = HTTPFields() + headers[.contentType] = "application/json" + + return ResponseConfig( + statusCode: 200, + headers: headers, + body: responseJSON.data(using: .utf8), + error: nil + ) + } +} diff --git a/Tests/MistKitTests/Service/CloudKitServiceUploadTests+SuccessCases.swift b/Tests/MistKitTests/Service/CloudKitServiceUploadTests+SuccessCases.swift new file mode 100644 index 00000000..f62e7d37 --- /dev/null +++ b/Tests/MistKitTests/Service/CloudKitServiceUploadTests+SuccessCases.swift @@ -0,0 +1,102 @@ +// +// CloudKitServiceUploadTests+SuccessCases.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Testing + +@testable import MistKit + +extension CloudKitServiceUploadTests { + @Suite("Success Cases") + internal struct SuccessCases { + @Test("uploadAssets() successfully uploads valid asset") + internal func uploadAssetsSuccessfullyUploadsValidAsset() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceUploadTests.makeSuccessfulUploadService(tokenCount: 1) + let testData = Data(count: 1024) // 1 KB of test data + + let result = try await service.uploadAssets( + data: testData, + recordType: "Note", + fieldName: "image", + using: CloudKitServiceUploadTests.makeMockAssetUploader() + ) + + #expect(result.recordName.isEmpty == false, "Result should have a record name") + #expect(result.fieldName == "file", "Result should have the field name from mock response") + #expect(result.asset.receipt != nil, "Asset should have a receipt from CloudKit") + } + + @Test("uploadAssets() parses single token from response") + internal func uploadAssetsParseSingleToken() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceUploadTests.makeSuccessfulUploadService(tokenCount: 1) + let testData = Data(count: 2048) + + let result = try await service.uploadAssets( + data: testData, + recordType: "Note", + fieldName: "image", + using: CloudKitServiceUploadTests.makeMockAssetUploader() + ) + + #expect(result.recordName == "test-record-0") + #expect(result.fieldName == "file") + #expect(result.asset.receipt != nil) + } + + @Test("uploadAssets() returns a single token") + internal func uploadAssetsReturnsSingleToken() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceUploadTests.makeSuccessfulUploadService(tokenCount: 1) + let testData = Data(count: 4096) + + let result = try await service.uploadAssets( + data: testData, + recordType: "Note", + fieldName: "image", + using: CloudKitServiceUploadTests.makeMockAssetUploader() + ) + + // Verify result has the expected fields + #expect(result.recordName == "test-record-0") + #expect(result.fieldName == "file") + #expect(result.asset.receipt != nil) + } + } +} diff --git a/Tests/MistKitTests/Service/CloudKitServiceUploadTests+Validation.swift b/Tests/MistKitTests/Service/CloudKitServiceUploadTests+Validation.swift new file mode 100644 index 00000000..dc1104d9 --- /dev/null +++ b/Tests/MistKitTests/Service/CloudKitServiceUploadTests+Validation.swift @@ -0,0 +1,131 @@ +// +// CloudKitServiceUploadTests+Validation.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Testing + +@testable import MistKit + +extension CloudKitServiceUploadTests { + @Suite("Validation") + internal struct Validation { + @Test("uploadAssets() validates empty data") + internal func uploadAssetsValidatesEmptyData() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceUploadTests.makeUploadValidationErrorService(.emptyData) + + do { + _ = try await service.uploadAssets( + data: Data(), + recordType: "Note", + fieldName: "image" + ) + Issue.record("Expected error for empty data") + } catch let error as CloudKitError { + // Verify we get the correct validation error + if case .httpErrorWithRawResponse(let statusCode, let response) = error { + #expect(statusCode == 400) + #expect(response.contains("Asset data cannot be empty")) + } else { + Issue.record("Expected httpErrorWithRawResponse error") + } + } catch { + Issue.record("Expected CloudKitError, got \(type(of: error))") + } + } + + @Test("uploadAssets() validates 15 MB size limit", .disabled(if: Platform.isWasm)) + internal func uploadAssetsValidates15MBLimit() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + // Create data just over 15 MB (15 * 1024 * 1024 + 1 bytes) + let oversizedData = Data(count: 15_728_641) + let service = try await CloudKitServiceUploadTests.makeUploadValidationErrorService( + .oversizedAsset(oversizedData.count) + ) + + do { + _ = try await service.uploadAssets( + data: oversizedData, + recordType: "Note", + fieldName: "image" + ) + Issue.record("Expected error for oversized asset") + } catch let error as CloudKitError { + // Verify we get the correct validation error + if case .httpErrorWithRawResponse(let statusCode, let response) = error { + #expect(statusCode == 413) + #expect(response.contains("exceeds maximum")) + } else { + Issue.record("Expected httpErrorWithRawResponse error, got \(error)") + } + } catch { + Issue.record("Expected CloudKitError, got \(type(of: error))") + } + } + + @Test("uploadAssets() accepts valid data sizes", .disabled(if: Platform.isWasm)) + internal func uploadAssetsAcceptsValidSizes() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceUploadTests.makeSuccessfulUploadService() + + // Test various valid sizes (CloudKit limit is 15 MB) + let validSizes = [ + 1, // 1 byte + 1024, // 1 KB + 1024 * 1024, // 1 MB + 10 * 1024 * 1024, // 10 MB + 15 * 1024 * 1024 // Exactly 15 MB (maximum allowed) + ] + + for size in validSizes { + let data = Data(count: size) + do { + let result = try await service.uploadAssets( + data: data, + recordType: "Note", + fieldName: "image", + using: CloudKitServiceUploadTests.makeMockAssetUploader() + ) + #expect(result.asset.receipt != nil, "Should receive asset with receipt") + } catch { + Issue.record("Valid size \(size) bytes should not throw error: \(error)") + } + } + } + } +} diff --git a/Tests/MistKitTests/Service/CloudKitServiceUploadTests.swift b/Tests/MistKitTests/Service/CloudKitServiceUploadTests.swift new file mode 100644 index 00000000..f039b105 --- /dev/null +++ b/Tests/MistKitTests/Service/CloudKitServiceUploadTests.swift @@ -0,0 +1,36 @@ +// +// CloudKitServiceUploadTests.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Testing + +@testable import MistKit + +@Suite("CloudKitService Upload Operations", .enabled(if: Platform.isCryptoAvailable)) +internal enum CloudKitServiceUploadTests {} diff --git a/openapi-generator-config.yaml b/openapi-generator-config.yaml index 2a971a43..8942f958 100644 --- a/openapi-generator-config.yaml +++ b/openapi-generator-config.yaml @@ -2,9 +2,6 @@ generate: - types - client accessModifier: internal -typeOverrides: - schemas: - FieldValue: CustomFieldValue additionalFileComments: - periphery:ignore:all - swift-format-ignore-file diff --git a/openapi.yaml b/openapi.yaml index ba1b74ac..02390e67 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -642,8 +642,13 @@ paths: /database/{version}/{container}/{environment}/{database}/assets/upload: post: - summary: Upload Assets - description: Upload binary assets to CloudKit + summary: Request Asset Upload URLs + description: | + Request upload URLs for asset fields. This is the first step in a two-step process: + 1. Request upload URLs by specifying the record type and field name + 2. Upload the actual binary data to the returned URL (separate HTTP request) + + Upload URLs are valid for 15 minutes. Maximum file size is 15 MB. operationId: uploadAssets tags: - Assets @@ -655,16 +660,36 @@ paths: requestBody: required: true content: - multipart/form-data: + application/json: schema: type: object properties: - file: - type: string - format: binary + zoneID: + $ref: '#/components/schemas/ZoneID' + description: Optional zone ID. Defaults to default zone if not specified. + tokens: + type: array + description: Array of asset fields to request upload URLs for + items: + type: object + required: + - recordType + - fieldName + properties: + recordName: + type: string + description: Unique name to identify the record. Defaults to random UUID if not specified. + recordType: + type: string + description: Name of the record type + fieldName: + type: string + description: Name of the Asset or Asset list field + required: + - tokens responses: '200': - description: Asset uploaded successfully + description: Upload URLs returned successfully content: application/json: schema: @@ -802,7 +827,7 @@ components: fieldName: type: string fieldValue: - $ref: '#/components/schemas/FieldValue' + $ref: '#/components/schemas/FieldValueRequest' Sort: type: object @@ -819,10 +844,11 @@ components: type: string enum: [create, update, forceUpdate, replace, forceReplace, delete, forceDelete] record: - $ref: '#/components/schemas/Record' + $ref: '#/components/schemas/RecordRequest' - Record: + RecordRequest: type: object + description: Record schema for API requests (fields use FieldValueRequest) properties: recordName: type: string @@ -835,13 +861,54 @@ components: description: Change tag for optimistic concurrency control fields: type: object - description: Record fields with their values and types + description: Record fields with their values (no type metadata) additionalProperties: - $ref: '#/components/schemas/FieldValue' + $ref: '#/components/schemas/FieldValueRequest' - FieldValue: + RecordResponse: type: object - description: A CloudKit field value with its type information + description: Record schema for API responses (fields use FieldValueResponse) + properties: + recordName: + type: string + description: The unique identifier for the record + recordType: + type: string + description: The record type (schema name) + recordChangeTag: + type: string + description: Change tag for optimistic concurrency control + fields: + type: object + description: Record fields with their values and optional type information + additionalProperties: + $ref: '#/components/schemas/FieldValueResponse' + + FieldValueRequest: + type: object + description: | + A CloudKit field value for API requests. + The type field is omitted as CloudKit infers types from the value structure. + properties: + value: + oneOf: + - $ref: '#/components/schemas/StringValue' + - $ref: '#/components/schemas/Int64Value' + - $ref: '#/components/schemas/DoubleValue' + - $ref: '#/components/schemas/BytesValue' + - $ref: '#/components/schemas/DateValue' + - $ref: '#/components/schemas/LocationValue' + - $ref: '#/components/schemas/ReferenceValue' + - $ref: '#/components/schemas/AssetValue' + - $ref: '#/components/schemas/ListValue' + required: + - value + + FieldValueResponse: + type: object + description: | + A CloudKit field value from API responses. + May include optional type field for explicit type information. properties: value: oneOf: @@ -857,7 +924,9 @@ components: type: type: string enum: [STRING, INT64, DOUBLE, BYTES, REFERENCE, ASSET, ASSETID, LOCATION, TIMESTAMP, LIST] - description: The CloudKit field type + description: The CloudKit field type (optional, may be inferred from value) + required: + - value StringValue: type: string @@ -1016,7 +1085,7 @@ components: records: type: array items: - $ref: '#/components/schemas/Record' + $ref: '#/components/schemas/RecordResponse' continuationMarker: type: string @@ -1026,7 +1095,7 @@ components: records: type: array items: - $ref: '#/components/schemas/Record' + $ref: '#/components/schemas/RecordResponse' LookupResponse: type: object @@ -1034,7 +1103,7 @@ components: records: type: array items: - $ref: '#/components/schemas/Record' + $ref: '#/components/schemas/RecordResponse' ChangesResponse: type: object @@ -1042,7 +1111,7 @@ components: records: type: array items: - $ref: '#/components/schemas/Record' + $ref: '#/components/schemas/RecordResponse' syncToken: type: string moreComing: