diff --git a/.github/workflows/swift-package.yml b/.github/workflows/swift-package.yml index d3fe39c..7f493fb 100644 --- a/.github/workflows/swift-package.yml +++ b/.github/workflows/swift-package.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '16.4.0' + xcode-version: '26.0' - name: Build run: swift build -v - name: Run tests diff --git a/.github/workflows/swiftformat.yml b/.github/workflows/swiftformat.yml index ca22790..da2a2c2 100644 --- a/.github/workflows/swiftformat.yml +++ b/.github/workflows/swiftformat.yml @@ -11,7 +11,9 @@ jobs: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '16.4.0' + xcode-version: '26.0' + - name: Install SwiftFromat + run: brew install swiftformat - name: Check SwiftFromat - run: swift run swiftformat . --lint + run: swiftformat . --lint diff --git a/EndpointsTestbed/EndpointsTestbed.xcodeproj/project.pbxproj b/EndpointsTestbed/EndpointsTestbed.xcodeproj/project.pbxproj deleted file mode 100644 index 4a42133..0000000 --- a/EndpointsTestbed/EndpointsTestbed.xcodeproj/project.pbxproj +++ /dev/null @@ -1,579 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 77; - objects = { - -/* Begin PBXBuildFile section */ - 8211A84D2AFBDBFD00A36244 /* AsyncReactor in Frameworks */ = {isa = PBXBuildFile; productRef = 8211A84C2AFBDBFD00A36244 /* AsyncReactor */; }; - 8211A84F2AFBDC1200A36244 /* ExampleReactorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8211A84E2AFBDC1200A36244 /* ExampleReactorView.swift */; }; - 8211A8512AFBDC2000A36244 /* ExampleReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8211A8502AFBDC2000A36244 /* ExampleReactor.swift */; }; - 82B1AB9E29E3544B007B23C1 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B1AB9D29E3544B007B23C1 /* ExampleView.swift */; }; - 82B1ABA029E35455007B23C1 /* ExampleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B1AB9F29E35455007B23C1 /* ExampleViewModel.swift */; }; - 82B1ABA229E35486007B23C1 /* ExampleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B1ABA129E35486007B23C1 /* ExampleModel.swift */; }; - 82B1ABA529E354C0007B23C1 /* PostmanEchoClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B1ABA429E354C0007B23C1 /* PostmanEchoClient.swift */; }; - 82BA52F129DEE35C00F7726A /* EndpointsTestbedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82BA52F029DEE35C00F7726A /* EndpointsTestbedApp.swift */; }; - 82BA52F529DEE35D00F7726A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 82BA52F429DEE35D00F7726A /* Assets.xcassets */; }; - 82BA52F829DEE35D00F7726A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 82BA52F729DEE35D00F7726A /* Preview Assets.xcassets */; }; - 82BA530229DEE35D00F7726A /* EndpointsTestbedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82BA530129DEE35D00F7726A /* EndpointsTestbedTests.swift */; }; - 82BA531E29DEE73D00F7726A /* Endpoints in Frameworks */ = {isa = PBXBuildFile; productRef = 82BA531D29DEE73D00F7726A /* Endpoints */; }; - 82E74FFA2AADDF68001A230C /* World.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E74FF92AADDF68001A230C /* World.swift */; }; - 82E74FFC2AADE042001A230C /* HTTPBinClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E74FFB2AADE042001A230C /* HTTPBinClient.swift */; }; - 82E74FFE2AADE19A001A230C /* ManipulatedHTTPBinClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E74FFD2AADE19A001A230C /* ManipulatedHTTPBinClient.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 82BA52FE29DEE35D00F7726A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 82BA52E529DEE35C00F7726A /* Project object */; - proxyType = 1; - remoteGlobalIDString = 82BA52EC29DEE35C00F7726A; - remoteInfo = EndpointsTestbed; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 8211A84E2AFBDC1200A36244 /* ExampleReactorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleReactorView.swift; sourceTree = ""; }; - 8211A8502AFBDC2000A36244 /* ExampleReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleReactor.swift; sourceTree = ""; }; - 82B1AB9D29E3544B007B23C1 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; - 82B1AB9F29E35455007B23C1 /* ExampleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleViewModel.swift; sourceTree = ""; }; - 82B1ABA129E35486007B23C1 /* ExampleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleModel.swift; sourceTree = ""; }; - 82B1ABA429E354C0007B23C1 /* PostmanEchoClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostmanEchoClient.swift; sourceTree = ""; }; - 82BA52ED29DEE35C00F7726A /* EndpointsTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EndpointsTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 82BA52F029DEE35C00F7726A /* EndpointsTestbedApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndpointsTestbedApp.swift; sourceTree = ""; }; - 82BA52F429DEE35D00F7726A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 82BA52F729DEE35D00F7726A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 82BA52FD29DEE35D00F7726A /* EndpointsTestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EndpointsTestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 82BA530129DEE35D00F7726A /* EndpointsTestbedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndpointsTestbedTests.swift; sourceTree = ""; }; - 82BA531B29DEE38000F7726A /* Endpoints */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Endpoints; path = ..; sourceTree = ""; }; - 82E74FF92AADDF68001A230C /* World.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = World.swift; sourceTree = ""; }; - 82E74FFB2AADE042001A230C /* HTTPBinClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPBinClient.swift; sourceTree = ""; }; - 82E74FFD2AADE19A001A230C /* ManipulatedHTTPBinClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManipulatedHTTPBinClient.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 82BA52EA29DEE35C00F7726A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 8211A84D2AFBDBFD00A36244 /* AsyncReactor in Frameworks */, - 82BA531E29DEE73D00F7726A /* Endpoints in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 82BA52FA29DEE35D00F7726A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 8211A84A2AFBDBD200A36244 /* ExampleAsyncReactor */ = { - isa = PBXGroup; - children = ( - 8211A84E2AFBDC1200A36244 /* ExampleReactorView.swift */, - 8211A8502AFBDC2000A36244 /* ExampleReactor.swift */, - ); - path = ExampleAsyncReactor; - sourceTree = ""; - }; - 82B1AB9C29E3543C007B23C1 /* ExampleMVVM */ = { - isa = PBXGroup; - children = ( - 82B1AB9D29E3544B007B23C1 /* ExampleView.swift */, - 82B1AB9F29E35455007B23C1 /* ExampleViewModel.swift */, - 82B1ABA129E35486007B23C1 /* ExampleModel.swift */, - ); - path = ExampleMVVM; - sourceTree = ""; - }; - 82B1ABA329E354B0007B23C1 /* Networking */ = { - isa = PBXGroup; - children = ( - 82B1ABA429E354C0007B23C1 /* PostmanEchoClient.swift */, - 82E74FFB2AADE042001A230C /* HTTPBinClient.swift */, - 82E74FFD2AADE19A001A230C /* ManipulatedHTTPBinClient.swift */, - ); - path = Networking; - sourceTree = ""; - }; - 82BA52E429DEE35C00F7726A = { - isa = PBXGroup; - children = ( - 82BA531A29DEE38000F7726A /* Packages */, - 82BA52EF29DEE35C00F7726A /* EndpointsTestbed */, - 82BA530029DEE35D00F7726A /* EndpointsTestbedTests */, - 82BA52EE29DEE35C00F7726A /* Products */, - 82BA531C29DEE73D00F7726A /* Frameworks */, - ); - sourceTree = ""; - }; - 82BA52EE29DEE35C00F7726A /* Products */ = { - isa = PBXGroup; - children = ( - 82BA52ED29DEE35C00F7726A /* EndpointsTestbed.app */, - 82BA52FD29DEE35D00F7726A /* EndpointsTestbedTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 82BA52EF29DEE35C00F7726A /* EndpointsTestbed */ = { - isa = PBXGroup; - children = ( - 82B1ABA329E354B0007B23C1 /* Networking */, - 8211A84A2AFBDBD200A36244 /* ExampleAsyncReactor */, - 82B1AB9C29E3543C007B23C1 /* ExampleMVVM */, - 82BA52F029DEE35C00F7726A /* EndpointsTestbedApp.swift */, - 82E74FF92AADDF68001A230C /* World.swift */, - 82BA52F429DEE35D00F7726A /* Assets.xcassets */, - 82BA52F629DEE35D00F7726A /* Preview Content */, - ); - path = EndpointsTestbed; - sourceTree = ""; - }; - 82BA52F629DEE35D00F7726A /* Preview Content */ = { - isa = PBXGroup; - children = ( - 82BA52F729DEE35D00F7726A /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 82BA530029DEE35D00F7726A /* EndpointsTestbedTests */ = { - isa = PBXGroup; - children = ( - 82BA530129DEE35D00F7726A /* EndpointsTestbedTests.swift */, - ); - path = EndpointsTestbedTests; - sourceTree = ""; - }; - 82BA531A29DEE38000F7726A /* Packages */ = { - isa = PBXGroup; - children = ( - 82BA531B29DEE38000F7726A /* Endpoints */, - ); - name = Packages; - sourceTree = ""; - }; - 82BA531C29DEE73D00F7726A /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 82BA52EC29DEE35C00F7726A /* EndpointsTestbed */ = { - isa = PBXNativeTarget; - buildConfigurationList = 82BA531129DEE35D00F7726A /* Build configuration list for PBXNativeTarget "EndpointsTestbed" */; - buildPhases = ( - 82BA52E929DEE35C00F7726A /* Sources */, - 82BA52EA29DEE35C00F7726A /* Frameworks */, - 82BA52EB29DEE35C00F7726A /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = EndpointsTestbed; - packageProductDependencies = ( - 82BA531D29DEE73D00F7726A /* Endpoints */, - 8211A84C2AFBDBFD00A36244 /* AsyncReactor */, - ); - productName = EndpointsTestbed; - productReference = 82BA52ED29DEE35C00F7726A /* EndpointsTestbed.app */; - productType = "com.apple.product-type.application"; - }; - 82BA52FC29DEE35D00F7726A /* EndpointsTestbedTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 82BA531429DEE35D00F7726A /* Build configuration list for PBXNativeTarget "EndpointsTestbedTests" */; - buildPhases = ( - 82BA52F929DEE35D00F7726A /* Sources */, - 82BA52FA29DEE35D00F7726A /* Frameworks */, - 82BA52FB29DEE35D00F7726A /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 82BA52FF29DEE35D00F7726A /* PBXTargetDependency */, - ); - name = EndpointsTestbedTests; - productName = EndpointsTestbedTests; - productReference = 82BA52FD29DEE35D00F7726A /* EndpointsTestbedTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 82BA52E529DEE35C00F7726A /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1500; - TargetAttributes = { - 82BA52EC29DEE35C00F7726A = { - CreatedOnToolsVersion = 14.3; - }; - 82BA52FC29DEE35D00F7726A = { - CreatedOnToolsVersion = 14.3; - TestTargetID = 82BA52EC29DEE35C00F7726A; - }; - }; - }; - buildConfigurationList = 82BA52E829DEE35C00F7726A /* Build configuration list for PBXProject "EndpointsTestbed" */; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 82BA52E429DEE35C00F7726A; - packageReferences = ( - 8211A84B2AFBDBFD00A36244 /* XCRemoteSwiftPackageReference "AsyncReactor" */, - ); - preferredProjectObjectVersion = 77; - productRefGroup = 82BA52EE29DEE35C00F7726A /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 82BA52EC29DEE35C00F7726A /* EndpointsTestbed */, - 82BA52FC29DEE35D00F7726A /* EndpointsTestbedTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 82BA52EB29DEE35C00F7726A /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 82BA52F829DEE35D00F7726A /* Preview Assets.xcassets in Resources */, - 82BA52F529DEE35D00F7726A /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 82BA52FB29DEE35D00F7726A /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 82BA52E929DEE35C00F7726A /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 82B1ABA529E354C0007B23C1 /* PostmanEchoClient.swift in Sources */, - 82B1ABA029E35455007B23C1 /* ExampleViewModel.swift in Sources */, - 82E74FFC2AADE042001A230C /* HTTPBinClient.swift in Sources */, - 82B1ABA229E35486007B23C1 /* ExampleModel.swift in Sources */, - 8211A84F2AFBDC1200A36244 /* ExampleReactorView.swift in Sources */, - 82BA52F129DEE35C00F7726A /* EndpointsTestbedApp.swift in Sources */, - 82E74FFE2AADE19A001A230C /* ManipulatedHTTPBinClient.swift in Sources */, - 82E74FFA2AADDF68001A230C /* World.swift in Sources */, - 82B1AB9E29E3544B007B23C1 /* ExampleView.swift in Sources */, - 8211A8512AFBDC2000A36244 /* ExampleReactor.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 82BA52F929DEE35D00F7726A /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 82BA530229DEE35D00F7726A /* EndpointsTestbedTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 82BA52FF29DEE35D00F7726A /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 82BA52EC29DEE35C00F7726A /* EndpointsTestbed */; - targetProxy = 82BA52FE29DEE35D00F7726A /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - 82BA530F29DEE35D00F7726A /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 28TM58T3GZ; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 82BA531029DEE35D00F7726A /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 28TM58T3GZ; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 82BA531229DEE35D00F7726A /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"EndpointsTestbed/Preview Content\""; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.EndpointsTestbed; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 82BA531329DEE35D00F7726A /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"EndpointsTestbed/Preview Content\""; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.EndpointsTestbed; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 82BA531529DEE35D00F7726A /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.EndpointsTestbedTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/EndpointsTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/EndpointsTestbed"; - }; - name = Debug; - }; - 82BA531629DEE35D00F7726A /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.EndpointsTestbedTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/EndpointsTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/EndpointsTestbed"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 82BA52E829DEE35C00F7726A /* Build configuration list for PBXProject "EndpointsTestbed" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 82BA530F29DEE35D00F7726A /* Debug */, - 82BA531029DEE35D00F7726A /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 82BA531129DEE35D00F7726A /* Build configuration list for PBXNativeTarget "EndpointsTestbed" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 82BA531229DEE35D00F7726A /* Debug */, - 82BA531329DEE35D00F7726A /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 82BA531429DEE35D00F7726A /* Build configuration list for PBXNativeTarget "EndpointsTestbedTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 82BA531529DEE35D00F7726A /* Debug */, - 82BA531629DEE35D00F7726A /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 8211A84B2AFBDBFD00A36244 /* XCRemoteSwiftPackageReference "AsyncReactor" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/diamirio/AsyncReactor"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 8211A84C2AFBDBFD00A36244 /* AsyncReactor */ = { - isa = XCSwiftPackageProductDependency; - package = 8211A84B2AFBDBFD00A36244 /* XCRemoteSwiftPackageReference "AsyncReactor" */; - productName = AsyncReactor; - }; - 82BA531D29DEE73D00F7726A /* Endpoints */ = { - isa = XCSwiftPackageProductDependency; - productName = Endpoints; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 82BA52E529DEE35C00F7726A /* Project object */; -} diff --git a/EndpointsTestbed/EndpointsTestbed.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/EndpointsTestbed/EndpointsTestbed.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/EndpointsTestbed/EndpointsTestbed.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/EndpointsTestbed/EndpointsTestbed.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/EndpointsTestbed/EndpointsTestbed.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/EndpointsTestbed/EndpointsTestbed.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/EndpointsTestbed/EndpointsTestbed.xcodeproj/xcshareddata/xcschemes/EndpointsTestbed.xcscheme b/EndpointsTestbed/EndpointsTestbed.xcodeproj/xcshareddata/xcschemes/EndpointsTestbed.xcscheme deleted file mode 100644 index 95a0aef..0000000 --- a/EndpointsTestbed/EndpointsTestbed.xcodeproj/xcshareddata/xcschemes/EndpointsTestbed.xcscheme +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/EndpointsTestbed/EndpointsTestbed/Assets.xcassets/AccentColor.colorset/Contents.json b/EndpointsTestbed/EndpointsTestbed/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/EndpointsTestbed/EndpointsTestbed/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json b/EndpointsTestbed/EndpointsTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3..0000000 --- a/EndpointsTestbed/EndpointsTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/Assets.xcassets/Contents.json b/EndpointsTestbed/EndpointsTestbed/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/EndpointsTestbed/EndpointsTestbed/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/EndpointsTestbedApp.swift b/EndpointsTestbed/EndpointsTestbed/EndpointsTestbedApp.swift deleted file mode 100644 index 893c819..0000000 --- a/EndpointsTestbed/EndpointsTestbed/EndpointsTestbedApp.swift +++ /dev/null @@ -1,20 +0,0 @@ -import AsyncReactor -import SwiftUI - -@main -struct EndpointsTestbedApp: App { - var body: some Scene { - WindowGroup { - NavigationStack { - List { - Section { - NavigationLink("MVVM", destination: ExampleView()) - NavigationLink("AsyncReactor", destination: ReactorView(ExampleReactor()) { - ExampleReactorView() - }) - } - } - } - } - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/ExampleAsyncReactor/ExampleReactor.swift b/EndpointsTestbed/EndpointsTestbed/ExampleAsyncReactor/ExampleReactor.swift deleted file mode 100644 index 1205cd7..0000000 --- a/EndpointsTestbed/EndpointsTestbed/ExampleAsyncReactor/ExampleReactor.swift +++ /dev/null @@ -1,40 +0,0 @@ -import AsyncReactor -import Endpoints -import Foundation - -class ExampleReactor: AsyncReactor { - enum Action { - case executeRequests - } - - struct State { - var text = "" - } - - @Published - private(set) var state = State() - - func action(_ action: Action) async { - switch action { - case .executeRequests: - await executeRequest() - } - } - - private func executeRequest() async { - do { - let (body, response) = try await world.postmanSession.dataTask( - for: PostmanEchoClient.ExampleGetCall() - ) - - guard response.statusCode == 200 else { return } - - await MainActor.run { - state.text = body.url - } - } catch { - guard let error = error as? EndpointsError else { return } - print(error.response?.statusCode ?? "") - } - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/ExampleAsyncReactor/ExampleReactorView.swift b/EndpointsTestbed/EndpointsTestbed/ExampleAsyncReactor/ExampleReactorView.swift deleted file mode 100644 index 79be285..0000000 --- a/EndpointsTestbed/EndpointsTestbed/ExampleAsyncReactor/ExampleReactorView.swift +++ /dev/null @@ -1,26 +0,0 @@ -import AsyncReactor -import SwiftUI - -struct ExampleReactorView: View { - @EnvironmentObject var reactor: ExampleReactor - - var body: some View { - VStack { - if reactor.state.text.isEmpty { - ProgressView() - } else { - Text(reactor.state.text) - .font(.headline) - } - } - .onAppear { - reactor.send(.executeRequests) - } - } -} - -#Preview { - ReactorView(ExampleReactor()) { - ExampleReactorView() - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/ExampleMVVM/ExampleModel.swift b/EndpointsTestbed/EndpointsTestbed/ExampleMVVM/ExampleModel.swift deleted file mode 100644 index 701fa8c..0000000 --- a/EndpointsTestbed/EndpointsTestbed/ExampleMVVM/ExampleModel.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -struct ExampleModel: Codable { - var url: String -} diff --git a/EndpointsTestbed/EndpointsTestbed/ExampleMVVM/ExampleView.swift b/EndpointsTestbed/EndpointsTestbed/ExampleMVVM/ExampleView.swift deleted file mode 100644 index 80186fc..0000000 --- a/EndpointsTestbed/EndpointsTestbed/ExampleMVVM/ExampleView.swift +++ /dev/null @@ -1,25 +0,0 @@ -import SwiftUI - -struct ExampleView: View { - @StateObject var viewModel = ExampleViewModel() - - var body: some View { - VStack { - if viewModel.text.isEmpty { - ProgressView() - } else { - Text(viewModel.text) - .font(.headline) - } - } - .onAppear { - viewModel.executeRequests() - } - } -} - -struct ExampleView_Previews: PreviewProvider { - static var previews: some View { - ExampleView() - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/ExampleMVVM/ExampleViewModel.swift b/EndpointsTestbed/EndpointsTestbed/ExampleMVVM/ExampleViewModel.swift deleted file mode 100644 index ab9cda8..0000000 --- a/EndpointsTestbed/EndpointsTestbed/ExampleMVVM/ExampleViewModel.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Endpoints -import Foundation - -@MainActor -class ExampleViewModel: ObservableObject { - @Published - var text: String = "" - - func executeRequests() { - Task { - let (body, response) = try await world.postmanSession.dataTask( - for: PostmanEchoClient.ExampleGetCall() - ) - guard response.statusCode == 200 else { return } - - await MainActor.run { - self.text = body.url - } - } - - Task { - let (_, response) = try await world.manipulatedHttpBinSession.dataTask( - for: ManipulatedHTTPBinClient.GetStatusCode(deliveredStatusCode: 220) - ) - guard response.statusCode == 200 else { return } - print("Success") - } - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/Networking/HTTPBinClient.swift b/EndpointsTestbed/EndpointsTestbed/Networking/HTTPBinClient.swift deleted file mode 100644 index 0cee9e9..0000000 --- a/EndpointsTestbed/EndpointsTestbed/Networking/HTTPBinClient.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Endpoints -import Foundation - -class HTTPBinClient: AnyClient { - public init() { - let url = URL(string: "https://httpbin.org/")! - super.init(baseURL: url) - } - - struct GetStatusCode: Call { - let deliveredStatusCode: Int - - typealias Parser = JSONParser - - var request: URLRequestEncodable { - Request(.get, "/status/\(deliveredStatusCode)") - } - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/Networking/ManipulatedHTTPBinClient.swift b/EndpointsTestbed/EndpointsTestbed/Networking/ManipulatedHTTPBinClient.swift deleted file mode 100644 index ff6857f..0000000 --- a/EndpointsTestbed/EndpointsTestbed/Networking/ManipulatedHTTPBinClient.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Endpoints -import Foundation - -class ManipulatedHTTPBinClient: AnyClient { - private var defaultClient: AnyClient - - init() { - let url = URL(string: "https://httpbin.org/")! - self.defaultClient = AnyClient(baseURL: url) - super.init(baseURL: url) - } - - override func encode(call: some Endpoints.Call) async throws -> URLRequest { - // Custom manipulation i.e. OAuth implementation - print("- MANIPULATED encode -") - return try await defaultClient.encode(call: call) - } - - override func parse(response: HTTPURLResponse?, data: Data?, for call: C) async throws -> C.Parser.OutputType - where C: Call { - // Custom manipulation i.e. react on error responses or invalid tokens - print("- MANIPULATED parse -") - return try await defaultClient.parse(response: response, data: data, for: call) - } - - override func validate(response: HTTPURLResponse?, data: Data?) async throws { - // Custom validation if needed - print("- MANIPULATED validate -") - } - - struct GetStatusCode: Call { - typealias Parser = JSONParser - - let deliveredStatusCode: Int - - var request: URLRequestEncodable { - Request(.get, "/status/\(deliveredStatusCode)") - } - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/Networking/PostmanEchoClient.swift b/EndpointsTestbed/EndpointsTestbed/Networking/PostmanEchoClient.swift deleted file mode 100644 index a79137c..0000000 --- a/EndpointsTestbed/EndpointsTestbed/Networking/PostmanEchoClient.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Endpoints -import Foundation - -public class PostmanEchoClient: AnyClient { - public init() { - let url = URL(string: "https://postman-echo.com")! - super.init(baseURL: url) - } - - struct ExampleGetCall: Call { - typealias Parser = JSONParser - - var request: URLRequestEncodable { - Request(.get, "/get") - } - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/Preview Content/Preview Assets.xcassets/Contents.json b/EndpointsTestbed/EndpointsTestbed/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/EndpointsTestbed/EndpointsTestbed/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/EndpointsTestbed/EndpointsTestbed/World.swift b/EndpointsTestbed/EndpointsTestbed/World.swift deleted file mode 100644 index 14004c0..0000000 --- a/EndpointsTestbed/EndpointsTestbed/World.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Endpoints -import Foundation - -@MainActor -let world = World() - -struct World { - let postmanSession: Session - let httpBinSession: Session - let manipulatedHttpBinSession: Session - - init() { - let postmanClient = PostmanEchoClient() - self.postmanSession = Session(with: postmanClient) - postmanSession.debug = true - - let httpBinClient = HTTPBinClient() - self.httpBinSession = Session(with: httpBinClient) - httpBinSession.debug = true - - let manipulatedHttpBinClient = ManipulatedHTTPBinClient() - self.manipulatedHttpBinSession = Session(with: manipulatedHttpBinClient) - manipulatedHttpBinSession.debug = true - } -} diff --git a/EndpointsTestbed/EndpointsTestbedTests/EndpointsTestbedTests.swift b/EndpointsTestbed/EndpointsTestbedTests/EndpointsTestbedTests.swift deleted file mode 100644 index 6fd10fd..0000000 --- a/EndpointsTestbed/EndpointsTestbedTests/EndpointsTestbedTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// EndpointsTestbedTests.swift -// EndpointsTestbedTests -// -// Created by Alexander Kauer on 06.04.23. -// - -@testable import EndpointsTestbed -import XCTest - -final class EndpointsTestbedTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions - // afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - measure { - // Put the code you want to measure the time of here. - } - } -} diff --git a/LICENSE b/LICENSE index 7ed0942..088b025 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2022 Tailored Media GmbH +Copyright (c) 2025 Tailored Media GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Migration/V4_0_0.md b/Migration/V4_0_0.md new file mode 100644 index 0000000..0c7afe7 --- /dev/null +++ b/Migration/V4_0_0.md @@ -0,0 +1,250 @@ +# v4.0.0 - Changelog & Migration Guide + +Endpoints 4.0.0 brings full Swift 6.2+ support with strict concurrency compliance, requiring `Sendable` conformance throughout the library and introducing the `Session` actor for thread-safe networking. + +## Changelog + +### Developer Facing + +- **Swift 6.2+ Strict Concurrency** + - All core protocols now require `Sendable` conformance + - `Session` is now an actor, ensuring thread-safe access to URLSession + - All `Call`, `Client`, and `ResponseParser` types must be `Sendable` + - `ResponseParser.OutputType` must be `Sendable` for safe concurrent access + +- **AnyClient Renamed to DefaultClient** + - The `AnyClient` class has been renamed to `DefaultClient` and changed from a class to a struct + - `DefaultClient` is now a value type (struct) conforming to `Sendable` + - The `open class` pattern for subclassing is no longer supported + +- **Client Protocol Changes** + - Client protocol now requires `Sendable` conformance + - Removed default protocol extensions that previously allowed delegation to an internal `client` property + - Custom clients must now implement all methods directly (typically by delegating to a `DefaultClient` instance) + - Changed from `func encode(call: C)` to `func encode(call: some Call)` for improved ergonomics + +- **Parameter Naming Updates** + - `DefaultClient` initializer: `baseURL` parameter renamed to `url` + - Example: `DefaultClient(url: myURL)` instead of `DefaultClient(baseURL: myURL)` + +- **JSONParser Improvements** + - Default `JSONParser` now includes standard configuration: + - `dateDecodingStrategy = .iso8601` + - `keyDecodingStrategy = .convertFromSnakeCase` + - Custom decoder configuration can still be achieved by creating a custom parser + +- **Session Changes** + - `Session` is now an actor for thread-safe networking + - All `Session` methods must be called with `await` from outside the actor context + - `debug` parameter added to Session initializer for request/response logging + +### Internal + +- Updated minimum Swift version requirement to 6.2+ +- Updated CI workflows to use Xcode 26.0 and macOS 15 +- Applied SwiftFormat across the codebase +- Moved example project to separate repository (Endpoints-Example) + +## Migration Guide + +### AnyClient → DefaultClient + +**Before (3.x):** +```swift +let client = AnyClient(baseURL: URL(string: "https://api.example.com")!) +``` + +**After (4.x):** +```swift +let client = DefaultClient(url: URL(string: "https://api.example.com")!) +``` + +### Custom Client Implementation + +In 3.x, you could subclass `AnyClient` using the `open class` pattern. In 4.x, you must use composition with a struct. + +**Before (3.x):** +```swift +class MyAPIClient: AnyClient { + var apiKey = "secret" + + override func encode(call: C) async throws -> URLRequest { + var request = try await super.encode(call: call) + request.addValue(apiKey, forHTTPHeaderField: "API-Key") + return request + } +} +``` + +**After (4.x):** +```swift +struct MyAPIClient: Client { + private let client: Client + let apiKey = "secret" + + init() { + let url = URL(string: "https://api.example.com")! + self.client = DefaultClient(url: url) + } + + func encode(call: some Call) async throws -> URLRequest { + var request = try await client.encode(call: call) + request.addValue(apiKey, forHTTPHeaderField: "API-Key") + return request + } + + func parse(response: HTTPURLResponse?, data: Data?, for call: C) async throws -> C.Parser.OutputType + where C: Call { + try await client.parse(response: response, data: data, for: call) + } + + func validate(response: HTTPURLResponse?, data: Data?) async throws { + try await client.validate(response: response, data: data) + } +} +``` + +### Session is Now an Actor + +**Before (3.x):** +```swift +let session = Session(with: client) +let (body, response) = try await session.dataTask(for: call) +``` + +**After (4.x):** +```swift +// Session is now an actor - same usage pattern but with actor isolation +let session = Session(with: client) +let (body, response) = try await session.dataTask(for: call) + +// If you need debug logging: +let session = Session(with: client, debug: true) +``` + +The syntax remains the same, but `Session` is now actor-isolated. This means: +- All access is automatically serialized and thread-safe +- You must use `await` when calling session methods from outside the actor +- You cannot directly access session properties without `await` + +### Sendable Conformance for Custom Types + +All custom `Call`, `Client`, and response types must now be `Sendable`. + +**Before (3.x):** +```swift +struct GetUser: Call { + typealias Parser = JSONParser + let userId: String + + var request: URLRequestEncodable { + Request(.get, "users/\(userId)") + } +} + +struct User: Codable { + let id: String + let name: String +} +``` + +**After (4.x):** +```swift +// Call types must be Sendable (structs with Sendable properties are automatically Sendable) +struct GetUser: Call { + typealias Parser = JSONParser + let userId: String // String is Sendable + + var request: URLRequestEncodable { + Request(.get, "users/\(userId)") + } +} + +// Response types must be Sendable +struct User: Codable, Sendable { // Add explicit Sendable conformance + let id: String + let name: String +} +``` + +**Important:** If your custom types contain non-Sendable properties (like closures, class instances, or other reference types), you'll need to refactor them to use value types or actor-isolated references. + +### Custom ResponseParser + +**Before (3.x):** +```swift +struct CustomParser: ResponseParser { + typealias OutputType = T + + func parse(data: Data, encoding: String.Encoding) throws -> T { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return try decoder.decode(T.self, from: data) + } +} +``` + +**After (4.x):** +```swift +struct CustomParser: ResponseParser { // T must be Sendable + typealias OutputType = T // OutputType is automatically Sendable + + func parse(data: Data, encoding: String.Encoding) throws -> T { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return try decoder.decode(T.self, from: data) + } +} +``` + +### AnyCall ValidationBlock + +The `ValidationBlock` closure type is now marked as `@Sendable`. + +**Before (3.x):** +```swift +public typealias ValidationBlock = (HTTPURLResponse?, Data?) throws -> Void +``` + +**After (4.x):** +```swift +public typealias ValidationBlock = @Sendable (HTTPURLResponse?, Data?) throws -> Void +``` + +This means any closures passed to `AnyCall` for validation must be `@Sendable`, which requires them to only capture `Sendable` values. + +## Common Migration Issues + +### Issue: "Type 'MyClient' does not conform to protocol 'Sendable'" + +**Solution:** Ensure your client is a struct (value type) and all its stored properties are `Sendable`. If you have reference types, consider using actors or refactoring to value types. + +### Issue: "Stored property 'client' of 'Sendable'-conforming struct has non-sendable type" + +**Solution:** Make sure any stored clients are themselves `Sendable`. `DefaultClient` is `Sendable`, so storing it works. If you're storing a custom client, ensure it also conforms to `Sendable`. + +### Issue: "Cannot pass argument of non-sendable type 'MyModel' to parameter of type 'some Sendable'" + +**Solution:** Add `Sendable` conformance to your model types: +```swift +struct MyModel: Codable, Sendable { + // ... +} +``` + +### Issue: "Expression is 'async' but is not marked with 'await'" + +**Solution:** Since `Session` is now an actor, all calls to its methods require `await`: +```swift +let (body, response) = try await session.dataTask(for: call) +``` + +## Benefits of 4.x + +- **Thread Safety:** The actor-based `Session` ensures safe concurrent access to networking +- **Data Race Prevention:** `Sendable` conformance prevents data races at compile time +- **Swift 6 Future-Proofing:** Full compatibility with Swift's modern concurrency model +- **Better Composition:** Struct-based clients encourage better composition patterns over inheritance +- **Type Safety:** Improved type inference with `some Call` parameter + +For more examples, see the [Endpoints-Example](https://github.com/diamirio/Endpoints-Example) repository. diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index cc7023d..0000000 --- a/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "6be90f4e656cb9ee68672462c82ac186ebd3d650a42c62ae1c35723995bd7c2a", - "pins" : [ - { - "identity" : "swiftformat", - "kind" : "remoteSourceControl", - "location" : "https://github.com/nicklockwood/SwiftFormat", - "state" : { - "revision" : "ad7707bd34a33fa64a2c593c53deaa7d7469e2f0", - "version" : "0.52.11" - } - } - ], - "version" : 3 -} diff --git a/Package.swift b/Package.swift index 4e2d7d3..b9367d7 100755 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.1 +// swift-tools-version:6.2 import PackageDescription @@ -17,9 +17,7 @@ let package = Package( targets: ["Endpoints"] ) ], - dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.4") - ], + dependencies: [], targets: [ .target( name: "Endpoints", diff --git a/README.md b/README.md index c947f1c..38ab8bc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,33 @@ Endpoints makes it easy to write a type-safe network abstraction layer for any Web-API. -It requires Swift 5, makes heavy use of generics (and generalized existentials) and protocols (and protocol extensions). It also encourages a clean separation of concerns and the use of value types (i.e. structs). +It requires Swift 6.2+, makes heavy use of generics and protocols (with protocol extensions). It also encourages a clean separation of concerns and the use of value types (i.e. structs). Built for modern Swift concurrency with async/await and actor support. + +**Key Features:** +- **Type-safe API**: Strongly typed requests and responses +- **Swift 6.2+**: Full support for Swift's strict concurrency model +- **Actor-based Session**: Thread-safe networking with `Session` as an actor +- **Sendable conformance**: All core protocols require `Sendable` conformance for safe concurrent access +- **Async/await**: Native async/await support throughout the API +- **Flexible parsing**: Multiple built-in response parsers with support for custom parsers +- **JSON Codable**: First-class support for `Codable` types + +## Requirements + +* Swift 6.2+ +* iOS 13+ +* tvOS 12+ +* macOS 10.15+ +* watchOS 6+ +* visionOS 1+ + +## Installation + +**Swift Package Manager:** + +```swift +.package(url: "https://github.com/diamirio/Endpoints.git", .upToNextMajor(from: "4.0.0")) +``` ## Usage @@ -19,18 +45,16 @@ Here's how to load a random image from Giphy. ```swift // A client is responsible for encoding and parsing all calls for a given Web-API. -let client = AnyClient(baseURL: URL(string: "https://api.giphy.com/v1/")!) +let client = DefaultClient(url: URL(string: "https://api.giphy.com/v1/")!) // A call encapsulates the request that is sent to the server and the type that is expected in the response. let call = AnyCall(Request(.get, "gifs/random", query: ["tag": "cat", "api_key": "dc6zaTOxFJmzC"])) -// A session wraps `URLSession` and allows you to start the request for the call and get the parsed response object (or an error) in a completion block. +// A session is an actor that wraps `URLSession` and allows you to start the request for the call and get the parsed response object (or an error). +// Session is an actor, ensuring thread-safe access to URLSession. let session = Session(with: client) -// enable debug-mode to log network traffic -session.debug = true - -// start call +// Start call - returns the parsed body and HTTPURLResponse let (body, httpResponse) = try await session.dataTask(for: call) ``` @@ -72,33 +96,51 @@ Look up the documentation in the code for further explanations of the types. #### JSON Codable Integration -`Endpoints` has a built in JSON Codable support. +`Endpoints` has built-in JSON Codable support. ##### Decoding -The `ResponseParser` responsible for handling decodable types is the `JSONParser`. -The `JSONParser` uses the default `JSONDecoder()`, however, the `JSONParser` can be subclassed, and the `jsonDecoder` can be overwritten with your configured `JSONDecoder`. +The `ResponseParser` responsible for handling decodable types is the `JSONParser`. + +The default `JSONParser` comes pre-configured with: +- `dateDecodingStrategy = .iso8601` +- `keyDecodingStrategy = .convertFromSnakeCase` ```swift -// Decode a type using the default decoder +// Decode a type using the default decoder (with iso8601 dates and snake_case conversion) struct GiphyCall: Call { typealias Parser = JSONParser - ... + + var request: URLRequestEncodable { + Request(.get, "gifs/random", query: ["tag": "cat"]) + } } -// custom decoder +// If you need different decoder settings, create a custom parser +// Note: T must be Sendable for Swift 6.2+ concurrency safety +struct CustomJSONParser: ResponseParser { + typealias OutputType = T + + let jsonDecoder: JSONDecoder -struct GiphyParser: JSONParser { - override public var jsonDecoder: JSONDecoder { + init() { let decoder = JSONDecoder() - // configure... - return decoder + decoder.dateDecodingStrategy = .secondsSince1970 + decoder.keyDecodingStrategy = .useDefaultKeys + self.jsonDecoder = decoder + } + + func parse(data: Data, encoding: String.Encoding) throws -> T { + try jsonDecoder.decode(T.self, from: data) } } struct GiphyCall: Call { - typealias Parser = GiphyParser - ... + typealias Parser = CustomJSONParser + + var request: URLRequestEncodable { + Request(.get, "gifs/random", query: ["tag": "cat"]) + } } ``` @@ -108,14 +150,16 @@ Every encodable is able to provide a `JSONEncoder()` to encode itself via the `t ### Dedicated Calls -`AnyCall` is the default implementation of the `Call` protocol, which you can use as-is. But if you want to make your networking layer really type-safe you'll want to create a dedicated `Call` type for each operation of your Web-API: +`AnyCall` is the default implementation of the `Call` protocol, which you can use as-is. But if you want to make your networking layer really type-safe you'll want to create a dedicated `Call` type for each operation of your Web-API. + +**Note:** All `Call` types must conform to `Sendable` for Swift 6.2+ concurrency safety. Use value types (structs) with sendable properties: ```swift struct GetRandomImage: Call { typealias Parser = DictionaryParser - + var tag: String - + var request: URLRequestEncodable { return Request(.get, "gifs/random", query: [ "tag": tag, "api_key": "dc6zaTOxFJmzC" ]) } @@ -129,31 +173,43 @@ let call = GetRandomImage(tag: "cat") A client is responsible for handling things that are common for all operations of a given Web-API. Typically this includes appending API tokens or authentication tokens to a request or validating responses and handling errors. -`AnyClient` is the default implementation of the `Client` protocol and can be used as-is or as a starting point for your own dedicated client. +`DefaultClient` is the default implementation of the `Client` protocol and can be used as-is or as a starting point for your own dedicated client. -You'll usually need to create your own dedicated client that either subclasses `AnyClient` or delegates the encoding of requests and parsing of responses to an `AnyClient` instance, as done here: +You'll usually need to create your own dedicated client that implements the `Client` protocol and delegates the encoding of requests and parsing of responses to a `DefaultClient` instance, as done here. + +**Note:** All `Client` types must conform to `Sendable`. Use structs with sendable properties to ensure thread-safety: ```swift -class GiphyClient: Client { - private let anyClient = AnyClient(baseURL: URL(string: "https://api.giphy.com/v1/")!) - - var apiKey = "dc6zaTOxFJmzC" - - override func encode(call: C) async throws -> URLRequest { - var request = anyClient.encode(call: call) - - // Append the API key to every request - request.append(query: ["api_key": apiKey]) - +struct GiphyClient: Client { + private let client: Client + let apiKey = "dc6zaTOxFJmzC" + + init() { + let url = URL(string: "https://api.giphy.com/v1/")! + self.client = DefaultClient(url: url) + } + + func encode(call: some Call) async throws -> URLRequest { + var request = try await client.encode(call: call) + + // Append the API key to every request's URL + if let url = request.url, + var components = URLComponents(url: url, resolvingAgainstBaseURL: true) { + var queryItems = components.queryItems ?? [] + queryItems.append(URLQueryItem(name: "api_key", value: apiKey)) + components.queryItems = queryItems + request.url = components.url + } + return request } - - override func parse(response: HTTPURLResponse?, data: Data?, for call: C) async throws -> C.Parser.OutputType + + func parse(response: HTTPURLResponse?, data: Data?, for call: C) async throws -> C.Parser.OutputType where C: Call { do { - // Use `AnyClient` to parse the response + // Use `DefaultClient` to parse the response // If this fails, try to read error details from response body - return try await anyClient.parse(sessionTaskResult: result, for: call) + return try await client.parse(response: response, data: data, for: call) } catch { // See if the backend sent detailed error information guard @@ -161,39 +217,51 @@ class GiphyClient: Client { let data, let errorDict = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any], let meta = errorDict?["meta"] as? [String: Any], - let errorCode = meta["error_code"] as? String + let errorCode = meta["error_code"] as? String else { // no error info from backend -> rethrow default error throw error } - + // Propagate error that contains errorCode as reason from backend throw StatusCodeError.unacceptable(code: response.statusCode, reason: errorCode) } } + + func validate(response: HTTPURLResponse?, data: Data?) async throws { + // Delegate to the default client's validation + try await client.validate(response: response, data: data) + } } ``` ### Dedicated Response Types -You usually want your networking layer to provide a dedicated response type for every supported call. In our example this could look like this: +You usually want your networking layer to provide a dedicated response type for every supported call. In our example this could look like this: + +**Note:** Response types must conform to `Sendable` for Swift 6.2+ concurrency safety: ```swift -struct RandomImage: Decodable { - struct Data: Decodable { +struct RandomImage: Decodable, Sendable { + struct Data: Decodable, Sendable { let url: URL - + private enum CodingKeys: String, CodingKey { case url = "image_url" } } - + let data: Data } struct GetRandomImage: Call { typealias Parser = JSONParser - ... + + var tag: String + + var request: URLRequestEncodable { + Request(.get, "gifs/random", query: ["tag": tag]) + } } ``` @@ -210,22 +278,120 @@ let (body, response) = try await session.dataTask(for: call) print("image url: \(body.data.url)") ``` -## Installation +## Example -**Swift Package Manager:** +Example implementation can be found [here](https://github.com/diamirio/Endpoints-Example). + +## Migration Guides + +If you're upgrading from a previous version, please refer to the migration guides: + +- [Migrating from 3.x to 4.x](Migration/V4_0_0.md) - Swift 6.2+ strict concurrency, `AnyClient` → `DefaultClient`, and more +- [Migrating from 2.x to 3.x](Migration/V3_0_0.md) - Native async/await APIs +- [Migrating from 1.x to 2.x](Migration/V2_0_0.md) -```bash -.package(url: "https://github.com/tailoredmedia/Endpoints.git", .upToNextMajor(from: "3.0.0")) +## Advanced Features + +### Debug Logging + +Enable debug logging to see detailed request and response information: + +```swift +let session = Session(with: client, debug: true) ``` -## Example +This will log: +- cURL representation of the request +- Response status and headers +- Response body data -Example implementation can be found [here](./EndpointsTestbed). +### Request Body Encoding -## Requirements +Endpoints supports multiple body encoding strategies: + +```swift +// JSON encoded body +let jsonBody = try JSONEncodedBody(encodable: myModel) +let request = Request(.post, "users", body: jsonBody) + +// Form-urlencoded body +let formBody = FormEncodedBody(parameters: ["username": "john", "password": "secret"]) +let request = Request(.post, "login", body: formBody) + +// Multipart form data (for file uploads) +let multipartBody = MultipartBody(parts: [ + MultipartBody.Part(name: "avatar", data: imageData, filename: "profile.jpg", mimeType: "image/jpeg"), + MultipartBody.Part(name: "name", data: "John Doe".data(using: .utf8)!) +]) +let request = Request(.post, "upload", body: multipartBody) +``` + +### Custom Validation + +Both `Call` and `Client` can implement custom validation logic: + +```swift +struct MyCall: Call { + typealias Parser = JSONParser + + var request: URLRequestEncodable { + Request(.get, "data") + } -* Swift 5 -* iOS 13 -* tvOS 12 -* macOS 10.15 -* watchOS 6 + // Custom validation for this specific call + func validate(response: HTTPURLResponse?, data: Data?) async throws { + guard let response = response else { return } + + // Require a specific header for this call + guard response.value(forHTTPHeaderField: "X-Custom-Header") != nil else { + throw MyError.missingHeader + } + } +} + +struct MyClient: Client { + private let client: Client + + init() { + self.client = DefaultClient(url: URL(string: "https://api.example.com")!) + } + + // ... encode and parse implementations ... + + // Custom validation for all calls using this client + func validate(response: HTTPURLResponse?, data: Data?) async throws { + // First, do the default validation + try await client.validate(response: response, data: data) + + // Then add custom validation + guard let response = response else { return } + + // Example: Check for maintenance mode + if response.statusCode == 503 { + throw MaintenanceError() + } + } +} +``` + +### Error Handling + +Endpoints wraps all errors in `EndpointsError`, which includes the `HTTPURLResponse` if available: + +```swift +do { + let (body, response) = try await session.dataTask(for: call) + // Handle success +} catch let error as EndpointsError { + // Access the underlying error + print("Error: \(error.error)") + + // Access the HTTP response if available + if let response = error.response { + print("Status code: \(response.statusCode)") + } +} catch { + // Handle other errors + print("Unexpected error: \(error)") +} +``` diff --git a/Sources/Async/Client.swift b/Sources/Async/Client.swift index e481079..8478a05 100644 --- a/Sources/Async/Client.swift +++ b/Sources/Async/Client.swift @@ -3,11 +3,11 @@ import Foundation /// A type responsible for encoding and parsing all calls for a given Web API. -/// A basic implementation is provided by `AnyClient`. -public protocol Client: ResponseValidator { +/// A basic implementation is provided by `DefaultClient`. +public protocol Client: ResponseValidator, Sendable { /// Converts a `Call` created for this client's Web API /// into a `URLRequest`. - func encode(call: C) async throws -> URLRequest + func encode(call: some Call) async throws -> URLRequest /// Converts the `URLSession`s result for a `Call` to /// this client's Web API into the expected output type. diff --git a/Sources/Async/AnyClient.swift b/Sources/Async/DefaultClient.swift similarity index 67% rename from Sources/Async/AnyClient.swift rename to Sources/Async/DefaultClient.swift index 4df663e..3a62d14 100644 --- a/Sources/Async/AnyClient.swift +++ b/Sources/Async/DefaultClient.swift @@ -2,31 +2,31 @@ import Foundation -open class AnyClient: Client { +public struct DefaultClient: Client { /// The base URL used by `encode` to convert `Call`s into `URLRequest`s. - public let baseURL: URL + public let url: URL /// Used by `validate` to check if the status code of a response is valid. public let statusCodeValidator = StatusCodeValidator() /// Creates a client with a base URL. - public init(baseURL: URL) { - self.baseURL = baseURL + public init(url: URL) { + self.url = url } - open func encode( + public func encode( call: some Call ) async throws -> URLRequest { var urlRequest = call.request.urlRequest - if let url = urlRequest.url, url.isRelative { - urlRequest.url = URL(string: url.relativeString, relativeTo: baseURL) + if let requestUrl = urlRequest.url, requestUrl.isRelative { + urlRequest.url = URL(string: requestUrl.relativeString, relativeTo: url) } return urlRequest } - open func parse( + public func parse( response: HTTPURLResponse?, data: Data?, for call: C @@ -38,10 +38,10 @@ open class AnyClient: Client { return try C.Parser().parse(response: response, data: data) } - open func validate( + public func validate( response: HTTPURLResponse?, data: Data? ) async throws { - try statusCodeValidator.validate(response: response, data: data) + try await statusCodeValidator.validate(response: response, data: data) } } diff --git a/Sources/Async/Session.swift b/Sources/Async/Session.swift index c80ad46..5ea7eec 100644 --- a/Sources/Async/Session.swift +++ b/Sources/Async/Session.swift @@ -5,11 +5,10 @@ import Foundation import OSLog #endif -open class Session { - public var debug = false - - public var urlSession: URLSession +public actor Session { public let client: CL + public let urlSession: URLSession + public let debug: Bool public init( with client: CL, @@ -17,14 +16,18 @@ open class Session { configuration: .default, delegate: URLSessionDelegateHandler(), delegateQueue: nil - ) + ), + debug: Bool = false ) { self.client = client self.urlSession = urlSession + self.debug = debug } @discardableResult - open func dataTask(for call: C) async throws -> (C.Parser.OutputType, HTTPURLResponse) { + public func dataTask( + for call: C + ) async throws -> (C.Parser.OutputType, HTTPURLResponse) { let urlRequest = try await client.encode(call: call) let (data, response) = try await urlSession.data(for: urlRequest) diff --git a/Sources/Body/Body.swift b/Sources/Body/Body.swift index 6706c64..0475d18 100644 --- a/Sources/Body/Body.swift +++ b/Sources/Body/Body.swift @@ -8,7 +8,7 @@ import Foundation /// /// Adopted by `Data` and `String`. /// - seealso: `FormEncodedBody`, `JSONEncodedBody`. -public protocol Body { +public protocol Body: Sendable { /// Returns HTTP Header parameters required for `self`, if any. /// /// This is usally a "Content-Type" header like "application/json" for a diff --git a/Sources/Body/Multipart/MultipartBodyPart.swift b/Sources/Body/Multipart/MultipartBodyPart.swift index f128bea..eb1cce0 100644 --- a/Sources/Body/Multipart/MultipartBodyPart.swift +++ b/Sources/Body/Multipart/MultipartBodyPart.swift @@ -2,7 +2,7 @@ import Foundation -public protocol MultipartBodyPart { +public protocol MultipartBodyPart: Sendable { /// The name (usually from the HTML form) /// /// There can be multiple parts with the same name. diff --git a/Sources/Convenience/AnyCall.swift b/Sources/Convenience/AnyCall.swift index eeeed8b..e827142 100644 --- a/Sources/Convenience/AnyCall.swift +++ b/Sources/Convenience/AnyCall.swift @@ -5,7 +5,7 @@ import Foundation public struct AnyCall: Call { public typealias Parser = Parser - public typealias ValidationBlock = (HTTPURLResponse?, Data?) throws -> Void + public typealias ValidationBlock = @Sendable (HTTPURLResponse?, Data?) throws -> Void public var request: URLRequestEncodable diff --git a/Sources/Core/Call.swift b/Sources/Core/Call.swift index 6faf24c..eadd64a 100644 --- a/Sources/Core/Call.swift +++ b/Sources/Core/Call.swift @@ -35,11 +35,11 @@ import Foundation /// /// Adopts `ResponseValidator`, so you can override `validate` if /// you want to validate the response for a specific `Call` type. -/// `AnyClient` will use this method to validate the response of the calls +/// `Session` will use this method to validate the response of the calls /// request before using its `Parser` to parse it. /// /// - seealso: `Client`, `Session`, `DataParser`, `Request` -public protocol Call: ResponseValidator { +public protocol Call: ResponseValidator, Sendable { associatedtype Parser: ResponseParser var request: URLRequestEncodable { get } @@ -50,5 +50,5 @@ public extension Call { func validate( response _: HTTPURLResponse?, data _: Data? - ) throws { /* no validation by default */ } + ) async throws { /* no validation by default */ } } diff --git a/Sources/Core/HTTPMethod.swift b/Sources/Core/HTTPMethod.swift index fcb9140..ae27a18 100644 --- a/Sources/Core/HTTPMethod.swift +++ b/Sources/Core/HTTPMethod.swift @@ -3,7 +3,7 @@ import Foundation /// An enum containing all HTTPMethods defined in RFC 2616 -public enum HTTPMethod: String { +public enum HTTPMethod: String, Sendable { case get = "GET" case post = "POST" case put = "PUT" diff --git a/Sources/Core/Request.swift b/Sources/Core/Request.swift index b6dba0c..a927f0e 100644 --- a/Sources/Core/Request.swift +++ b/Sources/Core/Request.swift @@ -48,16 +48,18 @@ public struct Request: URLRequestEncodable { } } -public extension URL { +extension URL { /// `true` if `self` has no scheme. /// - /// - note: Used by `AnyClient.encode` to determine if a `URLRequest` should be + /// - note: Used by `DefaultClient.encode` to determine if a `URLRequest` should be /// encoded using `self` alone (when `false`) or in combination with - /// its `baseURL` (when `true`). + /// its base URL (when `true`). var isRelative: Bool { scheme == nil } +} +public extension URL { /// Creates a relative URL with a given `path` and `query` Dictionary. init(path: String?, query: Parameters?) { var components = URLComponents() diff --git a/Sources/Core/URLRequestEncodable.swift b/Sources/Core/URLRequestEncodable.swift index 9707461..835b23b 100644 --- a/Sources/Core/URLRequestEncodable.swift +++ b/Sources/Core/URLRequestEncodable.swift @@ -5,7 +5,7 @@ import Foundation /// A type that can transform itself into an `URLRequest`. /// /// This protocol is adopted by `Request`, `URLRequest`, `URL` and `Call`. -public protocol URLRequestEncodable: CustomDebugStringConvertible { +public protocol URLRequestEncodable: CustomDebugStringConvertible, Sendable { /// Returns an `URLRequest` configured with the data encapsulated by `self`. var urlRequest: URLRequest { get } } diff --git a/Sources/Error/EndpointsParsingError.swift b/Sources/Error/EndpointsParsingError.swift index 53ebf6c..5c7af10 100644 --- a/Sources/Error/EndpointsParsingError.swift +++ b/Sources/Error/EndpointsParsingError.swift @@ -6,7 +6,7 @@ import Foundation public enum EndpointsParsingError: LocalizedError { /// `Data` is missing. /// - /// Thrown by `AnyClient.parse` when the response data is `nil`. + /// Thrown by `DefaultClient.parse` when the response data is `nil`. case missingData /// `Data` is in an invalid format. diff --git a/Sources/Parsing/DataParser.swift b/Sources/Parsing/DataParser.swift index 7d8537e..6849b28 100644 --- a/Sources/Parsing/DataParser.swift +++ b/Sources/Parsing/DataParser.swift @@ -6,9 +6,9 @@ import Foundation /// /// Used by `Call` to define the expected response type for its associated /// request. -public protocol DataParser { +public protocol DataParser: Sendable { /// The type that can be produced by `self`. - associatedtype OutputType + associatedtype OutputType: Sendable /// Converts a `Data` object with a specified encoding to `OutputType`. /// diff --git a/Sources/Parsing/ResponseParser/JSONParser.swift b/Sources/Parsing/ResponseParser/JSONParser.swift index 76453db..19449c9 100644 --- a/Sources/Parsing/ResponseParser/JSONParser.swift +++ b/Sources/Parsing/ResponseParser/JSONParser.swift @@ -4,13 +4,15 @@ import Foundation /// A `JSONParser` is a `DecodableParser` that works with JSON representation. /// It provides aa `jsonDecoder` to decode a response. -open class JSONParser: ResponseParser { +public struct JSONParser: ResponseParser { public typealias OutputType = T - public required init() {} + public let jsonDecoder: JSONDecoder - open var jsonDecoder: JSONDecoder { - JSONDecoder() + public init() { + self.jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .iso8601 + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase } public func parse(data: Data, encoding _: String.Encoding) throws -> OutputType { diff --git a/Sources/ResponseValidator/ResponseValidator.swift b/Sources/ResponseValidator/ResponseValidator.swift index d4dd1a4..5b11896 100644 --- a/Sources/ResponseValidator/ResponseValidator.swift +++ b/Sources/ResponseValidator/ResponseValidator.swift @@ -4,7 +4,7 @@ import Foundation /// A type responsible for validating the result produced by a /// `URLSession`s `completionHandler` block. -public protocol ResponseValidator { +public protocol ResponseValidator: Sendable { /// Validates the data provided by `URLSession`s `completionHandler` /// block. /// - throws: Any `Error`, if `result` is not valid. diff --git a/Sources/ResponseValidator/StatusCodeValidator.swift b/Sources/ResponseValidator/StatusCodeValidator.swift index 87f133f..82bd756 100644 --- a/Sources/ResponseValidator/StatusCodeValidator.swift +++ b/Sources/ResponseValidator/StatusCodeValidator.swift @@ -3,7 +3,7 @@ import Foundation /// A type validating the status code of `HTTPURLResponse`. -public class StatusCodeValidator: ResponseValidator { +public final class StatusCodeValidator: ResponseValidator { /// Checks if an HTTP status code is acceptable /// - returns: `true` if `code` is between 200 and 299. public func isAcceptableStatus(code: Int) -> Bool { @@ -14,7 +14,7 @@ public class StatusCodeValidator: ResponseValidator { public func validate( response: HTTPURLResponse?, data _: Data? - ) throws { + ) async throws { if let code = response?.statusCode, !isAcceptableStatus(code: code) { throw StatusCodeError.unacceptable(code: code, reason: nil) diff --git a/Sources/Testing/FakeResultProvider.swift b/Sources/Testing/FakeResultProvider.swift deleted file mode 100644 index 44629ce..0000000 --- a/Sources/Testing/FakeResultProvider.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -public protocol FakeResultProvider { - func data(for call: C) async throws -> (URLResponse, Data) -} diff --git a/Sources/Testing/FakeSession.swift b/Sources/Testing/FakeSession.swift deleted file mode 100644 index 1de89b8..0000000 --- a/Sources/Testing/FakeSession.swift +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright © 2023 DIAMIR. All Rights Reserved. - -import Foundation -#if canImport(OSLog) - import OSLog -#endif - -public class FakeSession: Session { - var resultProvider: FakeResultProvider - - public init(with client: CL, resultProvider: FakeResultProvider) { - self.resultProvider = resultProvider - - super.init(with: client) - } - - override public func dataTask( - for call: C - ) async throws -> (C.Parser.OutputType, HTTPURLResponse) { - let (response, data) = try await resultProvider.data(for: call) - - guard let response = response as? HTTPURLResponse else { - if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) { - Logger.default.debug("no response.") - } else { - os_log("no response.", log: .default, type: .debug) - } - - throw EndpointsError( - error: EndpointsParsingError.invalidData( - description: "Response was not a valid HTTPURLResponse" - ), - response: nil - ) - } - - if debug { - var message = "" - message += "\(call.request.cURLRepresentation)\n" - message += "\(response.debugDescription)\n" - message += "\(data.debugDescription(encoding: response.stringEncoding))" - - if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) { - Logger.default.debug("\(message, privacy: .private)") - } else { - os_log("%s", log: .default, type: .debug, message) - } - } - - do { - try await call.validate(response: response, data: data) // request-specific validation - try await client.validate(response: response, data: data) // global validation - - let value = try await client.parse(response: response, data: data, for: call) - return (value, response) - } catch { - throw EndpointsError(error: error, response: response) - } - } -} diff --git a/Tests/ClientTests.swift b/Tests/ClientTests.swift index e28f71e..20a1061 100644 --- a/Tests/ClientTests.swift +++ b/Tests/ClientTests.swift @@ -4,14 +4,14 @@ import Testing @Suite("Client Tests") struct ClientTests { - let tester: ClientTester + let tester: ClientTester init() { let baseURL = URL(string: "https://nghttp2.org/httpbin/")! - self.tester = ClientTester(client: AnyClient(baseURL: baseURL)) + self.tester = ClientTester(client: DefaultClient(url: baseURL)) } - @Test func testStatusError() async throws { + @Test func statusError() async throws { do { let call = AnyCall(Request(.get, "status/400")) _ = try await tester.performTest(call: call) @@ -23,14 +23,14 @@ struct ClientTests { } } - @Test func testGetData() async throws { + @Test func getData() async throws { let call = AnyCall(Request(.get, "get")) let (_, response) = try await tester.performTest(call: call) #expect(response.statusCode == 200) } @MainActor - @Test func testGetDataWithCancellation() async throws { + @Test func getDataWithCancellation() async throws { let task = Task { do { let call = AnyCall(Request(.get, "delay/5")) @@ -50,7 +50,7 @@ struct ClientTests { } @MainActor - @Test func testGetDataWithCancellationWhenTaskIsNotStarted() async throws { + @Test func getDataWithCancellationWhenTaskIsNotStarted() async throws { let task = Task { do { let call = AnyCall(Request(.get, "delay/5")) @@ -67,7 +67,7 @@ struct ClientTests { _ = await task.value } - @Test func testPostRawString() async throws { + @Test func postRawString() async throws { let requestBody = "body" let call = AnyCall>(Request( .post, @@ -81,7 +81,7 @@ struct ClientTests { #expect(headers["Content-Type"] == "raw") } - @Test func testPostString() async throws { + @Test func postString() async throws { let requestBody = "key=value" let call = AnyCall>(Request(.post, "post", body: requestBody)) let (body, _) = try await tester.performTest(call: call) @@ -90,7 +90,7 @@ struct ClientTests { #expect(form["key"] == "value") } - @Test func testPostFormEncodedBody() async throws { + @Test func postFormEncodedBody() async throws { let params = ["key": "&=?value+*-:_.😀"] let requestBody = FormEncodedBody(parameters: params) let call = AnyCall>(Request(.post, "post", body: requestBody)) @@ -103,14 +103,14 @@ struct ClientTests { #expect(headers["Content-Type"] == "application/x-www-form-urlencoded") } - @Test func testPostJSONBody() async throws { + @Test func postJSONBody() async throws { let params = ["key": "value"] let body = try JSONEncodedBody(jsonObject: params) let json = try await _testPostJSONBody(body: body) #expect(json == params) } - @Test func testPostJSONBodyEncodable() async throws { + @Test func postJSONBodyEncodable() async throws { let params = ["key": "value"] let json = try await _testPostJSONBody(body: JSONEncodedBody(encodable: params)) #expect(json == params) @@ -128,14 +128,14 @@ struct ClientTests { return json } - @Test func testGetString() async throws { + @Test func getString() async throws { let c = AnyCall(Request(.get, "get", query: ["inputParam": "inputParamValue"])) let (body, response) = try await tester.performTest(call: c) #expect(response.statusCode == 200) #expect(body.contains("inputParamValue")) } - @Test func testGetJSONDictionary() async throws { + @Test func getJSONDictionary() async throws { let c = AnyCall>(Request(.get, "get", query: ["inputParam": "inputParamValue"])) let (body, _) = try await tester.performTest(call: c) @@ -146,7 +146,7 @@ struct ClientTests { #expect(param == "inputParamValue") } - @Test func testParseJSONArray() throws { + @Test func parseJSONArray() throws { let inputArray = ["one", "two", "three"] let arrayData = try JSONSerialization.data(withJSONObject: inputArray) @@ -155,7 +155,7 @@ struct ClientTests { #expect(inputArray == parsedObject) } - @Test func testFailStringParsing() throws { + @Test func failStringParsing() throws { let input = "😜 test" let data = try #require(input.data(using: .utf8)) @@ -164,7 +164,7 @@ struct ClientTests { } } - @Test func testFailJSONParsing() async throws { + @Test func failJSONParsing() async throws { let c = AnyCall>(Request(.get, "xml")) let error = await #expect(throws: EndpointsError.self) { @@ -179,7 +179,7 @@ struct ClientTests { } } - @Test func testTypedRequest() async throws { + @Test func typedRequest() async throws { let value = "value" let c = GetOutput(value: value) let (body, _) = try await tester.performTest(call: c) @@ -190,14 +190,14 @@ struct ClientTests { #expect(param == value) } - @Test func testBasicAuth() async throws { + @Test func basicAuth() async throws { let auth = BasicAuthorization(user: "a", password: "a") let c = AnyCall(Request(.get, "basic-auth/a/a", header: auth.header)) let (_, response) = try await tester.performTest(call: c) #expect(response.statusCode == 200) } - @Test func testBasicAuthFail() async throws { + @Test func basicAuthFail() async throws { let auth = BasicAuthorization(user: "a", password: "b") let c = AnyCall(Request(.get, "basic-auth/a/a", header: auth.header)) @@ -209,28 +209,30 @@ struct ClientTests { #expect(response.statusCode == 401) } - @Test func testSimpleAbsoluteURLCall() async throws { + @Test func simpleAbsoluteURLCall() async throws { let url = try #require(URL(string: "https://httpbin.org/get?q=a")) let c = AnyCall(url) let (_, response) = try await tester.performTest(call: c) #expect(response.url == url) } - @Test func testSimpleRelativeURLRequestCall() async throws { + @Test func simpleRelativeURLRequestCall() async throws { let url = try #require(URL(string: "get?q=a")) let c = AnyCall(URLRequest(url: url)) let (_, response) = try await tester.performTest(call: c) - #expect(response.url == URL(string: url.relativeString, relativeTo: tester.session.client.baseURL)?.absoluteURL) + let expectedUrl = await URL(string: url.relativeString, relativeTo: tester.session.client.url)?.absoluteURL + #expect(response.url == expectedUrl) } - @Test func testRedirect() async throws { + @Test func redirect() async throws { let req = Request(.get, "relative-redirect/2", header: ["x": "y"]) let c = AnyCall(req) let (_, response) = try await tester.performTest(call: c) - #expect(response.url == URL(string: "get", relativeTo: tester.session.client.baseURL)?.absoluteURL) + let expectedUrl = await URL(string: "get", relativeTo: tester.session.client.url)?.absoluteURL + #expect(response.url == expectedUrl) } - @Test func testNoResponseBody() async throws { + @Test func noResponseBody() async throws { let c = AnyCall(Request(.get, "status/200")) let (_, response) = try await tester.performTest(call: c) #expect(response.statusCode == 200) diff --git a/Tests/Codable/Endpoints+JSONCodableTests.swift b/Tests/Codable/Endpoints+JSONCodableTests.swift index d41a045..1357330 100644 --- a/Tests/Codable/Endpoints+JSONCodableTests.swift +++ b/Tests/Codable/Endpoints+JSONCodableTests.swift @@ -13,7 +13,7 @@ class EndpointsJSONCodableTests: XCTestCase { // this test case is relevant, as there is a difference between using [City].parse // and parse on the same type, but with the heavy generics use of Endpoints func testDecodingArrayViaResponse() async throws { - let client = AnyClient(baseURL: URL(string: "www.tailored-apps.com")!) + let client = DefaultClient(url: URL(string: "www.tailored-apps.com")!) let call = CitiesCall() let cities = try await client.parse( @@ -43,8 +43,8 @@ class EndpointsJSONCodableTests: XCTestCase { } } - func testUsingCustomDecoderAndAnyClient() async throws { - let client = AnyClient(baseURL: URL(string: "www.tailored-apps.com")!) + func testUsingCustomDecoderAndDefaultClient() async throws { + let client = DefaultClient(url: URL(string: "www.tailored-apps.com")!) let call = PersonCall() do { @@ -127,8 +127,12 @@ extension EndpointsJSONCodableTests.Person { } } -class DateCrashParser: JSONParser { - override var jsonDecoder: JSONDecoder { - EndpointsJSONCodableTests.getDateCrashDecoder() +struct DateCrashParser: ResponseParser { + typealias OutputType = T + + let jsonDecoder = EndpointsJSONCodableTests.getDateCrashDecoder() + + func parse(data: Data, encoding: String.Encoding) throws -> T { + try jsonDecoder.decode(OutputType.self, from: data) } } diff --git a/Tests/MultipartTests.swift b/Tests/MultipartTests.swift index c5c92cd..af18b9a 100644 --- a/Tests/MultipartTests.swift +++ b/Tests/MultipartTests.swift @@ -91,7 +91,7 @@ class MultipartTests: XCTestCase { let multipartBody: MultipartBody } - let client = AnyClient(baseURL: URL(string: "https://httpbin.org")!) + let client = DefaultClient(url: URL(string: "https://httpbin.org")!) let session = Session(with: client) let call = PostCall(multipartBody: multipartBody) diff --git a/Tests/PostmanEcho/PostmanEchoClient.swift b/Tests/PostmanEcho/PostmanEchoClient.swift index 9d50c4f..045e4d0 100644 --- a/Tests/PostmanEcho/PostmanEchoClient.swift +++ b/Tests/PostmanEcho/PostmanEchoClient.swift @@ -3,11 +3,8 @@ @testable import Endpoints import Foundation -public class PostmanEchoClient: AnyClient { - public init() { - let url = URL(string: "https://postman-echo.com")! - super.init(baseURL: url) - } +public struct PostmanEchoClient { + public init() {} struct MyCall: Call { typealias Parser = JSONParser diff --git a/Tests/RequestTests.swift b/Tests/RequestTests.swift index 5c9a867..7dd182b 100644 --- a/Tests/RequestTests.swift +++ b/Tests/RequestTests.swift @@ -40,7 +40,7 @@ class RequestTests: XCTestCase { req.url = absoluteURL let c = AnyCall(req) - let urlReq = try await AnyClient(baseURL: URL(string: "http://google.com")!).encode(call: c) + let urlReq = try await DefaultClient(url: URL(string: "http://google.com")!).encode(call: c) XCTAssertEqual(urlReq.url, absoluteURL) XCTAssertEqual(urlReq.httpBody, body.requestData) @@ -101,7 +101,7 @@ extension RequestTests { ) async throws -> URLRequest { let request = Request(.get, path, query: queryParams) let call = AnyCall(request) - let client = AnyClient(baseURL: URL(string: baseUrl)!) + let client = DefaultClient(url: URL(string: baseUrl)!) let urlRequest = try await client.encode(call: call) let (data, response) = try await URLSession.shared.data(for: urlRequest) diff --git a/Tests/Utils/ClientTester.swift b/Tests/Utils/ClientTester.swift index 480d854..e35f685 100644 --- a/Tests/Utils/ClientTester.swift +++ b/Tests/Utils/ClientTester.swift @@ -5,12 +5,11 @@ import Foundation import Testing // Helper struct for running API calls within tests. -struct ClientTester { +struct ClientTester: Sendable { var session: Session init(client: CL) { self.session = Session(with: client) - session.debug = true } func performTest( diff --git a/Sources/Testing/FakeHTTPURLResponse.swift b/Tests/Utils/FakeHTTPURLResponse.swift similarity index 92% rename from Sources/Testing/FakeHTTPURLResponse.swift rename to Tests/Utils/FakeHTTPURLResponse.swift index 3fd2c42..c2d1d09 100644 --- a/Sources/Testing/FakeHTTPURLResponse.swift +++ b/Tests/Utils/FakeHTTPURLResponse.swift @@ -2,6 +2,8 @@ import Foundation +public typealias Parameters = [String: String] + public class FakeHTTPURLResponse: HTTPURLResponse, @unchecked Sendable { public init( status code: Int = 200,