Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ opt_in_rules:
- contains_over_range_nil_comparison
- convenience_type
- discouraged_object_literal
- discouraged_optional_boolean
- empty_collection_literal
- empty_count
- empty_string
Expand Down
14 changes: 12 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,18 @@ swift run mistdemo demo-errors
swift run mistdemo test-public
swift run mistdemo test-private

# Run with specific configuration
swift run mistdemo --config-file ~/.mistdemo/config.json query
# Configuration (no config-file flag — MistDemo uses Swift Configuration):
# highest priority first — (1) CLI args, (2) CLOUDKIT_-prefixed env vars,
# (3) a .env file in the working dir (Examples/MistDemo/.env, CLOUDKIT_-prefixed),
# (4) in-memory defaults. Provide credentials via env vars or .env, e.g.:
# CLOUDKIT_CONTAINER_ID=iCloud.com.yourorg.yourapp
# CLOUDKIT_ENVIRONMENT=development
# CLOUDKIT_API_TOKEN=… CLOUDKIT_WEB_AUTH_TOKEN=… # web-auth scopes
# CLOUDKIT_KEY_ID=… CLOUDKIT_PRIVATE_KEY[_PATH]=… # server-to-server
# Recognized keys: CLOUDKIT_CONTAINER_ID, CLOUDKIT_DATABASE, CLOUDKIT_ENVIRONMENT,
# CLOUDKIT_API_TOKEN, CLOUDKIT_WEB_AUTH_TOKEN, CLOUDKIT_KEY_ID, CLOUDKIT_PRIVATE_KEY,
# CLOUDKIT_PRIVATE_KEY_PATH, CLOUDKIT_LOOKUP_EMAIL, CLOUDKIT_ERROR.
swift run mistdemo query
```

## Architecture Considerations
Expand Down
4 changes: 2 additions & 2 deletions Examples/BushelCloud/.gitrepo
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[subrepo]
remote = git@github.com:brightdigit/BushelCloud.git
branch = mistkit
commit = fa92cff25dd3bd632c567e19e383689f74f86056
parent = 86e2fe6ab292be4b005126f12b1520e6c60c22dd
commit = 66bbe468d8a80c375b1a03c229f68a510eaf4f0a
parent = 3b84901cd775dfc0fb928ce4b4cc7e52c0badc64
method = merge
cmdver = 0.4.9
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,9 @@ public struct BushelCloudKitService: Sendable, RecordManaging, CloudKitRecordCol
case .failure(let error):
totalFailed += 1
batchFailed += 1
failedRecordNames.append(error.recordName)
failedRecordNames.append(error.identifier)
Self.logger.debug(
"Error: recordName=\(error.recordName), code=\(error.serverErrorCode.rawValue)"
"Error: recordName=\(error.identifier), code=\(error.serverErrorCode.rawValue)"
)
case .success(let record):
batchSucceeded += 1
Expand Down
1 change: 0 additions & 1 deletion Examples/MistDemo/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ opt_in_rules:
- contains_over_range_nil_comparison
- convenience_type
- discouraged_object_literal
- discouraged_optional_boolean
- empty_collection_literal
- empty_count
- empty_string
Expand Down
6 changes: 3 additions & 3 deletions Examples/MistDemo/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@

extension PlatformApplicationDelegate where Self: NSApplicationDelegate {
/// A remote notification arrived (AppKit variant).
///
/// Cannot be `@objc` because Swift forbids `@objc` on protocol extension
/// members; AppKit's optional `application(_:didReceiveRemoteNotification:)`
/// selector is bridged by a concrete `@objc` shim on
/// ``PushNotificationDelegate`` that delegates here.
public func application(
_ application: NSApplication,
_: NSApplication,
didReceiveRemoteNotification userInfo: [String: Any]
) {
Self.receiver?.didReceiveRemoteNotification(userInfo: userInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@
extension PlatformApplicationDelegate {
/// APNs delivered a device token — forward it to the registered receiver.
///
/// `public` because, when adopted by a `public` class, this satisfies the
/// matching requirement in the `public` system delegate protocol.
/// Cannot be `@objc` (Swift forbids `@objc` on protocol extension
/// members); the optional system-delegate selector is bridged by a
/// concrete `@objc` shim on ``PushNotificationDelegate`` that
/// delegates here.
public func application(
_ application: PlatformApplication,
_: PlatformApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Self.receiver?.didRegisterForRemoteNotifications(deviceToken: deviceToken)
Expand All @@ -45,7 +47,7 @@
/// APNs refused registration — forward the error to the receiver so it
/// can surface in the UI.
public func application(
_ application: PlatformApplication,
_: PlatformApplication,
didFailToRegisterForRemoteNotificationsWithError error: any Error
) {
Self.receiver?.didFailToRegisterForRemoteNotifications(error: error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,94 @@
override public init() {
super.init()
}

// MARK: - Obj-C bridge for system-delegate optional selectors
//
// Swift forbids `@objc` on protocol extension members, so the
// ``PlatformApplicationDelegate`` defaults can't directly satisfy the
// optional `@objc` requirements on the system delegate protocols. These
// concrete shims expose the selectors to the Obj-C runtime so AppKit /
// UIKit / WatchKit dispatch lands on this class, and forward to the
// same `static weak receiver` the protocol extensions use.

#if canImport(AppKit) && !targetEnvironment(macCatalyst)
/// APNs delivered a device token (AppKit variant).
@objc
public func application(
_ application: NSApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Self.receiver?.didRegisterForRemoteNotifications(deviceToken: deviceToken)
}

/// APNs refused registration (AppKit variant).
@objc
public func application(
_ application: NSApplication,
didFailToRegisterForRemoteNotificationsWithError error: any Error
) {
Self.receiver?.didFailToRegisterForRemoteNotifications(error: error)
}

/// A remote notification arrived (AppKit variant).
@objc
public func application(
_ application: NSApplication,
didReceiveRemoteNotification userInfo: [String: Any]
) {
Self.receiver?.didReceiveRemoteNotification(userInfo: userInfo)
}
#elseif canImport(UIKit) && !os(watchOS)
/// APNs delivered a device token (UIKit variant).
@objc
public func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Self.receiver?.didRegisterForRemoteNotifications(deviceToken: deviceToken)
}

/// APNs refused registration (UIKit variant).
@objc
public func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: any Error
) {
Self.receiver?.didFailToRegisterForRemoteNotifications(error: error)
}

/// A remote notification arrived (UIKit variant).
@objc
public func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
Self.receiver?.didReceiveRemoteNotification(userInfo: userInfo)
completionHandler(.newData)
}
#elseif canImport(WatchKit)
/// APNs delivered a device token (watchOS variant).
@objc
public func didRegisterForRemoteNotifications(withDeviceToken deviceToken: Data) {
Self.receiver?.didRegisterForRemoteNotifications(deviceToken: deviceToken)
}

/// APNs refused registration (watchOS variant).
@objc
public func didFailToRegisterForRemoteNotificationsWithError(_ error: any Error) {
Self.receiver?.didFailToRegisterForRemoteNotifications(error: error)
}

/// A remote notification arrived (watchOS variant).
@objc
public func didReceiveRemoteNotification(
_ userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (WKBackgroundFetchResult) -> Void
) {
Self.receiver?.didReceiveRemoteNotification(userInfo: userInfo)
completionHandler(.newData)
}
#endif
}
#endif
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// CreateTokenPhase.swift
// AtomicBool.swift
// MistDemo
//
// Created by Leo Dion.
Expand Down Expand Up @@ -29,20 +29,20 @@

internal import Foundation

/// Stub phase for `tokens/create`. Not wired into the public/private
/// pipelines yet; `#52` flips this into a real run when the MistKit Swift
/// wrapper lands.
internal struct CreateTokenPhase: IntegrationPhase {
internal typealias Input = NoState
internal typealias Output = NoState
/// Minimal atomic Bool — avoids pulling in Atomics for one flag.
///
/// `Sendable` conformance is sound because all mutation goes through the
/// `NSLock`; the `value` field is never read or written outside `exchange`.
internal final class AtomicBool: Sendable {
private let lock = NSLock()
nonisolated(unsafe) private var value = false

internal static let title = "Create token (pending #52)"
internal static let emoji = "🎟️"
internal static let apiName = "createToken"

internal func run(input: NoState, context: PhaseContext) async throws -> NoState {
print("\n\(Self.emoji) \(Self.title)")
PendingStub.printPending(endpoint: "tokens/create", trackingIssue: 52)
return NoState()
/// Set to `newValue` and return the prior value.
internal func exchange(_ newValue: Bool) -> Bool {
lock.lock()
defer { lock.unlock() }
let old = value
value = newValue
return old
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,38 @@
//

internal import Foundation
internal import MistKit

/// Stub command for `tokens/create`. Creates an APNs token CloudKit can use
/// to deliver push notifications to a registered subscription. MistKit
/// Swift wrapper tracked in #52.
public struct CreateTokenCommand: MistDemoCommand {
// `helpText` below is a multi-line string whose option column doesn't align
// with Swift's indent steps; the rule isn't useful inside literal help text.
// swiftlint:disable indentation_width

/// Command for `tokens/create`. Mints a CloudKit-managed APNs token that
/// non-device callers use as the destination for subscription-triggered pushes.
public struct CreateTokenCommand: MistDemoCommand, OutputFormatting {
/// The configuration type.
public typealias Config = CreateTokenConfig
/// The command name.
public static let commandName = "create-token"
/// The command abstract.
public static let abstract = "Create an APNs token for CloudKit subscriptions (pending #52)"
public static let abstract = "Create an APNs token for CloudKit subscriptions"
/// The command help text.
public static let helpText = """
CREATE-TOKEN - Create an APNs token for CloudKit subscriptions

USAGE:
mistdemo create-token --apns-token <hex> [--apns-environment <env>]
mistdemo create-token [--apns-environment <env>] [--client-id <uuid>]

OPTIONS:
--apns-token <hex> APNs device token (hex string)
--apns-environment <env> APNs environment (development, production)
--apns-environment <env> APNs environment, default development
--client-id <uuid> Logical CloudKit client identifier
(default: fresh UUID per call)
--database <type> Database to target
--output-format <format> Output format (json, table, csv, yaml)

STATUS:
Not yet implemented — pending MistKit support, tracked in #52.
EXAMPLES:
mistdemo create-token --apns-environment development
mistdemo create-token --client-id 1A2B3C4D-... # stable identity
"""

private let config: CreateTokenConfig
Expand All @@ -64,6 +71,25 @@ public struct CreateTokenCommand: MistDemoCommand {

/// Executes the command.
public func execute() async throws {
PendingStub.printPending(endpoint: "tokens/create", trackingIssue: 52)
let environment = try resolveEnvironment()
let service = try MistKitClientFactory.create(for: config.base)
let result = try await service.createAPNsToken(
environment: environment,
clientId: config.clientId,
database: config.base.database
)
try await outputResult(result, format: config.output)
}

private func resolveEnvironment() throws -> APNsEnvironment {
guard let raw = config.apnsEnvironment else {
return .development
}
guard let environment = APNsEnvironment(rawValue: raw) else {
throw TokenCommandError.invalidEnvironment(raw)
}
return environment
}
}

// swiftlint:enable indentation_width
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@
//

internal import Foundation
internal import MistKit

/// Stub command for `subscriptions/list`. Lists every CloudKit subscription
/// registered against the selected database. MistKit Swift wrapper tracked
/// in #49.
public struct ListSubscriptionsCommand: MistDemoCommand {
/// Command for `subscriptions/list`. Lists every CloudKit subscription
/// registered against the selected database.
public struct ListSubscriptionsCommand: MistDemoCommand, OutputFormatting {
/// The configuration type.
public typealias Config = ListSubscriptionsConfig
/// The command name.
public static let commandName = "list-subscriptions"
/// The command abstract.
public static let abstract = "List CloudKit subscriptions (pending #49)"
public static let abstract = "List CloudKit subscriptions"
/// The command help text.
public static let helpText = """
LIST-SUBSCRIPTIONS - List CloudKit subscriptions
Expand All @@ -50,8 +50,8 @@ public struct ListSubscriptionsCommand: MistDemoCommand {
--database <type> Database to target (private, shared, public)
--output-format <format> Output format (json, table, csv, yaml)

STATUS:
Not yet implemented — pending MistKit support, tracked in #49.
EXAMPLES:
mistdemo list-subscriptions --database private
"""

private let config: ListSubscriptionsConfig
Expand All @@ -63,6 +63,8 @@ public struct ListSubscriptionsCommand: MistDemoCommand {

/// Executes the command.
public func execute() async throws {
PendingStub.printPending(endpoint: "subscriptions/list", trackingIssue: 49)
let service = try MistKitClientFactory.create(for: config.base)
let subscriptions = try await service.listSubscriptions(database: config.base.database)
try await outputResults(subscriptions, format: config.output)
}
}
Loading
Loading