diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml new file mode 100644 index 0000000..ce95625 --- /dev/null +++ b/.github/workflows/testing.yaml @@ -0,0 +1,29 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + name: Run Tests + runs-on: macos-15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '26.1.1' + + - name: Build and Test + run: | + xcodebuild test \ + -project EndpointsExample.xcodeproj \ + -scheme EndpointsExample \ + -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \ + -enableCodeCoverage YES \ + | xcpretty && exit ${PIPESTATUS[0]} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8a6972 --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# Xcode +# + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/EndpointsExample.xcodeproj/project.pbxproj b/EndpointsExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b6db740 --- /dev/null +++ b/EndpointsExample.xcodeproj/project.pbxproj @@ -0,0 +1,553 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 8211A84D2AFBDBFD00A36244 /* AsyncReactor in Frameworks */ = {isa = PBXBuildFile; productRef = 8211A84C2AFBDBFD00A36244 /* AsyncReactor */; }; + 82BA531E29DEE73D00F7726A /* Endpoints in Frameworks */ = {isa = PBXBuildFile; productRef = 82BA531D29DEE73D00F7726A /* Endpoints */; }; + FA00E2132EA1235900F4B75A /* Endpoints in Frameworks */ = {isa = PBXBuildFile; productRef = FA00E2122EA1235900F4B75A /* Endpoints */; }; + FA1376D02E97832400AB27E1 /* Endpoints in Frameworks */ = {isa = PBXBuildFile; productRef = FA1376CF2E97832400AB27E1 /* Endpoints */; }; + FA1376DA2E97849000AB27E1 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = FA1376D92E97848C00AB27E1 /* .gitignore */; }; + FA1377002E978A9000AB27E1 /* Endpoints in Frameworks */ = {isa = PBXBuildFile; productRef = FA1376FF2E978A9000AB27E1 /* Endpoints */; }; + FAD06B6B2E8C1C8C00E87155 /* Injection in Frameworks */ = {isa = PBXBuildFile; productRef = FAD06B6A2E8C1C8C00E87155 /* Injection */; }; +/* 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 */ + 82BA52ED29DEE35C00F7726A /* EndpointsExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EndpointsExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 82BA52FD29DEE35D00F7726A /* EndpointsExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EndpointsExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FA1376D92E97848C00AB27E1 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + FA07EB582E977E6900D9EA84 /* EndpointsExample */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = EndpointsExample; + sourceTree = ""; + }; + FA07EB672E977E6F00D9EA84 /* EndpointsExampleTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = EndpointsExampleTests; + sourceTree = ""; + }; + FA1376D42E9783AA00AB27E1 /* .github */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = .github; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 82BA52EA29DEE35C00F7726A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FAD06B6B2E8C1C8C00E87155 /* Injection in Frameworks */, + FA1376D02E97832400AB27E1 /* Endpoints in Frameworks */, + 8211A84D2AFBDBFD00A36244 /* AsyncReactor in Frameworks */, + FA00E2132EA1235900F4B75A /* Endpoints in Frameworks */, + FA1377002E978A9000AB27E1 /* Endpoints in Frameworks */, + 82BA531E29DEE73D00F7726A /* Endpoints in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 82BA52FA29DEE35D00F7726A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 82BA52E429DEE35C00F7726A = { + isa = PBXGroup; + children = ( + FA1376D92E97848C00AB27E1 /* .gitignore */, + FA1376D42E9783AA00AB27E1 /* .github */, + FA07EB582E977E6900D9EA84 /* EndpointsExample */, + FA07EB672E977E6F00D9EA84 /* EndpointsExampleTests */, + 82BA52EE29DEE35C00F7726A /* Products */, + 82BA531C29DEE73D00F7726A /* Frameworks */, + ); + sourceTree = ""; + }; + 82BA52EE29DEE35C00F7726A /* Products */ = { + isa = PBXGroup; + children = ( + 82BA52ED29DEE35C00F7726A /* EndpointsExample.app */, + 82BA52FD29DEE35D00F7726A /* EndpointsExampleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 82BA531C29DEE73D00F7726A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 82BA52EC29DEE35C00F7726A /* EndpointsExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 82BA531129DEE35D00F7726A /* Build configuration list for PBXNativeTarget "EndpointsExample" */; + buildPhases = ( + 82BA52E929DEE35C00F7726A /* Sources */, + 82BA52EA29DEE35C00F7726A /* Frameworks */, + 82BA52EB29DEE35C00F7726A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + FA07EB582E977E6900D9EA84 /* EndpointsExample */, + FA1376D42E9783AA00AB27E1 /* .github */, + ); + name = EndpointsExample; + packageProductDependencies = ( + 82BA531D29DEE73D00F7726A /* Endpoints */, + 8211A84C2AFBDBFD00A36244 /* AsyncReactor */, + FAD06B6A2E8C1C8C00E87155 /* Injection */, + FA1376CF2E97832400AB27E1 /* Endpoints */, + FA1376FF2E978A9000AB27E1 /* Endpoints */, + FA00E2122EA1235900F4B75A /* Endpoints */, + ); + productName = EndpointsTestbed; + productReference = 82BA52ED29DEE35C00F7726A /* EndpointsExample.app */; + productType = "com.apple.product-type.application"; + }; + 82BA52FC29DEE35D00F7726A /* EndpointsExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 82BA531429DEE35D00F7726A /* Build configuration list for PBXNativeTarget "EndpointsExampleTests" */; + buildPhases = ( + 82BA52F929DEE35D00F7726A /* Sources */, + 82BA52FA29DEE35D00F7726A /* Frameworks */, + 82BA52FB29DEE35D00F7726A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 82BA52FF29DEE35D00F7726A /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + FA07EB672E977E6F00D9EA84 /* EndpointsExampleTests */, + ); + name = EndpointsExampleTests; + productName = EndpointsTestbedTests; + productReference = 82BA52FD29DEE35D00F7726A /* EndpointsExampleTests.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 = 2600; + TargetAttributes = { + 82BA52EC29DEE35C00F7726A = { + CreatedOnToolsVersion = 14.3; + }; + 82BA52FC29DEE35D00F7726A = { + CreatedOnToolsVersion = 14.3; + TestTargetID = 82BA52EC29DEE35C00F7726A; + }; + }; + }; + buildConfigurationList = 82BA52E829DEE35C00F7726A /* Build configuration list for PBXProject "EndpointsExample" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 82BA52E429DEE35C00F7726A; + packageReferences = ( + 8211A84B2AFBDBFD00A36244 /* XCRemoteSwiftPackageReference "AsyncReactor" */, + FAD06B692E8C1C8C00E87155 /* XCRemoteSwiftPackageReference "Injection" */, + FA00E2112EA1235900F4B75A /* XCRemoteSwiftPackageReference "Endpoints" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 82BA52EE29DEE35C00F7726A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 82BA52EC29DEE35C00F7726A /* EndpointsExample */, + 82BA52FC29DEE35D00F7726A /* EndpointsExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 82BA52EB29DEE35C00F7726A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FA1376DA2E97849000AB27E1 /* .gitignore 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 = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 82BA52F929DEE35D00F7726A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 82BA52FF29DEE35D00F7726A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 82BA52EC29DEE35C00F7726A /* EndpointsExample */; + targetProxy = 82BA52FE29DEE35D00F7726A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 82BA530F29DEE35D00F7726A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + 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"; + SWIFT_STRICT_CONCURRENCY = complete; + }; + name = Debug; + }; + 82BA531029DEE35D00F7726A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + 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"; + SWIFT_STRICT_CONCURRENCY = complete; + 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 = "\"EndpointsExample/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 = "\"EndpointsExample/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)/EndpointsExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/EndpointsExample"; + }; + 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)/EndpointsExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/EndpointsExample"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 82BA52E829DEE35C00F7726A /* Build configuration list for PBXProject "EndpointsExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 82BA530F29DEE35D00F7726A /* Debug */, + 82BA531029DEE35D00F7726A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 82BA531129DEE35D00F7726A /* Build configuration list for PBXNativeTarget "EndpointsExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 82BA531229DEE35D00F7726A /* Debug */, + 82BA531329DEE35D00F7726A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 82BA531429DEE35D00F7726A /* Build configuration list for PBXNativeTarget "EndpointsExampleTests" */ = { + 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; + }; + }; + FA00E2112EA1235900F4B75A /* XCRemoteSwiftPackageReference "Endpoints" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/diamirio/Endpoints"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.0; + }; + }; + FAD06B692E8C1C8C00E87155 /* XCRemoteSwiftPackageReference "Injection" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/diamirio/Injection"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 8211A84C2AFBDBFD00A36244 /* AsyncReactor */ = { + isa = XCSwiftPackageProductDependency; + package = 8211A84B2AFBDBFD00A36244 /* XCRemoteSwiftPackageReference "AsyncReactor" */; + productName = AsyncReactor; + }; + 82BA531D29DEE73D00F7726A /* Endpoints */ = { + isa = XCSwiftPackageProductDependency; + productName = Endpoints; + }; + FA00E2122EA1235900F4B75A /* Endpoints */ = { + isa = XCSwiftPackageProductDependency; + package = FA00E2112EA1235900F4B75A /* XCRemoteSwiftPackageReference "Endpoints" */; + productName = Endpoints; + }; + FA1376CF2E97832400AB27E1 /* Endpoints */ = { + isa = XCSwiftPackageProductDependency; + productName = Endpoints; + }; + FA1376FF2E978A9000AB27E1 /* Endpoints */ = { + isa = XCSwiftPackageProductDependency; + productName = Endpoints; + }; + FAD06B6A2E8C1C8C00E87155 /* Injection */ = { + isa = XCSwiftPackageProductDependency; + package = FAD06B692E8C1C8C00E87155 /* XCRemoteSwiftPackageReference "Injection" */; + productName = Injection; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 82BA52E529DEE35C00F7726A /* Project object */; +} diff --git a/EndpointsExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/EndpointsExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/EndpointsExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/EndpointsExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/EndpointsExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/EndpointsExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/EndpointsExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/EndpointsExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..1c30c57 --- /dev/null +++ b/EndpointsExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,33 @@ +{ + "originHash" : "06743052361b4b7a9ca66b7fa3c9fb2bb5e55d19a61ce2f13f2ac3dc267d3ada", + "pins" : [ + { + "identity" : "asyncreactor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/diamirio/AsyncReactor", + "state" : { + "revision" : "e3fde7f9c1fb4db349039796c5c138571c71dadd", + "version" : "1.2.1" + } + }, + { + "identity" : "endpoints", + "kind" : "remoteSourceControl", + "location" : "https://github.com/diamirio/Endpoints", + "state" : { + "revision" : "22acf3704ff43c029595c34728eeb15b68ae5f99", + "version" : "4.0.0" + } + }, + { + "identity" : "injection", + "kind" : "remoteSourceControl", + "location" : "https://github.com/diamirio/Injection", + "state" : { + "revision" : "57a66d3f21ff5564542a6b157838ae97b67447f0", + "version" : "1.1.0" + } + } + ], + "version" : 3 +} diff --git a/EndpointsExample.xcodeproj/xcshareddata/IDETemplateMacros.plist b/EndpointsExample.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..e7f0ded --- /dev/null +++ b/EndpointsExample.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,8 @@ + + + + + FILEHEADER + Copyright © ___YEAR___ DIAMIR. All Rights Reserved. + + diff --git a/EndpointsExample.xcodeproj/xcshareddata/xcschemes/EndpointsExample.xcscheme b/EndpointsExample.xcodeproj/xcshareddata/xcschemes/EndpointsExample.xcscheme new file mode 100644 index 0000000..f121ec4 --- /dev/null +++ b/EndpointsExample.xcodeproj/xcshareddata/xcschemes/EndpointsExample.xcscheme @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EndpointsExample/Assets.xcassets/AccentColor.colorset/Contents.json b/EndpointsExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/EndpointsExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EndpointsExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/EndpointsExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/EndpointsExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EndpointsExample/Assets.xcassets/Contents.json b/EndpointsExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/EndpointsExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EndpointsExample/DependencyInjection.swift b/EndpointsExample/DependencyInjection.swift new file mode 100644 index 0000000..ffc3002 --- /dev/null +++ b/EndpointsExample/DependencyInjection.swift @@ -0,0 +1,26 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +import Endpoints +import Foundation +import Injection + +@MainActor +enum DependencyInjection { + static func register() { + let postmanEchoUrl = URL(string: "https://postman-echo.com")! + let postmanClient = DefaultClient(url: postmanEchoUrl) + + let httpBinUrl = URL(string: "https://httpbin.org/")! + let httpBinClient = DefaultClient(url: httpBinUrl) + + let customizedHttpBinClient = CustomizedHTTPBinClient() + + let api = ExampleAPI( + postmanClient: postmanClient, + httpBinClient: httpBinClient, + customizedHttpBinClient: customizedHttpBinClient, + ) + + DependencyInjector.register(api, as: API.self) + } +} diff --git a/EndpointsExample/EndpointsExampleApp.swift b/EndpointsExample/EndpointsExampleApp.swift new file mode 100644 index 0000000..9848328 --- /dev/null +++ b/EndpointsExample/EndpointsExampleApp.swift @@ -0,0 +1,26 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +import AsyncReactor +import SwiftUI + +@main +struct EndpointsExampleApp: App { + init() { + DependencyInjection.register() + } + + var body: some Scene { + WindowGroup { + NavigationStack { + List { + Section { + NavigationLink("MVVM", destination: ExampleView()) + NavigationLink("AsyncReactor", destination: ReactorView(ExampleReactor()) { + ExampleReactorView() + }) + } + } + } + } + } +} diff --git a/EndpointsExample/ExampleAsyncReactor/ExampleReactor.swift b/EndpointsExample/ExampleAsyncReactor/ExampleReactor.swift new file mode 100644 index 0000000..0133e80 --- /dev/null +++ b/EndpointsExample/ExampleAsyncReactor/ExampleReactor.swift @@ -0,0 +1,41 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +import AsyncReactor +import Endpoints +import Foundation +import Injection + +class ExampleReactor: AsyncReactor { + enum Action { + case executeRequest + } + + struct State { + var text = "" + } + + @Published + private(set) var state = State() + + @Inject + private var api: API + + func action(_ action: Action) async { + switch action { + case .executeRequest: + await executeRequest() + } + } + + private func executeRequest() async { + do { + let (body, response) = try await api.loadExampleData() + + guard response.statusCode == 200 else { return } + state.text = body.url + } catch { + guard let error = error as? EndpointsError else { return } + print(error.response?.statusCode ?? "") + } + } +} diff --git a/EndpointsExample/ExampleAsyncReactor/ExampleReactorView.swift b/EndpointsExample/ExampleAsyncReactor/ExampleReactorView.swift new file mode 100644 index 0000000..4e6eefe --- /dev/null +++ b/EndpointsExample/ExampleAsyncReactor/ExampleReactorView.swift @@ -0,0 +1,30 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +import AsyncReactor +import SwiftUI + +struct ExampleReactorView: View { + + @EnvironmentObject + private var reactor: ExampleReactor + + var body: some View { + VStack { + if reactor.state.text.isEmpty { + ProgressView() + } else { + Text(reactor.state.text) + .font(.headline) + } + } + .task { + await reactor.action(.executeRequest) + } + } +} + +#Preview { + ReactorView(ExampleReactor()) { + ExampleReactorView() + } +} diff --git a/EndpointsExample/ExampleMVVM/ExampleModel.swift b/EndpointsExample/ExampleMVVM/ExampleModel.swift new file mode 100644 index 0000000..b599384 --- /dev/null +++ b/EndpointsExample/ExampleMVVM/ExampleModel.swift @@ -0,0 +1,7 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +import Foundation + +struct ExampleModel: Codable { + var url: String +} diff --git a/EndpointsExample/ExampleMVVM/ExampleView.swift b/EndpointsExample/ExampleMVVM/ExampleView.swift new file mode 100644 index 0000000..538ffe3 --- /dev/null +++ b/EndpointsExample/ExampleMVVM/ExampleView.swift @@ -0,0 +1,25 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +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() + } + } +} + +#Preview { + ExampleView() +} diff --git a/EndpointsExample/ExampleMVVM/ExampleViewModel.swift b/EndpointsExample/ExampleMVVM/ExampleViewModel.swift new file mode 100644 index 0000000..49efd75 --- /dev/null +++ b/EndpointsExample/ExampleMVVM/ExampleViewModel.swift @@ -0,0 +1,31 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +import Endpoints +import Foundation +import Injection + +@MainActor +class ExampleViewModel: ObservableObject { + @Published + var text: String = "" + + @Inject + private var api: API + + func executeRequests() { + Task { + let (body, response) = try await api.loadExampleData() + guard response.statusCode == 200 else { return } + + await MainActor.run { + self.text = body.url + } + } + + Task { + let (_, response) = try await api.loadCustomizedData(deliveredStatusCode: 220) + guard response.statusCode == 220 else { return } + print("Success") + } + } +} diff --git a/EndpointsExample/Networking/API.swift b/EndpointsExample/Networking/API.swift new file mode 100644 index 0000000..0bb0ce8 --- /dev/null +++ b/EndpointsExample/Networking/API.swift @@ -0,0 +1,37 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +import Endpoints +import Foundation + +protocol API: Actor { + func loadExampleData() async throws -> (ExampleModel, HTTPURLResponse) + func loadCustomizedData(deliveredStatusCode: Int) async throws -> (String, HTTPURLResponse) +} + +actor ExampleAPI: API { + private let postmanSession: Session + private let httpBinSession: Session + private let customizedHttpBinSession: Session + + init( + postmanClient: DefaultClient, + httpBinClient: DefaultClient, + customizedHttpBinClient: CustomizedHTTPBinClient, + ) { + self.postmanSession = Session(with: postmanClient) + self.httpBinSession = Session(with: httpBinClient) + self.customizedHttpBinSession = Session(with: customizedHttpBinClient) + } + + func loadExampleData() async throws -> (ExampleModel, HTTPURLResponse) { + try await postmanSession.dataTask( + for: PostmanEchoClient.ExampleGetCall() + ) + } + + func loadCustomizedData(deliveredStatusCode: Int) async throws -> (String, HTTPURLResponse) { + try await customizedHttpBinSession.dataTask( + for: CustomizedHTTPBinClient.GetStatusCode(deliveredStatusCode: deliveredStatusCode) + ) + } +} diff --git a/EndpointsExample/Networking/CustomizedHTTPBinClient.swift b/EndpointsExample/Networking/CustomizedHTTPBinClient.swift new file mode 100644 index 0000000..c93c3c9 --- /dev/null +++ b/EndpointsExample/Networking/CustomizedHTTPBinClient.swift @@ -0,0 +1,42 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +import Endpoints +import Foundation + +struct CustomizedHTTPBinClient: Client { + private var client: Client + + init() { + let url = URL(string: "https://httpbin.org/")! + self.client = DefaultClient(url: url) + } + + func encode(call: some Endpoints.Call) async throws -> URLRequest { + // Custom manipulation i.e. OAuth implementation + print("- CUSTOMIZED encode -") + return try await client.encode(call: call) + } + + 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("- CUSTOMIZED parse -") + return try await client.parse(response: response, data: data, for: call) + } + + func validate(response: HTTPURLResponse?, data: Data?) async throws { + // Custom validation if needed + print("- CUSTOMIZED validate -") + return try await client.validate(response: response, data: data) + } + + struct GetStatusCode: Call { + typealias Parser = JSONParser + + let deliveredStatusCode: Int + + var request: URLRequestEncodable { + Request(.get, "/status/\(deliveredStatusCode)") + } + } +} diff --git a/EndpointsExample/Networking/HTTPBinClient.swift b/EndpointsExample/Networking/HTTPBinClient.swift new file mode 100644 index 0000000..1e18f1c --- /dev/null +++ b/EndpointsExample/Networking/HTTPBinClient.swift @@ -0,0 +1,16 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +import Endpoints +import Foundation + +enum HTTPBinClient { + struct GetStatusCode: Call { + let deliveredStatusCode: Int + + typealias Parser = JSONParser + + var request: URLRequestEncodable { + Request(.get, "/status/\(deliveredStatusCode)") + } + } +} diff --git a/EndpointsExample/Networking/PostmanEchoClient.swift b/EndpointsExample/Networking/PostmanEchoClient.swift new file mode 100644 index 0000000..fbe57d0 --- /dev/null +++ b/EndpointsExample/Networking/PostmanEchoClient.swift @@ -0,0 +1,14 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +import Endpoints +import Foundation + +enum PostmanEchoClient { + struct ExampleGetCall: Call { + typealias Parser = JSONParser + + var request: URLRequestEncodable { + Request(.get, "/get") + } + } +} diff --git a/EndpointsExample/Preview Content/Preview Assets.xcassets/Contents.json b/EndpointsExample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/EndpointsExample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EndpointsExampleTests/EndpointsExampleTests.swift b/EndpointsExampleTests/EndpointsExampleTests.swift new file mode 100644 index 0000000..18e27a0 --- /dev/null +++ b/EndpointsExampleTests/EndpointsExampleTests.swift @@ -0,0 +1,30 @@ +// Copyright © 2025 DIAMIR. All Rights Reserved. + +@testable import EndpointsExample +import XCTest + +final class EndpointsExampleTests: 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/README.md b/README.md index 3e6ef6a..c19a938 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -# Endpoints-Example -Example for Endpoints framework +# Endpoints Example + +This is an example iOS application demonstrating the [Endpoints](https://github.com/diamirio/Endpoints) framework - a type-safe, declarative way to define and consume HTTP endpoints in Swift.