diff --git a/Package.resolved b/Package.resolved index d9823ed..fab81b6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "65db2e61c18e81132241ea93ca8a61c58429030954e6c853d3720afa9aeab01c", + "originHash" : "090c675500195d294a5f4987351db53f4409bd98981b45c7a1f34a60685e39b7", "pins" : [ + { + "identity" : "anycodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flight-School/AnyCodable", + "state" : { + "revision" : "69261f239f0fffaf51495dadc4f8483fbfe97025", + "version" : "0.6.1" + } + }, { "identity" : "swiftlintplugins", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 9074893..72867e9 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,7 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/Flight-School/AnyCodable", exact: "0.6.1"), .package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", exact: "0.55.1") ], targets: [ @@ -21,6 +22,7 @@ let package = Package( // Targets can depend on other targets in this package and products from dependencies. .target( name: "Passage", + dependencies: ["AnyCodable"], plugins: [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")] ), .testTarget( diff --git a/Sources/Passage/errors/PassageAppError.swift b/Sources/Passage/errors/PassageAppError.swift new file mode 100644 index 0000000..051d5ef --- /dev/null +++ b/Sources/Passage/errors/PassageAppError.swift @@ -0,0 +1,38 @@ +import Foundation + +public enum PassageAppError: PassageError { + + case appNotFound(message: String) + case invalidRequest(message: String) + case unspecified(message: String) + + public var errorDescription: String { + switch self { + case .appNotFound(let message), + .invalidRequest(let message), + .unspecified(let message): + return message + } + } + + public static func convert(error: Error) -> PassageAppError { + // Check if error is already proper + if let passageAppError = error as? PassageAppError { + return passageAppError + } + // Handle client error + if let errorResponse = error as? ErrorResponse, + let (_, errorData) = PassageErrorData.getData(from: errorResponse) + { + switch errorData.code { + case Model404Code.appNotFound.rawValue: + return .appNotFound(message: errorData.error) + case Model400Code.request.rawValue: + return .invalidRequest(message: errorData.error) + default: () + } + } + return .unspecified(message: "unspecified error") + } + +} diff --git a/Sources/Passage/errors/PassageError.swift b/Sources/Passage/errors/PassageError.swift new file mode 100644 index 0000000..4cd77af --- /dev/null +++ b/Sources/Passage/errors/PassageError.swift @@ -0,0 +1,21 @@ +import Foundation + +public protocol PassageError: Error, LocalizedError { + static func convert(error: Error) -> Self + var errorDescription: String { get } +} + +struct PassageErrorData: Codable { + let code: String + let error: String + + static func getData(from errorResponse: ErrorResponse) -> (Int, PassageErrorData)? { + guard + case let ErrorResponse.error(statusCode, data?, _, _) = errorResponse, + let errorData = try? JSONDecoder().decode(PassageErrorData.self, from: data) + else { + return nil + } + return (statusCode, errorData) + } +} diff --git a/Sources/Passage/interfaces/PassageApp.swift b/Sources/Passage/interfaces/PassageApp.swift new file mode 100644 index 0000000..3d97cf9 --- /dev/null +++ b/Sources/Passage/interfaces/PassageApp.swift @@ -0,0 +1,75 @@ +import AnyCodable + +/// A class representing the Passage application, allowing interaction with the application's API. +public class PassageApp { + + private let appId: String + + init(appId: String) { + self.appId = appId + } + + /// Fetches information about the Passage app. + /// - Returns: A PassageAppInfo struct containing the app's details. + /// - Throws: `PassageAppError` if the API call fails. + public func info() async throws -> PassageAppInfo { + do { + let appInfoResponse = try await AppsAPI.getApp(appId: appId) + return appInfoResponse.app + } catch { + throw PassageAppError.convert(error: error) + } + } + + /// Checks if a user exists for a given identifier. + /// + /// - Parameter identifier: The identifier (e.g., email or phone number) to check for existence. + /// - Returns: A PublicUserInfo struct if the user exists, or `nil` if not. + /// - Throws: `PassageAppError` if the request is invalid or an error occurs during the API call. + public func userExists(identifier: String) async throws -> PublicUserInfo? { + do { + guard let safeId = identifier + .addingPercentEncoding(withAllowedCharacters: .alphanumerics) + else { + throw PassageAppError.invalidRequest(message: "invalid identifier") + } + let response = try await UsersAPI + .checkUserIdentifier( + appId: appId, + identifier: safeId + ) + return response.user + } catch { + throw PassageAppError.convert(error: error) + } + } + + /// Creates a new user with the specified identifier and optional metadata. + /// - Parameters: + /// - identifier: The identifier (e.g., email or phone number) for the new user. + /// - userMetadata: Optional metadata to associate with the user. + /// - Returns: A PublicUserInfo struct representing the newly created user. + /// - Throws: `PassageAppError` if an error occurs during user creation or the API call fails. + public func createUser( + identifier: String, + userMetadata: AnyCodable? = nil + ) async throws -> PublicUserInfo { + do { + let params = CreateUserParams( + identifier: identifier, + userMetadata: userMetadata + ) + let resposne = try await UsersAPI.createUser( + appId: appId, + createUserParams: params + ) + guard let user = resposne.user else { + throw PassageAppError.invalidRequest(message: "invalid request") + } + return user + } catch { + throw PassageAppError.convert(error: error) + } + } + +} diff --git a/Sources/Passage/typealiases/PassageAppInfo.swift b/Sources/Passage/typealiases/PassageAppInfo.swift new file mode 100644 index 0000000..9a97f2b --- /dev/null +++ b/Sources/Passage/typealiases/PassageAppInfo.swift @@ -0,0 +1 @@ +public typealias PassageAppInfo = App diff --git a/Sources/Passage/typealiases/PublicUserInfo.swift b/Sources/Passage/typealiases/PublicUserInfo.swift new file mode 100644 index 0000000..1d09426 --- /dev/null +++ b/Sources/Passage/typealiases/PublicUserInfo.swift @@ -0,0 +1 @@ +public typealias PublicUserInfo = User