From c009a957f949c2326ceb1700de45f236f856dc85 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Fri, 20 Oct 2023 13:58:35 +0300 Subject: [PATCH 01/26] Generate an initial project structure --- .github/ISSUE_TEMPLATE/bug_report.md | 41 ++++++ .github/ISSUE_TEMPLATE/feature_request.md | 11 ++ .../PULL_REQUEST_TEMPLATE/bug_template.yml | 9 ++ .../feature_template.yml | 12 ++ .github/workflows/ci.yml | 62 ++++++++ .github/workflows/danger.yml | 31 ++++ .gitignore | 95 +----------- .swiftformat | 64 +++++++++ .swiftlint.yml | 136 ++++++++++++++++++ CHANGELOG.md | 2 + Dangerfile | 1 + Gemfile | 3 + Makefile | 19 +++ Mintfile | 2 + Package.swift | 27 ++++ Package@swift-5.7.swift | 26 ++++ README.md | 57 +++++++- .../NetworkLayer/Classes/NetworkLayer.swift | 6 + .../Classes/NetworkLayer.swift | 6 + .../Classes/NetworkLayer.swift | 6 + .../NetworkLayerTests/NetworkLayerTests.swift | 8 ++ hooks/pre-commit | 38 +++++ 22 files changed, 573 insertions(+), 89 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/bug_template.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE/feature_template.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/danger.yml create mode 100644 .swiftformat create mode 100644 .swiftlint.yml create mode 100644 CHANGELOG.md create mode 100644 Dangerfile create mode 100644 Gemfile create mode 100644 Makefile create mode 100644 Mintfile create mode 100644 Package.swift create mode 100644 Package@swift-5.7.swift create mode 100644 Sources/NetworkLayer/Classes/NetworkLayer.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/NetworkLayer.swift create mode 100644 Sources/NetworkLayerMock/Classes/NetworkLayer.swift create mode 100644 Tests/NetworkLayerTests/NetworkLayerTests.swift create mode 100755 hooks/pre-commit diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8dc7e75 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: "🐛 Bug Report" +about: Report a reproducible bug or regression. +title: 'Bug: ' +labels: 'bug' + +--- + + + +Application version: + +## Steps To Reproduce + +1. +2. + + + +Link to code example: + + + +## The current behavior + + +## The expected behavior \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..d5e4d05 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,11 @@ +--- +name: 🛠 Feature request +about: If you have a feature request for the network-layer, file it here. +labels: 'type: enhancement' +--- + +**Feature description** +Clearly and concisely describe the feature. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_template.yml b/.github/PULL_REQUEST_TEMPLATE/bug_template.yml new file mode 100644 index 0000000..7d6a149 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/bug_template.yml @@ -0,0 +1,9 @@ +## Bug description +Clearly and concisely describe the problem. + +## Solution description +Describe your code changes in detail for reviewers. Explain the technical solution you have provided and how it fixes the issue case. + +## Covered unit test cases +- [x] yes +- [x] no \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/feature_template.yml b/.github/PULL_REQUEST_TEMPLATE/feature_template.yml new file mode 100644 index 0000000..ab3978b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature_template.yml @@ -0,0 +1,12 @@ +## Feature description +Clearly and concisely describe the feature. + +## Solution description +Describe your code changes in detail for reviewers. + +## Areas affected and ensured +List out the areas affected by your code changes. + +## Covered unit test cases +- [x] yes +- [x] no \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4224308 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: network-layer + +on: + push: + branches: + - main + - dev + pull_request: + paths: + - '.swiftlint.yml' + - ".github/workflows/**" + - "Package.swift" + - "Source/**" + - "Tests/**" +jobs: + SwiftLint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: GitHub Action for SwiftLint + uses: norio-nomura/action-swiftlint@3.2.1 + with: + args: --strict + env: + DIFF_BASE: ${{ github.base_ref }} + Latest: + name: Test Latest (iOS, macOS, tvOS, watchOS) + runs-on: macOS-12 + env: + DEVELOPER_DIR: "/Applications/Xcode_14.1.app/Contents/Developer" + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + include: + - destination: "OS=16.1,name=iPhone 14 Pro" + name: "iOS" + scheme: "network-layer" + sdk: iphonesimulator + - destination: "OS=16.1,name=Apple TV" + name: "tvOS" + scheme: "network-layer" + sdk: appletvsimulator + - destination: "OS=9.1,name=Apple Watch Series 8 (45mm)" + name: "watchOS" + scheme: "network-layer" + sdk: watchsimulator + - destination: "platform=macOS" + name: "macOS" + scheme: "network-layer" + sdk: macosx + steps: + - uses: actions/checkout@v3 + - name: ${{ matrix.name }} + run: xcodebuild test -scheme "${{ matrix.scheme }}" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "./${{ matrix.sdk }}.xcresult" | xcpretty -r junit + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3.1.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + xcode: true + xcode_archive_path: "./${{ matrix.sdk }}.xcresult" + \ No newline at end of file diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml new file mode 100644 index 0000000..e6c6e9a --- /dev/null +++ b/.github/workflows/danger.yml @@ -0,0 +1,31 @@ +name: Danger + +on: + pull_request: + types: [synchronize, opened, reopened, labeled, unlabeled, edited] + +env: + LC_CTYPE: en_US.UTF-8 + LANG: en_US.UTF-8 + +jobs: + run-danger: + runs-on: ubuntu-latest + steps: + - name: ruby setup + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + bundler-cache: true + - name: Checkout code + uses: actions/checkout@v2 + - name: Setup gems + run: | + gem install bundler + bundle install --clean --path vendor/bundle + - name: danger + env: + + DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} + + run: bundle exec danger --verbose \ No newline at end of file diff --git a/.gitignore b/.gitignore index 330d167..3b29812 100644 --- a/.gitignore +++ b/.gitignore @@ -1,90 +1,9 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings +.DS_Store +/.build +/Packages +/*.xcodeproj 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/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..13422b1 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,64 @@ +# Stream rules + +--swiftversion 5.3 + +# Use 'swiftformat --options' to list all of the possible options + +--header "\nnetwork-layer\nCopyright © {created.year} Space Code. All rights reserved.\n//" + +--enable blankLinesBetweenScopes +--enable blankLinesAtStartOfScope +--enable blankLinesAtEndOfScope +--enable blankLinesAroundMark +--enable anyObjectProtocol +--enable consecutiveBlankLines +--enable consecutiveSpaces +--enable duplicateImports +--enable elseOnSameLine +--enable emptyBraces +--enable initCoderUnavailable +--enable leadingDelimiters +--enable numberFormatting +--enable preferKeyPath +--enable redundantBreak +--enable redundantExtensionACL +--enable redundantFileprivate +--enable redundantGet +--enable redundantInit +--enable redundantLet +--enable redundantLetError +--enable redundantNilInit +--enable redundantObjc +--enable redundantParens +--enable redundantPattern +--enable redundantRawValues +--enable redundantReturn +--enable redundantSelf +--enable redundantVoidReturnType +--enable semicolons +--enable sortImports +--enable sortSwitchCases +--enable spaceAroundBraces +--enable spaceAroundBrackets +--enable spaceAroundComments +--enable spaceAroundGenerics +--enable spaceAroundOperators +--enable spaceInsideBraces +--enable spaceInsideBrackets +--enable spaceInsideComments +--enable spaceInsideGenerics +--enable spaceInsideParens +--enable strongOutlets +--enable strongifiedSelf +--enable todos +--enable trailingClosures +--enable unusedArguments +--enable void +--enable markTypes +--enable isEmpty + +# format options + +--wraparguments before-first +--wrapcollections before-first +--maxwidth 140 \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..e6ef405 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,136 @@ +excluded: + - Tests + - Package.swift + - Package@swift-5.7.swift + - .build + +# Rules + +disabled_rules: + - trailing_comma + - todo + - opening_brace + +opt_in_rules: # some rules are only opt-in + - anyobject_protocol + - array_init + - attributes + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_object_literal + - discouraged_optional_boolean + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - explicit_init + - fallthrough + - fatal_error_message + - file_name + - file_types_order + - first_where + - flatmap_over_map_reduce + - force_unwrapping + - ibinspectable_in_extension + - identical_operands + - implicit_return + - inert_defer + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent + - multiline_arguments + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + - no_space_in_method_call + - operator_usage_whitespace + - optional_enum_case_matching + - orphaned_doc_comment + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - prefixed_toplevel_constant + - private_action + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_objc_attribute + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strict_fileprivate + - switch_case_on_newline + - toggle_bool + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition + +force_cast: warning +force_try: warning + +identifier_name: + excluded: + - id + - URL + +analyzer_rules: + - unused_import + - unused_declaration + +line_length: + warning: 130 + error: 200 + +type_body_length: + warning: 300 + error: 400 + +file_length: + warning: 500 + error: 1200 + +function_body_length: + warning: 30 + error: 50 + +large_tuple: + error: 3 + +nesting: + type_level: + warning: 2 + statement_level: + warning: 10 + + +type_name: + max_length: + warning: 40 + error: 50 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2e9885a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +# Change Log +All notable changes to this project will be documented in this file. \ No newline at end of file diff --git a/Dangerfile b/Dangerfile new file mode 100644 index 0000000..b266982 --- /dev/null +++ b/Dangerfile @@ -0,0 +1 @@ +danger.import_dangerfile(github: 'space-code/dangerfile') \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..20dff64 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem 'danger' \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..856d64b --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +all: bootstrap + +bootstrap: hook + mint bootstrap + +hook: + ln -sf ../../hooks/pre-commit .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + +mint: + mint bootstrap + +lint: + mint run swiftlint + +fmt: + mint run swiftformat Sources Tests + +.PHONY: all bootstrap hook mint lint fmt \ No newline at end of file diff --git a/Mintfile b/Mintfile new file mode 100644 index 0000000..e2cdefa --- /dev/null +++ b/Mintfile @@ -0,0 +1,2 @@ +nicklockwood/SwiftFormat@0.52.7 +realm/SwiftLint@0.53.0 \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..bbaec53 --- /dev/null +++ b/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "NetworkLayer", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v7), + .tvOS(.v13), + .visionOS(.v1), + ], + products: [ + .library(name: "NetworkLayer", targets: ["NetworkLayer"]), + .library(name: "NetworkLayerInterfaces", targets: ["NetworkLayerInterfaces"]), + .library(name: "NetworkLayerMock", targets: ["NetworkLayerMock"]), + ], + dependencies: [], + targets: [ + .target(name: "NetworkLayer", dependencies: ["NetworkLayerInterfaces"]), + .target(name: "NetworkLayerInterfaces", dependencies: []), + .target(name: "NetworkLayerMock", dependencies: ["NetworkLayerInterfaces"]), + .testTarget(name: "NetworkLayerTests", dependencies: ["NetworkLayer"]), + ] +) diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift new file mode 100644 index 0000000..f2a2979 --- /dev/null +++ b/Package@swift-5.7.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "NetworkLayer", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v7), + .tvOS(.v13), + ], + products: [ + .library(name: "NetworkLayer", targets: ["NetworkLayer"]), + .library(name: "NetworkLayerInterfaces", targets: ["NetworkLayerInterfaces"]), + .library(name: "NetworkLayerMock", targets: ["NetworkLayerMock"]), + ], + dependencies: [], + targets: [ + .target(name: "NetworkLayer", dependencies: ["NetworkLayerInterfaces"]), + .target(name: "NetworkLayerInterfaces", dependencies: []), + .target(name: "NetworkLayerMock", dependencies: ["NetworkLayerInterfaces"]), + .testTarget(name: "NetworkLayerTests", dependencies: ["NetworkLayer"]), + ] +) diff --git a/README.md b/README.md index 21c4160..0553f66 100644 --- a/README.md +++ b/README.md @@ -1 +1,56 @@ -# network-layer \ No newline at end of file +

network-layer

+ +

+License +5.7 +CI + +

+ +## Description +`network-layer` description. + +- [Usage](#usage) +- [Requirements](#requirements) +- [Installation](#installation) +- [Communication](#communication) +- [Contributing](#contributing) +- [Author](#author) +- [License](#license) + +## Usage + +## Requirements + +## Installation +### Swift Package Manager + +The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but `network-layer` does support its use on supported platforms. + +Once you have your Swift package set up, adding `network-layer` as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. + +```swift +dependencies: [ + .package(url: "https://github.com/space-code/network-layer.git", .upToNextMajor(from: "1.0.0")) +] +``` + +## Communication +- If you **found a bug**, open an issue. +- If you **have a feature request**, open an issue. +- If you **want to contribute**, submit a pull request. + +## Contributing +Bootstrapping development environment + +``` +make bootstrap +``` + +Please feel free to help out with this project! If you see something that could be made better or want a new feature, open up an issue or send a Pull Request! + +## Author +Nikita Vasilev, nv3212@gmail.com + +## License +network-layer is available under the MIT license. See the LICENSE file for more info. \ No newline at end of file diff --git a/Sources/NetworkLayer/Classes/NetworkLayer.swift b/Sources/NetworkLayer/Classes/NetworkLayer.swift new file mode 100644 index 0000000..3ba47d5 --- /dev/null +++ b/Sources/NetworkLayer/Classes/NetworkLayer.swift @@ -0,0 +1,6 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +final class NetworkLayer {} diff --git a/Sources/NetworkLayerInterfaces/Classes/NetworkLayer.swift b/Sources/NetworkLayerInterfaces/Classes/NetworkLayer.swift new file mode 100644 index 0000000..7b2ca12 --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/NetworkLayer.swift @@ -0,0 +1,6 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation diff --git a/Sources/NetworkLayerMock/Classes/NetworkLayer.swift b/Sources/NetworkLayerMock/Classes/NetworkLayer.swift new file mode 100644 index 0000000..7b2ca12 --- /dev/null +++ b/Sources/NetworkLayerMock/Classes/NetworkLayer.swift @@ -0,0 +1,6 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation diff --git a/Tests/NetworkLayerTests/NetworkLayerTests.swift b/Tests/NetworkLayerTests/NetworkLayerTests.swift new file mode 100644 index 0000000..c6156c6 --- /dev/null +++ b/Tests/NetworkLayerTests/NetworkLayerTests.swift @@ -0,0 +1,8 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import XCTest + +final class NetworkLayerTests: XCTestCase {} diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..956fdcb --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,38 @@ +#!/bin/bash +git diff --diff-filter=d --staged --name-only | grep -e '\.swift$' | while read line; do + if [[ $line == *"/Generated"* ]]; then + echo "IGNORING GENERATED FILE: " "$line"; + else + mint run swiftformat swiftformat "${line}"; + git add "$line"; + fi +done + +LINT=$(which mint) +if [[ -e "${LINT}" ]]; then + # Export files in SCRIPT_INPUT_FILE_$count to lint against later + count=0 + while IFS= read -r file_path; do + export SCRIPT_INPUT_FILE_$count="$file_path" + count=$((count + 1)) + done < <(git diff --name-only --cached --diff-filter=d | grep ".swift$") + export SCRIPT_INPUT_FILE_COUNT=$count + + if [ "$count" -eq 0 ]; then + echo "No files to lint!" + exit 0 + fi + + echo "Found $count lintable files! Linting now.." + mint run swiftlint --use-script-input-files --strict --config .swiftlint.yml + RESULT=$? # swiftline exit value is number of errors + + if [ $RESULT -eq 0 ]; then + echo "🎉 Well done. No violation." + fi + exit $RESULT +else + echo "⚠️ WARNING: SwiftLint not found" + echo "⚠️ You might want to edit .git/hooks/pre-commit to locate your swiftlint" + exit 0 +fi \ No newline at end of file From 78755ce3cf0dfc126f1e698b7b794ae769507b2c Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Wed, 1 Nov 2023 15:41:21 +0300 Subject: [PATCH 02/26] Create `RequestProcessor` & `DataRequestHandler` --- .swiftlint.yml | 3 - .swiftpm/NetworkLayer-Package.xctestplan | 32 +++++ .../xcschemes/NetworkLayer-Package.xcscheme | 125 ++++++++++++++++++ .../xcschemes/NetworkLayer.xcscheme | 77 +++++++++++ .../xcschemes/NetworkLayerInterfaces.xcscheme | 66 +++++++++ .../xcschemes/NetworkLayerMock.xcscheme | 66 +++++++++ Package.swift | 19 ++- Package@swift-5.7.swift | 19 ++- .../DataRequestHandler.swift | 63 +++++++++ .../IDataRequestHandler.swift | 10 ++ .../RequestProcessor/RequestProcessor.swift | 84 ++++++++++++ .../NetworkLayer/Classes/NetworkLayer.swift | 6 - .../Classes/Core/Models/IRequest.swift | 26 ++++ .../Core/Services/IRequestBuilder.swift | 16 +++ .../Core/Services/IRequestProcessor.swift | 11 ++ .../INetworkLayerAssembly.swift} | 2 + .../Classes/NetworkLayer.swift | 6 - .../NetworkLayerTests/NetworkLayerTests.swift | 8 -- .../UnitTests/NetworkLayerTests.swift | 22 +++ 19 files changed, 628 insertions(+), 33 deletions(-) create mode 100644 .swiftpm/NetworkLayer-Package.xctestplan create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerMock.xcscheme create mode 100644 Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift create mode 100644 Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift create mode 100644 Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift delete mode 100644 Sources/NetworkLayer/Classes/NetworkLayer.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift rename Sources/NetworkLayerInterfaces/Classes/{NetworkLayer.swift => DI/INetworkLayerAssembly.swift} (69%) delete mode 100644 Sources/NetworkLayerMock/Classes/NetworkLayer.swift delete mode 100644 Tests/NetworkLayerTests/NetworkLayerTests.swift create mode 100644 Tests/NetworkLayerTests/UnitTests/NetworkLayerTests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index e6ef405..297f875 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -12,14 +12,12 @@ disabled_rules: - opening_brace opt_in_rules: # some rules are only opt-in - - anyobject_protocol - array_init - attributes - closure_body_length - closure_end_indentation - closure_spacing - collection_alignment - - conditional_returns_on_newline - contains_over_filter_count - contains_over_filter_is_empty - contains_over_first_not_nil @@ -43,7 +41,6 @@ opt_in_rules: # some rules are only opt-in - ibinspectable_in_extension - identical_operands - implicit_return - - inert_defer - joined_default_parameter - last_where - legacy_multiple diff --git a/.swiftpm/NetworkLayer-Package.xctestplan b/.swiftpm/NetworkLayer-Package.xctestplan new file mode 100644 index 0000000..1b4a98c --- /dev/null +++ b/.swiftpm/NetworkLayer-Package.xctestplan @@ -0,0 +1,32 @@ +{ + "configurations" : [ + { + "id" : "E73A5EB4-1B61-4334-AD82-FF0C87AE0EE0", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:", + "identifier" : "NetworkLayer", + "name" : "NetworkLayer" + } + ] + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "NetworkLayerTests", + "name" : "NetworkLayerTests" + } + } + ], + "version" : 1 +} diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme new file mode 100644 index 0000000..02e0619 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme new file mode 100644 index 0000000..e5c02d1 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme new file mode 100644 index 0000000..fc20aba --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerMock.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerMock.xcscheme new file mode 100644 index 0000000..b0d21cf --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerMock.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index bbaec53..3403ff5 100644 --- a/Package.swift +++ b/Package.swift @@ -15,13 +15,22 @@ let package = Package( products: [ .library(name: "NetworkLayer", targets: ["NetworkLayer"]), .library(name: "NetworkLayerInterfaces", targets: ["NetworkLayerInterfaces"]), - .library(name: "NetworkLayerMock", targets: ["NetworkLayerMock"]), ], dependencies: [], targets: [ - .target(name: "NetworkLayer", dependencies: ["NetworkLayerInterfaces"]), - .target(name: "NetworkLayerInterfaces", dependencies: []), - .target(name: "NetworkLayerMock", dependencies: ["NetworkLayerInterfaces"]), - .testTarget(name: "NetworkLayerTests", dependencies: ["NetworkLayer"]), + .target( + name: "NetworkLayer", + dependencies: ["NetworkLayerInterfaces"] + ), + .target( + name: "NetworkLayerInterfaces", + dependencies: [] + ), + .testTarget( + name: "NetworkLayerTests", + dependencies: [ + "NetworkLayer", + ] + ), ] ) diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index f2a2979..3a2fb66 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -14,13 +14,22 @@ let package = Package( products: [ .library(name: "NetworkLayer", targets: ["NetworkLayer"]), .library(name: "NetworkLayerInterfaces", targets: ["NetworkLayerInterfaces"]), - .library(name: "NetworkLayerMock", targets: ["NetworkLayerMock"]), ], dependencies: [], targets: [ - .target(name: "NetworkLayer", dependencies: ["NetworkLayerInterfaces"]), - .target(name: "NetworkLayerInterfaces", dependencies: []), - .target(name: "NetworkLayerMock", dependencies: ["NetworkLayerInterfaces"]), - .testTarget(name: "NetworkLayerTests", dependencies: ["NetworkLayer"]), + .target( + name: "NetworkLayer", + dependencies: ["NetworkLayerInterfaces"] + ), + .target( + name: "NetworkLayerInterfaces", + dependencies: [] + ), + .testTarget( + name: "NetworkLayerTests", + dependencies: [ + "NetworkLayer", + ] + ), ] ) diff --git a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift new file mode 100644 index 0000000..107007f --- /dev/null +++ b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift @@ -0,0 +1,63 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +// MARK: - DataRequestHandler + +final class DataRequestHandler: NSObject, IDataRequestHandler { + // MARK: Properties + + private var handlers: [URLSessionTask: DataTaskHandler] = [:] + private var userDataDelegate: URLSessionDataDelegate? + + var urlSessionDelegate: URLSessionDelegate? { + didSet { + userDataDelegate = urlSessionDelegate as? URLSessionDataDelegate + } + } + + private class DataTaskHandler { + typealias Completion = (Result) -> Void + + var data: Data? + var completion: Completion? + } + + func startDataTask(_ task: URLSessionDataTask, session _: URLSession, delegate _: URLSessionDelegate?) async throws -> Data { + try await withTaskCancellationHandler(operation: { + try await withUnsafeThrowingContinuation { continuation in + let dataTaskHandler = DataTaskHandler() + dataTaskHandler.completion = continuation.resume(with:) + handlers[task] = dataTaskHandler + task.resume() + } + }, onCancel: { + task.cancel() + }) + } +} + +// MARK: URLSessionDataDelegate + +extension DataRequestHandler { + func urlSession(_: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + guard let handler = handlers[dataTask] else { return } + + if handler.data == nil { + handler.data = Data() + } + handler.data?.append(data) + } +} + +// MARK: URLSessionTaskDelegate + +extension DataRequestHandler { + func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError _: Error?) { + guard let handler = handlers[task] else { return } + handlers[task] = nil + } +} diff --git a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift new file mode 100644 index 0000000..0e08d47 --- /dev/null +++ b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift @@ -0,0 +1,10 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +protocol IDataRequestHandler: URLSessionTaskDelegate & URLSessionDataDelegate { + func startDataTask(_ task: URLSessionDataTask, session: URLSession, delegate: URLSessionDelegate?) async throws -> Data +} diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift new file mode 100644 index 0000000..2daa027 --- /dev/null +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -0,0 +1,84 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +// MARK: - RequestProcessor + +actor RequestProcessor { + // MARK: Properties + + private let configuration: Configuration + private let session: URLSession + private let dataRequestHandler: any IDataRequestHandler + + private let requestBuilder: IRequestBuilder + + struct Configuration { + let sessionConfiguration: URLSessionConfiguration + let sessionDelegate: URLSessionDelegate? + let sessionDelegateQueue: OperationQueue? + let jsonDecoder: JSONDecoder + } + + // MARK: Initialization + + init(configuration: Configuration, requestBuilder: IRequestBuilder, dataRequestHandler: any IDataRequestHandler) { + self.configuration = configuration + self.requestBuilder = requestBuilder + self.dataRequestHandler = dataRequestHandler + session = URLSession( + configuration: configuration.sessionConfiguration, + delegate: dataRequestHandler, + delegateQueue: configuration.sessionDelegateQueue + ) + } + + // MARK: Private + + private func performRequest( + _ request: T, + delegate: URLSessionDelegate?, + configure _: ((inout URLRequest) throws -> Void)? + ) async throws -> Data { + guard let request = requestBuilder.build(request) else { + throw URLError(URLError.badURL) + } + + return try await performRequest { + let task = session.dataTask(with: request) + + do { + let response = try await dataRequestHandler.startDataTask(task, session: session, delegate: delegate) + return response + } catch { + throw URLError(URLError.cancelled) + } + } + } + + private func performRequest(attempts _: Int = 1, _ send: () async throws -> T) async throws -> T { + do { + return try await send() + } catch { + throw error + } + } +} + +// MARK: IRequestProcessor + +extension RequestProcessor: IRequestProcessor { + func send( + _ request: T, + delegate: URLSessionDelegate? = nil, + configure: ((inout URLRequest) throws -> Void)? = nil + ) async throws -> M { + let response = try await performRequest(request, delegate: delegate, configure: configure) + let item = try configuration.jsonDecoder.decode(M.self, from: response) + return item + } +} diff --git a/Sources/NetworkLayer/Classes/NetworkLayer.swift b/Sources/NetworkLayer/Classes/NetworkLayer.swift deleted file mode 100644 index 3ba47d5..0000000 --- a/Sources/NetworkLayer/Classes/NetworkLayer.swift +++ /dev/null @@ -1,6 +0,0 @@ -// -// network-layer -// Copyright © 2023 Space Code. All rights reserved. -// - -final class NetworkLayer {} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift new file mode 100644 index 0000000..cc1b3c8 --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift @@ -0,0 +1,26 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +public protocol IRequest { + /// Base url to the resource. + var domainName: String { get } + + /// Endpoint path. + var path: String { get } + + /// A dictonary that contains the parameters that will be encoded into request's header. + var headers: [String: String]? { get } + + /// A dictionary that contains the parameters that will be encoded into request. + var parameters: [String: String]? { get } + + /// A Boolean value indicating whether the requires authentication. + var requiresAuthentification: Bool { get } + + /// Request's timeout. + var timeoutInterval: TimeInterval { get } +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift new file mode 100644 index 0000000..a2ad79f --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift @@ -0,0 +1,16 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +/// A type that creates a `URLRequest`. +public protocol IRequestBuilder { + /// Creates a new `URLRequest` using `IRequest.` + /// + /// - Parameter request: The request object that defines the request details. + /// + /// - Returns: A `URLRequest` constructed based on the given data. + func build(_ request: IRequest) -> URLRequest? +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift new file mode 100644 index 0000000..bfdb0c7 --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift @@ -0,0 +1,11 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +// MARK: - IRequestProcessor + +/// A type capable of performing network requests. +public protocol IRequestProcessor {} diff --git a/Sources/NetworkLayerInterfaces/Classes/NetworkLayer.swift b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift similarity index 69% rename from Sources/NetworkLayerInterfaces/Classes/NetworkLayer.swift rename to Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift index 7b2ca12..ac5b5c7 100644 --- a/Sources/NetworkLayerInterfaces/Classes/NetworkLayer.swift +++ b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift @@ -4,3 +4,5 @@ // import Foundation + +public protocol INetworkLayerAssembly {} diff --git a/Sources/NetworkLayerMock/Classes/NetworkLayer.swift b/Sources/NetworkLayerMock/Classes/NetworkLayer.swift deleted file mode 100644 index 7b2ca12..0000000 --- a/Sources/NetworkLayerMock/Classes/NetworkLayer.swift +++ /dev/null @@ -1,6 +0,0 @@ -// -// network-layer -// Copyright © 2023 Space Code. All rights reserved. -// - -import Foundation diff --git a/Tests/NetworkLayerTests/NetworkLayerTests.swift b/Tests/NetworkLayerTests/NetworkLayerTests.swift deleted file mode 100644 index c6156c6..0000000 --- a/Tests/NetworkLayerTests/NetworkLayerTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// network-layer -// Copyright © 2023 Space Code. All rights reserved. -// - -import XCTest - -final class NetworkLayerTests: XCTestCase {} diff --git a/Tests/NetworkLayerTests/UnitTests/NetworkLayerTests.swift b/Tests/NetworkLayerTests/UnitTests/NetworkLayerTests.swift new file mode 100644 index 0000000..a163b6f --- /dev/null +++ b/Tests/NetworkLayerTests/UnitTests/NetworkLayerTests.swift @@ -0,0 +1,22 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +@testable import NetworkLayer +import XCTest + +// MARK: - RequestProcessorTests + +final class NetworkLayerTests: XCTestCase { + // MARK: XCTestCase + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } +} From 8bcf0e0a3163e1cc0b38e589bf167934eedb7f19 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Tue, 7 Nov 2023 19:02:51 +0300 Subject: [PATCH 03/26] Add `HTTPMethod` & `Response` models --- .../DataRequestHandler.swift | 61 +++++++++++++++---- .../IDataRequestHandler.swift | 16 ++++- .../RequestProcessor/RequestProcessor.swift | 4 +- .../Classes/Core/Models/HTTPMethod.swift | 32 ++++++++++ .../Classes/Core/Models/IRequest.swift | 21 ++++--- .../Classes/Core/Models/Response.swift | 16 +++++ .../Core/Services/IRequestProcessor.swift | 28 ++++++++- 7 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Models/HTTPMethod.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift diff --git a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift index 107007f..69647cd 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift @@ -4,13 +4,16 @@ // import Foundation +import NetworkLayerInterfaces // MARK: - DataRequestHandler -final class DataRequestHandler: NSObject, IDataRequestHandler { +final class DataRequestHandler: NSObject { // MARK: Properties - private var handlers: [URLSessionTask: DataTaskHandler] = [:] + private typealias HandlerDictonary = [URLSessionTask: DataTaskHandler] + + private var handlers: HandlerDictonary = [:] private var userDataDelegate: URLSessionDataDelegate? var urlSessionDelegate: URLSessionDelegate? { @@ -18,18 +21,19 @@ final class DataRequestHandler: NSObject, IDataRequestHandler { userDataDelegate = urlSessionDelegate as? URLSessionDataDelegate } } +} - private class DataTaskHandler { - typealias Completion = (Result) -> Void - - var data: Data? - var completion: Completion? - } +// MARK: IDataRequestHandler - func startDataTask(_ task: URLSessionDataTask, session _: URLSession, delegate _: URLSessionDelegate?) async throws -> Data { +extension DataRequestHandler: IDataRequestHandler { + func startDataTask( + _ task: URLSessionDataTask, + session _: URLSession, + delegate: URLSessionDelegate? + ) async throws -> Response { try await withTaskCancellationHandler(operation: { try await withUnsafeThrowingContinuation { continuation in - let dataTaskHandler = DataTaskHandler() + let dataTaskHandler = DataTaskHandler(delegate: delegate) dataTaskHandler.completion = continuation.resume(with:) handlers[task] = dataTaskHandler task.resume() @@ -56,8 +60,43 @@ extension DataRequestHandler { // MARK: URLSessionTaskDelegate extension DataRequestHandler { - func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError _: Error?) { + func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { guard let handler = handlers[task] else { return } handlers[task] = nil + + if let error = error { + handler.completion?(.failure(error)) + } else { + if let response = task.response { + let data = handler.data ?? Data() + let response = Response(data: data, response: response) + handler.completion?(.success(response)) + } else { + handler.completion?(.failure(URLError(.unknown))) + } + } + } +} + +// MARK: DataRequestHandler.DataTaskHandler + +private extension DataRequestHandler { + private class DataTaskHandler { + // MARK: Types + + typealias Completion = (Result, Error>) -> Void + + // MARK: Properties + + let delegate: URLSessionDelegate? + + var data: Data? + var completion: Completion? + + // MARK: Initialization + + init(delegate: URLSessionDelegate?) { + self.delegate = delegate + } } } diff --git a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift index 0e08d47..f23cd83 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift @@ -4,7 +4,21 @@ // import Foundation +import NetworkLayerInterfaces +/// A protocol for handling data requests. protocol IDataRequestHandler: URLSessionTaskDelegate & URLSessionDataDelegate { - func startDataTask(_ task: URLSessionDataTask, session: URLSession, delegate: URLSessionDelegate?) async throws -> Data + /// Starts a data task for handling network requests. + /// + /// - Parameters: + /// - task: The `URLSessionDataTask` representing the network task to be initiated. + /// - session: The `URLSession` to use for the data task. + /// - delegate: An optional `URLSessionDelegate` for handling `URLSession` events and callbacks. Pass `nil` if not needed. + /// + /// - Returns: An asynchronous task that will result in a Response object containing data when the request is completed. + func startDataTask( + _ task: URLSessionDataTask, + session: URLSession, + delegate: URLSessionDelegate? + ) async throws -> Response } diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index 2daa027..f153bdf 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -43,7 +43,7 @@ actor RequestProcessor { _ request: T, delegate: URLSessionDelegate?, configure _: ((inout URLRequest) throws -> Void)? - ) async throws -> Data { + ) async throws -> Response { guard let request = requestBuilder.build(request) else { throw URLError(URLError.badURL) } @@ -78,7 +78,7 @@ extension RequestProcessor: IRequestProcessor { configure: ((inout URLRequest) throws -> Void)? = nil ) async throws -> M { let response = try await performRequest(request, delegate: delegate, configure: configure) - let item = try configuration.jsonDecoder.decode(M.self, from: response) + let item = try configuration.jsonDecoder.decode(M.self, from: response.data) return item } } diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/HTTPMethod.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/HTTPMethod.swift new file mode 100644 index 0000000..07a0b3c --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/HTTPMethod.swift @@ -0,0 +1,32 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +/// Enum representing HTTP methods. +/// +/// See https://tools.ietf.org/html/rfc7231#section-4.3 +public enum HTTPMethod { + /// `CONNECT` method. + case connect + /// `DELETE` method. + case delete + /// `GET` method. + case get + /// `HEAD` method. + case head + /// `OPTIONS` method. + case options + /// `PATCH` method. + case patch + /// `POST` method. + case post + /// `PUT` method. + case put + /// `QUERY` method. + case query + /// `TRACE` method. + case trace +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift index cc1b3c8..2428c9e 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift @@ -5,22 +5,29 @@ import Foundation +/// A type to which all requests must conform. public protocol IRequest { - /// Base url to the resource. + /// The base `URL` for the resource. var domainName: String { get } - /// Endpoint path. + /// The endpoint path. var path: String { get } - /// A dictonary that contains the parameters that will be encoded into request's header. + /// A dictionary that contains the parameters to be encoded into the request's header. var headers: [String: String]? { get } - /// A dictionary that contains the parameters that will be encoded into request. + /// A dictionary that contains the parameters to be encoded into the request. var parameters: [String: String]? { get } - /// A Boolean value indicating whether the requires authentication. - var requiresAuthentification: Bool { get } + /// A Boolean value indicating whether authentication is required. + var requiresAuthentication: Bool { get } - /// Request's timeout. + /// Request's timeout interval. var timeoutInterval: TimeInterval { get } + + /// The HTTP method. + var httpMethod: HTTPMethod { get } + + /// A dictonary that contains the request's body. + var httpBody: [String: Any]? { get } } diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift new file mode 100644 index 0000000..4320e99 --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift @@ -0,0 +1,16 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +public struct Response { + public let data: T + public let response: URLResponse + + public init(data: T, response: URLResponse) { + self.data = data + self.response = response + } +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift index bfdb0c7..56626ba 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift @@ -8,4 +8,30 @@ import Foundation // MARK: - IRequestProcessor /// A type capable of performing network requests. -public protocol IRequestProcessor {} +public protocol IRequestProcessor { + /// Sends a network request. + /// + /// - Parameters: + /// - request: The request object conforming to the `IRequest` protocol, representing the network request to be sent. + /// - delegate: An optional `URLSessionDelegate` for handling `URLSession` events and callbacks. Pass `nil` if not needed. + /// - configure: An optional closure that allows custom configuration of the URLRequest before sending the request. + /// Pass `nil` if not + /// needed. + func send( + _ request: T, + delegate: URLSessionDelegate?, + configure: ((inout URLRequest) throws -> Void)? + ) async throws -> M +} + +extension IRequestProcessor { + /// Sends a network request with default parameters. + /// + /// - Parameters: + /// - request: The request object conforming to the `IRequest` protocol, representing the network request to be sent. + func send( + _ request: T + ) async throws -> M { + try await send(request, delegate: nil, configure: nil) + } +} From a5e93f35987cdd11c18352ffc36e6ba8ee519af6 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 13 Nov 2023 18:56:11 +0300 Subject: [PATCH 04/26] Implement a retry policy - Integrate `Typhoon` to suppor a retry policy within the package - Integrate `Atomic` to protect a mutable shared resource --- .swiftpm/NetworkLayer-Package.xctestplan | 32 ----- .../xcschemes/NetworkLayer-Package.xcscheme | 125 ------------------ .../xcschemes/NetworkLayer.xcscheme | 77 ----------- .../xcschemes/NetworkLayerInterfaces.xcscheme | 66 --------- .../xcschemes/NetworkLayerMock.xcscheme | 66 --------- Package.resolved | 23 ++++ Package.swift | 15 ++- Package@swift-5.7.swift | 11 +- .../RequestProcessor/RequestProcessor.swift | 32 +++-- .../Core/Models/NetworkLayerError.swift | 12 ++ .../Core/Services/IRequestProcessor.swift | 7 +- 11 files changed, 81 insertions(+), 385 deletions(-) delete mode 100644 .swiftpm/NetworkLayer-Package.xctestplan delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerMock.xcscheme create mode 100644 Package.resolved create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Models/NetworkLayerError.swift diff --git a/.swiftpm/NetworkLayer-Package.xctestplan b/.swiftpm/NetworkLayer-Package.xctestplan deleted file mode 100644 index 1b4a98c..0000000 --- a/.swiftpm/NetworkLayer-Package.xctestplan +++ /dev/null @@ -1,32 +0,0 @@ -{ - "configurations" : [ - { - "id" : "E73A5EB4-1B61-4334-AD82-FF0C87AE0EE0", - "name" : "Test Scheme Action", - "options" : { - - } - } - ], - "defaultOptions" : { - "codeCoverage" : { - "targets" : [ - { - "containerPath" : "container:", - "identifier" : "NetworkLayer", - "name" : "NetworkLayer" - } - ] - } - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:", - "identifier" : "NetworkLayerTests", - "name" : "NetworkLayerTests" - } - } - ], - "version" : 1 -} diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme deleted file mode 100644 index 02e0619..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme deleted file mode 100644 index e5c02d1..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme deleted file mode 100644 index fc20aba..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerMock.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerMock.xcscheme deleted file mode 100644 index b0d21cf..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerMock.xcscheme +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..ef0df90 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "atomic", + "kind" : "remoteSourceControl", + "location" : "https://github.com/space-code/atomic", + "state" : { + "revision" : "53fae2fc8216bb5c27c87b245f893176d0d290eb", + "version" : "1.0.0" + } + }, + { + "identity" : "typhoon", + "kind" : "remoteSourceControl", + "location" : "https://github.com/space-code/typhoon", + "state" : { + "revision" : "c0e2970812f4ec837fc61afd72a7ca1f0aa39306", + "version" : "1.0.0" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift index 3403ff5..608fe27 100644 --- a/Package.swift +++ b/Package.swift @@ -16,15 +16,24 @@ let package = Package( .library(name: "NetworkLayer", targets: ["NetworkLayer"]), .library(name: "NetworkLayerInterfaces", targets: ["NetworkLayerInterfaces"]), ], - dependencies: [], + dependencies: [ + .package(url: "https://github.com/space-code/atomic", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/space-code/typhoon", .upToNextMajor(from: "1.0.0")), + ], targets: [ .target( name: "NetworkLayer", - dependencies: ["NetworkLayerInterfaces"] + dependencies: [ + "NetworkLayerInterfaces", + .product(name: "Atomic", package: "atomic"), + .product(name: "Typhoon", package: "typhoon"), + ] ), .target( name: "NetworkLayerInterfaces", - dependencies: [] + dependencies: [ + .product(name: "Typhoon", package: "typhoon"), + ] ), .testTarget( name: "NetworkLayerTests", diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index 3a2fb66..a718890 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -15,11 +15,18 @@ let package = Package( .library(name: "NetworkLayer", targets: ["NetworkLayer"]), .library(name: "NetworkLayerInterfaces", targets: ["NetworkLayerInterfaces"]), ], - dependencies: [], + dependencies: [ + .package(url: "https://github.com/space-code/atomic", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/space-code/typhoon", .upToNextMajor(from: "1.0.0")), + ], targets: [ .target( name: "NetworkLayer", - dependencies: ["NetworkLayerInterfaces"] + dependencies: [ + "NetworkLayerInterfaces", + .product(name: "Atomic", package: "atomic"), + .product(name: "Typhoon", package: "typhoon"), + ] ), .target( name: "NetworkLayerInterfaces", diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index f153bdf..406076b 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -5,6 +5,7 @@ import Foundation import NetworkLayerInterfaces +import Typhoon // MARK: - RequestProcessor @@ -16,6 +17,7 @@ actor RequestProcessor { private let dataRequestHandler: any IDataRequestHandler private let requestBuilder: IRequestBuilder + private let retryPolicyService: IRetryPolicyService struct Configuration { let sessionConfiguration: URLSessionConfiguration @@ -26,10 +28,16 @@ actor RequestProcessor { // MARK: Initialization - init(configuration: Configuration, requestBuilder: IRequestBuilder, dataRequestHandler: any IDataRequestHandler) { + init( + configuration: Configuration, + requestBuilder: IRequestBuilder, + dataRequestHandler: any IDataRequestHandler, + retryPolicyService: IRetryPolicyService + ) { self.configuration = configuration self.requestBuilder = requestBuilder self.dataRequestHandler = dataRequestHandler + self.retryPolicyService = retryPolicyService session = URLSession( configuration: configuration.sessionConfiguration, delegate: dataRequestHandler, @@ -41,31 +49,30 @@ actor RequestProcessor { private func performRequest( _ request: T, + strategy: RetryPolicyStrategy? = nil, delegate: URLSessionDelegate?, configure _: ((inout URLRequest) throws -> Void)? ) async throws -> Response { guard let request = requestBuilder.build(request) else { - throw URLError(URLError.badURL) + throw NetworkLayerError.badURL } - return try await performRequest { + return try await performRequest(strategy: strategy) { let task = session.dataTask(with: request) do { - let response = try await dataRequestHandler.startDataTask(task, session: session, delegate: delegate) - return response + return try await dataRequestHandler.startDataTask(task, session: session, delegate: delegate) } catch { - throw URLError(URLError.cancelled) + throw error } } } - private func performRequest(attempts _: Int = 1, _ send: () async throws -> T) async throws -> T { - do { - return try await send() - } catch { - throw error - } + private func performRequest( + strategy: RetryPolicyStrategy? = nil, + _ send: () async throws -> T + ) async throws -> T { + try await retryPolicyService.retry(strategy: strategy, send) } } @@ -74,6 +81,7 @@ actor RequestProcessor { extension RequestProcessor: IRequestProcessor { func send( _ request: T, + strategy _: RetryPolicyStrategy? = nil, delegate: URLSessionDelegate? = nil, configure: ((inout URLRequest) throws -> Void)? = nil ) async throws -> M { diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/NetworkLayerError.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/NetworkLayerError.swift new file mode 100644 index 0000000..50ad3a7 --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/NetworkLayerError.swift @@ -0,0 +1,12 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +/// `NetworkLayerError` is the error type returned by NetworkLayer. +public enum NetworkLayerError: Swift.Error { + /// A malformed URL prevented a URL request from being initiated. + case badURL +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift index 56626ba..0b2dafa 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift @@ -4,6 +4,7 @@ // import Foundation +import Typhoon // MARK: - IRequestProcessor @@ -19,6 +20,7 @@ public protocol IRequestProcessor { /// needed. func send( _ request: T, + strategy: RetryPolicyStrategy?, delegate: URLSessionDelegate?, configure: ((inout URLRequest) throws -> Void)? ) async throws -> M @@ -30,8 +32,9 @@ extension IRequestProcessor { /// - Parameters: /// - request: The request object conforming to the `IRequest` protocol, representing the network request to be sent. func send( - _ request: T + _ request: T, + strategy: RetryPolicyStrategy? ) async throws -> M { - try await send(request, delegate: nil, configure: nil) + try await send(request, strategy: strategy, delegate: nil, configure: nil) } } From 7a28851a262344bf442e4bffc60225c6179b0d37 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 13 Nov 2023 18:59:59 +0300 Subject: [PATCH 05/26] Wrap a property using `@Atomic` property wrapper --- .../Core/Services/DataRequestHandler/DataRequestHandler.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift index 69647cd..5f2639b 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift @@ -3,6 +3,7 @@ // Copyright © 2023 Space Code. All rights reserved. // +import Atomic import Foundation import NetworkLayerInterfaces @@ -13,7 +14,7 @@ final class DataRequestHandler: NSObject { private typealias HandlerDictonary = [URLSessionTask: DataTaskHandler] - private var handlers: HandlerDictonary = [:] + @Atomic private var handlers: HandlerDictonary = [:] private var userDataDelegate: URLSessionDataDelegate? var urlSessionDelegate: URLSessionDelegate? { From c94cbd6d12dfc734b8d65c8952968c569a2d1f51 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 13 Nov 2023 20:32:04 +0300 Subject: [PATCH 06/26] Add code comments to all public interfaces --- .../DataRequestHandler.swift | 6 +++ .../RequestProcessor/RequestProcessor.swift | 38 +++++++++++---- .../Classes/DI/NetworkLayerAssembly.swift | 46 +++++++++++++++++++ .../Classes/Core/Models/Configuration.swift | 43 +++++++++++++++++ .../Core/Services}/IDataRequestHandler.swift | 3 +- .../Classes/DI/INetworkLayerAssembly.swift | 23 +++++++++- 6 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Models/Configuration.swift rename Sources/{NetworkLayer/Classes/Core/Services/DataRequestHandler => NetworkLayerInterfaces/Classes/Core/Services}/IDataRequestHandler.swift (88%) diff --git a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift index 5f2639b..8d6dd0e 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift @@ -9,14 +9,20 @@ import NetworkLayerInterfaces // MARK: - DataRequestHandler +/// Manages data request handlers for URLSessionTasks. final class DataRequestHandler: NSObject { // MARK: Properties private typealias HandlerDictonary = [URLSessionTask: DataTaskHandler] + /// The dictonary that stores handlers. @Atomic private var handlers: HandlerDictonary = [:] + /// A protocol that defines methods that URL session instances call on their + /// delegates to handle task-level events specific to data and upload tasks. private var userDataDelegate: URLSessionDataDelegate? + /// A protocol that defines methods that URL session instances call on their + /// delegates to handle session-level events, like session life cycle changes. var urlSessionDelegate: URLSessionDelegate? { didSet { userDataDelegate = urlSessionDelegate as? URLSessionDataDelegate diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index 406076b..d363fc3 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -9,25 +9,30 @@ import Typhoon // MARK: - RequestProcessor +/// An object that handles request processing. actor RequestProcessor { // MARK: Properties + /// The network layer's configuration. private let configuration: Configuration + /// The object that coordinates a group of related, network data transfer tasks. private let session: URLSession + /// The data request handler. private let dataRequestHandler: any IDataRequestHandler - + /// The request builder. private let requestBuilder: IRequestBuilder + /// The retry policy service. private let retryPolicyService: IRetryPolicyService - struct Configuration { - let sessionConfiguration: URLSessionConfiguration - let sessionDelegate: URLSessionDelegate? - let sessionDelegateQueue: OperationQueue? - let jsonDecoder: JSONDecoder - } - // MARK: Initialization + /// Creates a new `RequestProcessor` instance. + /// + /// - Parameters: + /// - configure: The network layer's configuration. + /// - requestBuilder: The request builder. + /// - dataRequestHandler: The data request handler. + /// - retryPolicyService: The retry policy service. init( configuration: Configuration, requestBuilder: IRequestBuilder, @@ -47,6 +52,16 @@ actor RequestProcessor { // MARK: Private + /// Performs a network request. + /// + /// - Parameters: + /// - request: The network request. + /// - strategy: The retry policy strategy. + /// - delegate: A protocol that defines methods that URL session instances call on their delegates + /// to handle session-level events, like session life cycle changes. + /// - configure: A closure to configure the URLRequest. + /// + /// - Returns: The response from the network request. private func performRequest( _ request: T, strategy: RetryPolicyStrategy? = nil, @@ -68,6 +83,13 @@ actor RequestProcessor { } } + /// Performs a request with a retry policy. + /// + /// - Parameters: + /// - strategy: The strategy for retrying the request. + /// - send: The closure that sends the request. + /// + /// - Returns: The response from the network request. private func performRequest( strategy: RetryPolicyStrategy? = nil, _ send: () async throws -> T diff --git a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift new file mode 100644 index 0000000..f4291ca --- /dev/null +++ b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift @@ -0,0 +1,46 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces +import Typhoon + +public final class NetworkLayerAssembly: INetworkLayerAssembly { + // MARK: Properties + + /// The network layer's configuration. + private let configure: Configuration + /// The request builder. + private let requestBuilder: IRequestBuilder + /// The data request handler. + private let dataRequestHandler: IDataRequestHandler + /// The retry policy service. + private let retryPolicyService: IRetryPolicyService + + // MARK: Initialization + + public init( + configure: Configuration, + requestBuilder: IRequestBuilder, + dataRequestHandler: IDataRequestHandler, + retryPolicyService: IRetryPolicyService + ) { + self.configure = configure + self.requestBuilder = requestBuilder + self.dataRequestHandler = dataRequestHandler + self.retryPolicyService = retryPolicyService + } + + // MARK: INetworkLayerAssembly + + public func assemble() -> IRequestProcessor { + RequestProcessor( + configuration: configure, + requestBuilder: requestBuilder, + dataRequestHandler: dataRequestHandler, + retryPolicyService: retryPolicyService + ) + } +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/Configuration.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/Configuration.swift new file mode 100644 index 0000000..1d929ac --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/Configuration.swift @@ -0,0 +1,43 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +/// A type that represents a configuration for the network layer. +public struct Configuration { + // MARK: Properties + + /// A configuration object that defines behavior and policies for a URL session. + public let sessionConfiguration: URLSessionConfiguration + /// A protocol that defines methods that URL session instances call on their delegates + /// to handle session-level events, like session life cycle changes. + public let sessionDelegate: URLSessionDelegate? + /// A queue that regulates the execution of operations. + public let sessionDelegateQueue: OperationQueue? + /// An object that decodes instances of a data type from JSON objects. + public let jsonDecoder: JSONDecoder + + // MARK: Initialization + + /// Creates a new `Configuration` instance. + /// + /// - Parameters: + /// - sessionConfiguration: A configuration object that defines behavior and policies for a URL session. + /// - sessionDelegate: A protocol that defines methods that URL session instances call on their + /// delegates to handle session-level events, like session life cycle changes. + /// - sessionDelegateQueue: A queue that regulates the execution of operations. + /// - jsonDecoder: An object that decodes instances of a data type from JSON objects. + public init( + sessionConfiguration: URLSessionConfiguration, + sessionDelegate: URLSessionDelegate?, + sessionDelegateQueue: OperationQueue?, + jsonDecoder: JSONDecoder + ) { + self.sessionConfiguration = sessionConfiguration + self.sessionDelegate = sessionDelegate + self.sessionDelegateQueue = sessionDelegateQueue + self.jsonDecoder = jsonDecoder + } +} diff --git a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IDataRequestHandler.swift similarity index 88% rename from Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift rename to Sources/NetworkLayerInterfaces/Classes/Core/Services/IDataRequestHandler.swift index f23cd83..8baa9d6 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/IDataRequestHandler.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IDataRequestHandler.swift @@ -4,10 +4,9 @@ // import Foundation -import NetworkLayerInterfaces /// A protocol for handling data requests. -protocol IDataRequestHandler: URLSessionTaskDelegate & URLSessionDataDelegate { +public protocol IDataRequestHandler: URLSessionTaskDelegate & URLSessionDataDelegate { /// Starts a data task for handling network requests. /// /// - Parameters: diff --git a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift index ac5b5c7..20cf72f 100644 --- a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift +++ b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift @@ -4,5 +4,26 @@ // import Foundation +import protocol Typhoon.IRetryPolicyService -public protocol INetworkLayerAssembly {} +/// A type that represents a network layer assembly. +public protocol INetworkLayerAssembly { + /// Creates a new `INetworkLayerAssembly` instance. + /// + /// - Parameters: + /// - configure: The network layer's configuration. + /// - requestBuilder: The request builder. + /// - dataRequestHandler: The data request handler. + /// - retryPolicyService: The retry policy service. + init( + configure: Configuration, + requestBuilder: IRequestBuilder, + dataRequestHandler: IDataRequestHandler, + retryPolicyService: IRetryPolicyService + ) + + /// Assembles a request processor. + /// + /// - Returns: A request processor. + func assemble() -> IRequestProcessor +} From e296b969d7085841d30524fa3f3a7f40811dc246 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Tue, 14 Nov 2023 17:15:15 +0300 Subject: [PATCH 07/26] Implement the methods of the `URLSessionDataDelegate` & `URLSessionTaskDelegate` delegates --- .../DataRequestHandler.swift | 84 ++++++++++++++++++- .../RequestProcessor/RequestProcessor.swift | 6 +- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift index 8d6dd0e..d89577a 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift @@ -60,17 +60,39 @@ extension DataRequestHandler { if handler.data == nil { handler.data = Data() } + handler.data?.append(data) } + + func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + userDataDelegate?.urlSession?(session, didBecomeInvalidWithError: error) + } + + func urlSession( + _ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse, + completionHandler: @escaping (CachedURLResponse?) -> Void + ) { + userDataDelegate?.urlSession?( + session, + dataTask: dataTask, + willCacheResponse: proposedResponse, + completionHandler: completionHandler + ) + completionHandler(proposedResponse) + } } // MARK: URLSessionTaskDelegate extension DataRequestHandler { - func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { guard let handler = handlers[task] else { return } handlers[task] = nil + userDataDelegate?.urlSession?(session, task: task, didCompleteWithError: error) + if let error = error { handler.completion?(.failure(error)) } else { @@ -83,6 +105,66 @@ extension DataRequestHandler { } } } + + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + userDataDelegate?.urlSession?(session, task: task, didFinishCollecting: metrics) + } + + func urlSession( + _ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void + ) { + userDataDelegate?.urlSession?( + session, + task: task, + willPerformHTTPRedirection: response, + newRequest: request, + completionHandler: completionHandler + ) + completionHandler(request) + } + + func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + userDataDelegate?.urlSession?(session, taskIsWaitingForConnectivity: task) + } + + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + userDataDelegate?.urlSession?(session, didReceive: challenge, completionHandler: completionHandler) + completionHandler(.performDefaultHandling, nil) + } + + func urlSession( + _ session: URLSession, + task: URLSessionTask, + willBeginDelayedRequest request: URLRequest, + completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void + ) { + userDataDelegate?.urlSession?(session, task: task, willBeginDelayedRequest: request, completionHandler: completionHandler) + completionHandler(.continueLoading, nil) + } + + func urlSession( + _ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64 + ) { + userDataDelegate?.urlSession?( + session, + task: task, + didSendBodyData: bytesSent, + totalBytesSent: totalBytesSent, + totalBytesExpectedToSend: totalBytesExpectedToSend + ) + } } // MARK: DataRequestHandler.DataTaskHandler diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index d363fc3..d80c7ef 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -66,12 +66,14 @@ actor RequestProcessor { _ request: T, strategy: RetryPolicyStrategy? = nil, delegate: URLSessionDelegate?, - configure _: ((inout URLRequest) throws -> Void)? + configure: ((inout URLRequest) throws -> Void)? ) async throws -> Response { - guard let request = requestBuilder.build(request) else { + guard var request = requestBuilder.build(request) else { throw NetworkLayerError.badURL } + try configure?(&request) + return try await performRequest(strategy: strategy) { let task = session.dataTask(with: request) From 2fd716a791d4f7ad9708bf14da4cd56c2632d04f Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Tue, 14 Nov 2023 17:19:04 +0300 Subject: [PATCH 08/26] Refactor method arguments --- .../Core/Services/RequestProcessor/RequestProcessor.swift | 4 +--- .../Classes/Core/Services/IRequestBuilder.swift | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index d80c7ef..b4045a9 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -68,12 +68,10 @@ actor RequestProcessor { delegate: URLSessionDelegate?, configure: ((inout URLRequest) throws -> Void)? ) async throws -> Response { - guard var request = requestBuilder.build(request) else { + guard let request = requestBuilder.build(request, configure) else { throw NetworkLayerError.badURL } - try configure?(&request) - return try await performRequest(strategy: strategy) { let task = session.dataTask(with: request) diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift index a2ad79f..00781f1 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift @@ -12,5 +12,5 @@ public protocol IRequestBuilder { /// - Parameter request: The request object that defines the request details. /// /// - Returns: A `URLRequest` constructed based on the given data. - func build(_ request: IRequest) -> URLRequest? + func build(_ request: IRequest, _ configure: ((inout URLRequest) throws -> Void)?) -> URLRequest? } From c8c6ab3ae89932366229da85abf34d3d76b1a793 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Wed, 15 Nov 2023 18:17:15 +0300 Subject: [PATCH 09/26] Implement `RequestProcessorDelegate` --- Package.swift | 1 + Package@swift-5.7.swift | 1 + .../RequestProcessor/RequestProcessor.swift | 9 ++- .../Classes/DI/NetworkLayerAssembly.swift | 9 ++- .../Services/RequestProcessorDelegate.swift | 16 +++++ .../Classes/DI/INetworkLayerAssembly.swift | 4 +- .../NetworkLayerTests/Fakes/RequestFake.swift | 41 ++++++++++++ .../Mocks/DataRequestHandlerMock.swift | 27 ++++++++ .../Mocks/RequestBuilderMock.swift | 27 ++++++++ .../Mocks/RequestProcessorDelegateMock.swift | 21 +++++++ .../Mocks/RetryPolicyServiceMock.swift | 23 +++++++ .../UnitTests/RequestProcessorTests.swift | 63 +++++++++++++++++++ 12 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift create mode 100644 Tests/NetworkLayerTests/Fakes/RequestFake.swift create mode 100644 Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift create mode 100644 Tests/NetworkLayerTests/Mocks/RequestBuilderMock.swift create mode 100644 Tests/NetworkLayerTests/Mocks/RequestProcessorDelegateMock.swift create mode 100644 Tests/NetworkLayerTests/Mocks/RetryPolicyServiceMock.swift create mode 100644 Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift diff --git a/Package.swift b/Package.swift index 608fe27..dee16f0 100644 --- a/Package.swift +++ b/Package.swift @@ -39,6 +39,7 @@ let package = Package( name: "NetworkLayerTests", dependencies: [ "NetworkLayer", + .product(name: "Typhoon", package: "typhoon"), ] ), ] diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index a718890..73b0944 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -36,6 +36,7 @@ let package = Package( name: "NetworkLayerTests", dependencies: [ "NetworkLayer", + .product(name: "Typhoon", package: "typhoon"), ] ), ] diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index b4045a9..b9464c2 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -24,6 +24,9 @@ actor RequestProcessor { /// The retry policy service. private let retryPolicyService: IRetryPolicyService + /// The delegate. + private weak var delegate: RequestProcessorDelegate? + // MARK: Initialization /// Creates a new `RequestProcessor` instance. @@ -37,12 +40,14 @@ actor RequestProcessor { configuration: Configuration, requestBuilder: IRequestBuilder, dataRequestHandler: any IDataRequestHandler, - retryPolicyService: IRetryPolicyService + retryPolicyService: IRetryPolicyService, + delegate: RequestProcessorDelegate? ) { self.configuration = configuration self.requestBuilder = requestBuilder self.dataRequestHandler = dataRequestHandler self.retryPolicyService = retryPolicyService + self.delegate = delegate session = URLSession( configuration: configuration.sessionConfiguration, delegate: dataRequestHandler, @@ -73,6 +78,8 @@ actor RequestProcessor { } return try await performRequest(strategy: strategy) { + try await self.delegate?.requestProcessor(self, willSendRequest: request) + let task = session.dataTask(with: request) do { diff --git a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift index f4291ca..db4965a 100644 --- a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift +++ b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift @@ -18,6 +18,8 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { private let dataRequestHandler: IDataRequestHandler /// The retry policy service. private let retryPolicyService: IRetryPolicyService + /// The request processor delegate. + private let delegate: RequestProcessorDelegate // MARK: Initialization @@ -25,12 +27,14 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { configure: Configuration, requestBuilder: IRequestBuilder, dataRequestHandler: IDataRequestHandler, - retryPolicyService: IRetryPolicyService + retryPolicyService: IRetryPolicyService, + delegate: RequestProcessorDelegate ) { self.configure = configure self.requestBuilder = requestBuilder self.dataRequestHandler = dataRequestHandler self.retryPolicyService = retryPolicyService + self.delegate = delegate } // MARK: INetworkLayerAssembly @@ -40,7 +44,8 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { configuration: configure, requestBuilder: requestBuilder, dataRequestHandler: dataRequestHandler, - retryPolicyService: retryPolicyService + retryPolicyService: retryPolicyService, + delegate: delegate ) } } diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift new file mode 100644 index 0000000..24f6ea2 --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift @@ -0,0 +1,16 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +/// A protocol to define the delegate methods for handling network requests. +public protocol RequestProcessorDelegate: AnyObject { + /// Notifies the delegate that the request processor is about to send a request. + /// + /// - Parameters: + /// - requestProcessor: The request processor responsible for handling the request. + /// - request: The URLRequest about to be sent. + func requestProcessor(_ requestProcessor: IRequestProcessor, willSendRequest request: URLRequest) async throws +} diff --git a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift index 20cf72f..0d71bd9 100644 --- a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift +++ b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift @@ -15,11 +15,13 @@ public protocol INetworkLayerAssembly { /// - requestBuilder: The request builder. /// - dataRequestHandler: The data request handler. /// - retryPolicyService: The retry policy service. + /// - delegate: The request processor delegate. init( configure: Configuration, requestBuilder: IRequestBuilder, dataRequestHandler: IDataRequestHandler, - retryPolicyService: IRetryPolicyService + retryPolicyService: IRetryPolicyService, + delegate: RequestProcessorDelegate ) /// Assembles a request processor. diff --git a/Tests/NetworkLayerTests/Fakes/RequestFake.swift b/Tests/NetworkLayerTests/Fakes/RequestFake.swift new file mode 100644 index 0000000..18ebe0f --- /dev/null +++ b/Tests/NetworkLayerTests/Fakes/RequestFake.swift @@ -0,0 +1,41 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class RequestStub: IRequest { + var domainName: String { + "http://google.com/" + } + + var path: String { + "path" + } + + var headers: [String: String]? { + [:] + } + + var parameters: [String: String]? { + [:] + } + + var requiresAuthentication: Bool { + false + } + + var timeoutInterval: TimeInterval { + 1.0 + } + + var httpMethod: NetworkLayerInterfaces.HTTPMethod { + .delete + } + + var httpBody: [String: Any]? { + nil + } +} diff --git a/Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift b/Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift new file mode 100644 index 0000000..a073272 --- /dev/null +++ b/Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift @@ -0,0 +1,27 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class DataRequestHandlerMock: NSObject, IDataRequestHandler { + var invokedStartDataTask = false + var invokedStartDataTaskCount = 0 + var invokedStartDataTaskParameters: (task: URLSessionDataTask, session: URLSession, delegate: URLSessionDelegate?)? + var invokedStartDataTaskParametersList = [(task: URLSessionDataTask, session: URLSession, delegate: URLSessionDelegate?)]() + var stubbedStartDataTask: Response! + + func startDataTask( + _ task: URLSessionDataTask, + session: URLSession, + delegate: URLSessionDelegate? + ) async throws -> Response { + invokedStartDataTask = true + invokedStartDataTaskCount += 1 + invokedStartDataTaskParameters = (task, session, delegate) + invokedStartDataTaskParametersList.append((task, session, delegate)) + return stubbedStartDataTask + } +} diff --git a/Tests/NetworkLayerTests/Mocks/RequestBuilderMock.swift b/Tests/NetworkLayerTests/Mocks/RequestBuilderMock.swift new file mode 100644 index 0000000..bb845c2 --- /dev/null +++ b/Tests/NetworkLayerTests/Mocks/RequestBuilderMock.swift @@ -0,0 +1,27 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class RequestBuilderMock: IRequestBuilder { + var invokedBuild = false + var invokedBuildCount = 0 + var invokedBuildParameters: (request: IRequest, Void)? + var invokedBuildParametersList = [(request: IRequest, Void)]() + var stubbedBuildConfigureResult: (URLRequest, Void)? + var stubbedBuildResult: URLRequest! + + func build(_ request: IRequest, _ configure: ((inout URLRequest) throws -> Void)?) -> URLRequest? { + invokedBuild = true + invokedBuildCount += 1 + invokedBuildParameters = (request, ()) + invokedBuildParametersList.append((request, ())) + if var result = stubbedBuildConfigureResult { + try? configure?(&result.0) + } + return stubbedBuildResult + } +} diff --git a/Tests/NetworkLayerTests/Mocks/RequestProcessorDelegateMock.swift b/Tests/NetworkLayerTests/Mocks/RequestProcessorDelegateMock.swift new file mode 100644 index 0000000..33e6be4 --- /dev/null +++ b/Tests/NetworkLayerTests/Mocks/RequestProcessorDelegateMock.swift @@ -0,0 +1,21 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class RequestProcessorDelegateMock: RequestProcessorDelegate { + var invokedRequestProcessor = false + var invokedRequestProcessorCount = 0 + var invokedRequestProcessorParameters: (requestProcessor: IRequestProcessor, request: URLRequest)? + var invokedRequestProcessorParametersList = [(requestProcessor: IRequestProcessor, request: URLRequest)]() + + func requestProcessor(_ requestProcessor: IRequestProcessor, willSendRequest request: URLRequest) async throws { + invokedRequestProcessor = true + invokedRequestProcessorCount += 1 + invokedRequestProcessorParameters = (requestProcessor, request) + invokedRequestProcessorParametersList.append((requestProcessor, request)) + } +} diff --git a/Tests/NetworkLayerTests/Mocks/RetryPolicyServiceMock.swift b/Tests/NetworkLayerTests/Mocks/RetryPolicyServiceMock.swift new file mode 100644 index 0000000..f4c1c97 --- /dev/null +++ b/Tests/NetworkLayerTests/Mocks/RetryPolicyServiceMock.swift @@ -0,0 +1,23 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import Typhoon + +final class RetryPolicyServiceMock: IRetryPolicyService { + var invokedRetry = false + var invokedRetryCount = 0 + var invokedRetryParameters: (strategy: RetryPolicyStrategy?, closure: Void)? + var invokedRetryParametersList = [(strategy: RetryPolicyStrategy?, closure: Void)]() + var stubbedRetry: T! + + func retry(strategy: RetryPolicyStrategy?, _: () async throws -> T) async throws -> T { + invokedRetry = true + invokedRetryCount += 1 + invokedRetryParameters = (strategy, ()) + invokedRetryParametersList.append((strategy, ())) + return stubbedRetry as! T + } +} diff --git a/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift b/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift new file mode 100644 index 0000000..06487da --- /dev/null +++ b/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift @@ -0,0 +1,63 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +@testable import NetworkLayer +import NetworkLayerInterfaces +import XCTest + +final class RequestProcessorTests: XCTestCase { + // MARK: Properties + + private var requestBuilderMock: RequestBuilderMock! + private var dataRequestHandler: DataRequestHandlerMock! + private var retryPolicyMock: RetryPolicyServiceMock! + private var delegateMock: RequestProcessorDelegateMock! + + private var sut: RequestProcessor! + + // MARK: XCTestCase + + override func setUp() { + super.setUp() + requestBuilderMock = RequestBuilderMock() + dataRequestHandler = DataRequestHandlerMock() + retryPolicyMock = RetryPolicyServiceMock() + delegateMock = RequestProcessorDelegateMock() + sut = RequestProcessor( + configuration: Configuration( + sessionConfiguration: .default, + sessionDelegate: nil, + sessionDelegateQueue: nil, + jsonDecoder: JSONDecoder() + ), + requestBuilder: requestBuilderMock, + dataRequestHandler: dataRequestHandler, + retryPolicyService: retryPolicyMock, + delegate: delegateMock + ) + } + + override func tearDown() { + requestBuilderMock = nil + dataRequestHandler = nil + retryPolicyMock = nil + delegateMock = nil + sut = nil + super.tearDown() + } + + // MARK: Tests + + func test_thatRequestProcessorInvokesDelegate_whenRequestWillPerform() async throws { + // given + let request = RequestStub() + + // when + let _ = try await sut.send(request) as Int + + // then +// XCTAssertEqual(request.re, <#T##expression2: Equatable##Equatable#>) + } +} From 9f7218be382519f40f92c9bd970538a2a28a6970 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sat, 18 Nov 2023 22:01:15 +0300 Subject: [PATCH 10/26] Implement `AuthenticatorInterceptor` --- .../xcschemes/NetworkLayer-Package.xcscheme | 116 ++++++++++++++ .../xcschemes/NetworkLayer.xcscheme | 67 ++++++++ .../xcschemes/NetworkLayerInterfaces.xcscheme | 67 ++++++++ .../AuthenticatorInterceptor.swift | 69 +++++++++ .../DataRequestHandler.swift | 2 +- .../RequestProcessor/RequestProcessor.swift | 41 ++++- .../Classes/DI/NetworkLayerAssembly.swift | 9 +- .../AuthenticatorInterceptorError.swift | 12 ++ .../IAuthenticationCredential.swift | 12 ++ .../Core/Authenticator/IAuthenticator.swift | 44 ++++++ .../IAuthenticatorInterceptor.swift | 24 +++ .../Classes/Core/Models/Response.swift | 6 +- .../Classes/DI/INetworkLayerAssembly.swift | 4 +- .../NetworkLayerTests/Fakes/RequestFake.swift | 41 ----- .../Fakes/URLRequest+Fake.swift | 12 ++ .../Mocks/AuthenticatorMock.swift | 65 ++++++++ .../AuthentificatorInterceptorMock.swift | 33 ++++ .../Mocks/DataRequestHandlerMock.swift | 4 + .../NetworkLayerTests/Mocks/RequestMock.swift | 89 +++++++++++ .../Mocks/RetryPolicyServiceMock.swift | 23 --- .../Stubs/AuthenticationCredentialStub.swift | 15 ++ .../AuthenticationInterceptorTests.swift | 146 ++++++++++++++++++ .../UnitTests/NetworkLayerTests.swift | 22 --- .../UnitTests/RequestProcessorTests.swift | 85 +++++++++- 24 files changed, 903 insertions(+), 105 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme create mode 100644 Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/AuthenticatorInterceptorError.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticationCredential.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift delete mode 100644 Tests/NetworkLayerTests/Fakes/RequestFake.swift create mode 100644 Tests/NetworkLayerTests/Fakes/URLRequest+Fake.swift create mode 100644 Tests/NetworkLayerTests/Mocks/AuthenticatorMock.swift create mode 100644 Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift create mode 100644 Tests/NetworkLayerTests/Mocks/RequestMock.swift delete mode 100644 Tests/NetworkLayerTests/Mocks/RetryPolicyServiceMock.swift create mode 100644 Tests/NetworkLayerTests/Stubs/AuthenticationCredentialStub.swift create mode 100644 Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift delete mode 100644 Tests/NetworkLayerTests/UnitTests/NetworkLayerTests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme new file mode 100644 index 0000000..cabe72c --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme new file mode 100644 index 0000000..914e95b --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme new file mode 100644 index 0000000..2395097 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift new file mode 100644 index 0000000..7782c85 --- /dev/null +++ b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift @@ -0,0 +1,69 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Atomic +import Foundation +import NetworkLayerInterfaces + +/// <#Description#> +public final class AuthenticatorInterceptor: IAuthenticatorInterceptor { + // MARK: Types + + public typealias Credential = Authenticator.Credential + + // MARK: Private + + private let authenticator: Authenticator + @Atomic public var credential: Credential? + + // MARK: Initialization + + public init(authenticator: Authenticator, credential: Credential? = nil) { + self.authenticator = authenticator + self.credential = credential + } + + // MARK: IAuthentificatorInterceptor + + public func adapt(request: inout URLRequest, for session: URLSession) async throws { + guard let credential else { + throw AuthenticatorInterceptorError.missingCredential + } + + if credential.requiresRefresh { + try await refresh(credential, for: session) + } else { + try await authenticator.apply(credential, to: &request) + } + } + + public func refresh( + _ request: URLRequest, + with response: HTTPURLResponse, + for session: URLSession, + dutTo error: Error + ) async throws { + guard authenticator.didRequest(request, with: response, failDueToAuthenticationError: error) else { + return + } + + guard let credential = credential else { + throw AuthenticatorInterceptorError.missingCredential + } + + guard authenticator.isRequest(request, authenticatedWith: credential) else { + return + } + + try await refresh(credential, for: session) + } + + // MARK: Private + + private func refresh(_ credential: Credential, for session: URLSession) async throws { + let credential = try await authenticator.refresh(credential, for: session) + self.credential = credential + } +} diff --git a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift index d89577a..7b6e102 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift @@ -98,7 +98,7 @@ extension DataRequestHandler { } else { if let response = task.response { let data = handler.data ?? Data() - let response = Response(data: data, response: response) + let response = Response(data: data, response: response, task: task) handler.completion?(.success(response)) } else { handler.completion?(.failure(URLError(.unknown))) diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index b9464c2..27dcb4e 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -23,7 +23,8 @@ actor RequestProcessor { private let requestBuilder: IRequestBuilder /// The retry policy service. private let retryPolicyService: IRetryPolicyService - + /// The authenticator interceptor. + private let interceptor: IAuthenticatorInterceptor? /// The delegate. private weak var delegate: RequestProcessorDelegate? @@ -41,13 +42,15 @@ actor RequestProcessor { requestBuilder: IRequestBuilder, dataRequestHandler: any IDataRequestHandler, retryPolicyService: IRetryPolicyService, - delegate: RequestProcessorDelegate? + delegate: RequestProcessorDelegate?, + interceptor: IAuthenticatorInterceptor? ) { self.configuration = configuration self.requestBuilder = requestBuilder self.dataRequestHandler = dataRequestHandler self.retryPolicyService = retryPolicyService self.delegate = delegate + self.interceptor = interceptor session = URLSession( configuration: configuration.sessionConfiguration, delegate: dataRequestHandler, @@ -73,23 +76,47 @@ actor RequestProcessor { delegate: URLSessionDelegate?, configure: ((inout URLRequest) throws -> Void)? ) async throws -> Response { - guard let request = requestBuilder.build(request, configure) else { + guard var urlRequest = requestBuilder.build(request, configure) else { throw NetworkLayerError.badURL } return try await performRequest(strategy: strategy) { - try await self.delegate?.requestProcessor(self, willSendRequest: request) + try await self.delegate?.requestProcessor(self, willSendRequest: urlRequest) + try await self.adapt(request, urlRequest: &urlRequest, session: session) - let task = session.dataTask(with: request) + let task = session.dataTask(with: urlRequest) do { return try await dataRequestHandler.startDataTask(task, session: session, delegate: delegate) } catch { + try await refreshCredentialIfNeeded( + request, + urlRequest: &urlRequest, + response: HTTPURLResponse(), + session: session, + error: error + ) throw error } } } + private func adapt(_ request: T, urlRequest: inout URLRequest, session: URLSession) async throws { + guard request.requiresAuthentication else { return } + try await interceptor?.adapt(request: &urlRequest, for: session) + } + + private func refreshCredentialIfNeeded( + _ request: T, + urlRequest: inout URLRequest, + response: HTTPURLResponse, + session: URLSession, + error: Error + ) async throws { + guard request.requiresAuthentication else { return } + try await interceptor?.refresh(urlRequest, with: response, for: session, dutTo: error) + } + /// Performs a request with a retry policy. /// /// - Parameters: @@ -110,11 +137,11 @@ actor RequestProcessor { extension RequestProcessor: IRequestProcessor { func send( _ request: T, - strategy _: RetryPolicyStrategy? = nil, + strategy: RetryPolicyStrategy? = nil, delegate: URLSessionDelegate? = nil, configure: ((inout URLRequest) throws -> Void)? = nil ) async throws -> M { - let response = try await performRequest(request, delegate: delegate, configure: configure) + let response = try await performRequest(request, strategy: strategy, delegate: delegate, configure: configure) let item = try configuration.jsonDecoder.decode(M.self, from: response.data) return item } diff --git a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift index db4965a..789a178 100644 --- a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift +++ b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift @@ -20,6 +20,8 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { private let retryPolicyService: IRetryPolicyService /// The request processor delegate. private let delegate: RequestProcessorDelegate + /// The authenticator interceptor. + private let interceptor: IAuthenticatorInterceptor? // MARK: Initialization @@ -28,13 +30,15 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { requestBuilder: IRequestBuilder, dataRequestHandler: IDataRequestHandler, retryPolicyService: IRetryPolicyService, - delegate: RequestProcessorDelegate + delegate: RequestProcessorDelegate, + interceptor: IAuthenticatorInterceptor? ) { self.configure = configure self.requestBuilder = requestBuilder self.dataRequestHandler = dataRequestHandler self.retryPolicyService = retryPolicyService self.delegate = delegate + self.interceptor = interceptor } // MARK: INetworkLayerAssembly @@ -45,7 +49,8 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { requestBuilder: requestBuilder, dataRequestHandler: dataRequestHandler, retryPolicyService: retryPolicyService, - delegate: delegate + delegate: delegate, + interceptor: interceptor ) } } diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/AuthenticatorInterceptorError.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/AuthenticatorInterceptorError.swift new file mode 100644 index 0000000..d189575 --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/AuthenticatorInterceptorError.swift @@ -0,0 +1,12 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +/// `AuthenticatorInterceptorError` is the error type returned by AuthenticatorInterceptor. +public enum AuthenticatorInterceptorError: Swift.Error { + /// The credential was not found. + case missingCredential +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticationCredential.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticationCredential.swift new file mode 100644 index 0000000..192e55c --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticationCredential.swift @@ -0,0 +1,12 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +/// A type defines an authentication credential interface. +public protocol IAuthenticationCredential { + /// Determines whether the authentication credential requires a refresh. + var requiresRefresh: Bool { get } +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift new file mode 100644 index 0000000..68bbde9 --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift @@ -0,0 +1,44 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +/// <#Description#> +public protocol IAuthenticator { + associatedtype Credential: IAuthenticationCredential + + /// Applies the `Credential` to the `URLRequest`. + /// + /// - Parameters: + /// - credential: The `Credential`. + /// - urlRequest: The `URLRequest`. + func apply(_ credential: Credential, to urlRequest: inout URLRequest) async throws + + /// Refreshes the `Credential`. + /// + /// - Parameters: + /// - credential: The `Credential` to refresh. + /// - session: The `URLSession` requiring the refresh. + func refresh(_ credential: Credential, for session: URLSession) async throws -> Credential + + /// Determines whether the `URLRequest` failed due to an authentication error based on the `HTTPURLResponse`. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest`. + /// - response: The `HTTPURLResponse`. + /// - error: The `Error`. + /// + /// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise. + func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool + + /// Determines whether the `URLRequest` is authenticated with the `Credential`. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest`. + /// - credential: The `Credential`. + /// + /// - Returns: `true` if the `URLRequest` is authenticated with the `Credential`, `false` otherwise. + func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift new file mode 100644 index 0000000..ed4c9a5 --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift @@ -0,0 +1,24 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +/// A type defines the authenticator interceptor interface. +public protocol IAuthenticatorInterceptor { + /// Adapts the request with credentials. + /// + /// - Parameters: + /// - request: The URLRequest to be adapted. + /// - session: The URLSession for which the request is being adapted. + func adapt(request: inout URLRequest, for session: URLSession) async throws + + /// Refreshes credential for the request. + /// + /// - Parameters: + /// - request: The URLRequest to be refreshed. + /// - session: The URLSession for which the request is being refreshed. + /// - error: The error occurred during the request. + func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession, dutTo error: Error) async throws +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift index 4320e99..d54dfe2 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift @@ -8,9 +8,13 @@ import Foundation public struct Response { public let data: T public let response: URLResponse + public let task: URLSessionTask + public let statusCode: Int? - public init(data: T, response: URLResponse) { + public init(data: T, response: URLResponse, task: URLSessionTask) { self.data = data self.response = response + statusCode = (response as? HTTPURLResponse)?.statusCode + self.task = task } } diff --git a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift index 0d71bd9..c8a0ae6 100644 --- a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift +++ b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift @@ -16,12 +16,14 @@ public protocol INetworkLayerAssembly { /// - dataRequestHandler: The data request handler. /// - retryPolicyService: The retry policy service. /// - delegate: The request processor delegate. + /// - interceptor: The authenticator interceptor. init( configure: Configuration, requestBuilder: IRequestBuilder, dataRequestHandler: IDataRequestHandler, retryPolicyService: IRetryPolicyService, - delegate: RequestProcessorDelegate + delegate: RequestProcessorDelegate, + interceptor: IAuthenticatorInterceptor? ) /// Assembles a request processor. diff --git a/Tests/NetworkLayerTests/Fakes/RequestFake.swift b/Tests/NetworkLayerTests/Fakes/RequestFake.swift deleted file mode 100644 index 18ebe0f..0000000 --- a/Tests/NetworkLayerTests/Fakes/RequestFake.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// network-layer -// Copyright © 2023 Space Code. All rights reserved. -// - -import Foundation -import NetworkLayerInterfaces - -final class RequestStub: IRequest { - var domainName: String { - "http://google.com/" - } - - var path: String { - "path" - } - - var headers: [String: String]? { - [:] - } - - var parameters: [String: String]? { - [:] - } - - var requiresAuthentication: Bool { - false - } - - var timeoutInterval: TimeInterval { - 1.0 - } - - var httpMethod: NetworkLayerInterfaces.HTTPMethod { - .delete - } - - var httpBody: [String: Any]? { - nil - } -} diff --git a/Tests/NetworkLayerTests/Fakes/URLRequest+Fake.swift b/Tests/NetworkLayerTests/Fakes/URLRequest+Fake.swift new file mode 100644 index 0000000..fd02280 --- /dev/null +++ b/Tests/NetworkLayerTests/Fakes/URLRequest+Fake.swift @@ -0,0 +1,12 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +extension URLRequest { + static func fake() -> URLRequest { + URLRequest(url: URL(string: "https://google.com/")!) + } +} diff --git a/Tests/NetworkLayerTests/Mocks/AuthenticatorMock.swift b/Tests/NetworkLayerTests/Mocks/AuthenticatorMock.swift new file mode 100644 index 0000000..a457382 --- /dev/null +++ b/Tests/NetworkLayerTests/Mocks/AuthenticatorMock.swift @@ -0,0 +1,65 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class AuthenticatorMock: IAuthenticator { + typealias Credential = AuthenticationCredentialStub + + var invokedApply = false + var invokedApplyCount = 0 + var invokedApplyParameters: (credential: Credential, urlRequest: URLRequest)? + var invokedApplyParametersList = [(credential: Credential, urlRequest: URLRequest)]() + + func apply(_ credential: Credential, to urlRequest: inout URLRequest) async throws { + invokedApply = true + invokedApplyCount += 1 + invokedApplyParameters = (credential, urlRequest) + invokedApplyParametersList.append((credential, urlRequest)) + } + + var invokedRefresh = false + var invokedRefreshCount = 0 + var invokedRefreshParameters: (credential: Credential, session: URLSession)? + var invokedRefreshParametersList = [(credential: Credential, session: URLSession)]() + var stubbedRefresh: Credential! + + func refresh(_ credential: Credential, for session: URLSession) async throws -> Credential { + invokedRefresh = true + invokedRefreshCount += 1 + invokedRefreshParameters = (credential, session) + invokedRefreshParametersList.append((credential, session)) + return stubbedRefresh + } + + var invokedDidRequest = false + var invokedDidRequestCount = 0 + var invokedDidRequestParameters: (urlRequest: URLRequest, response: HTTPURLResponse, error: Error)? + var invokedDidRequestParametersList = [(urlRequest: URLRequest, response: HTTPURLResponse, error: Error)]() + var stubbedDidRequestResult: Bool! = false + + func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool { + invokedDidRequest = true + invokedDidRequestCount += 1 + invokedDidRequestParameters = (urlRequest, response, error) + invokedDidRequestParametersList.append((urlRequest, response, error)) + return stubbedDidRequestResult + } + + var invokedIsRequest = false + var invokedIsRequestCount = 0 + var invokedIsRequestParameters: (urlRequest: URLRequest, credential: Credential)? + var invokedIsRequestParametersList = [(urlRequest: URLRequest, credential: Credential)]() + var stubbedIsRequestResult: Bool! = false + + func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool { + invokedIsRequest = true + invokedIsRequestCount += 1 + invokedIsRequestParameters = (urlRequest, credential) + invokedIsRequestParametersList.append((urlRequest, credential)) + return stubbedIsRequestResult + } +} diff --git a/Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift b/Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift new file mode 100644 index 0000000..65c2158 --- /dev/null +++ b/Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift @@ -0,0 +1,33 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class AuthentificatorInterceptorMock: IAuthenticatorInterceptor { + var invokedAdapt = false + var invokedAdaptCount = 0 + var invokedAdaptParameters: (request: URLRequest, session: URLSession)? + var invokedAdaptParametersList = [(request: URLRequest, session: URLSession)]() + + func adapt(request: inout URLRequest, for session: URLSession) async throws { + invokedAdapt = true + invokedAdaptCount += 1 + invokedAdaptParameters = (request, session) + invokedAdaptParametersList.append((request, session)) + } + + var invokedRefresh = false + var invokedRefreshCount = 0 + var invokedRefreshParameters: (request: URLRequest, response: HTTPURLResponse, session: URLSession, error: Error)? + var invokedRefreshParametersList = [(request: URLRequest, response: HTTPURLResponse, session: URLSession, error: Error)]() + + func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession, dutTo error: Error) async throws { + invokedRefresh = true + invokedRefreshCount += 1 + invokedRefreshParameters = (request, response, session, error) + invokedRefreshParametersList.append((request, response, session, error)) + } +} diff --git a/Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift b/Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift index a073272..3d3c379 100644 --- a/Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift +++ b/Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift @@ -12,6 +12,7 @@ final class DataRequestHandlerMock: NSObject, IDataRequestHandler { var invokedStartDataTaskParameters: (task: URLSessionDataTask, session: URLSession, delegate: URLSessionDelegate?)? var invokedStartDataTaskParametersList = [(task: URLSessionDataTask, session: URLSession, delegate: URLSessionDelegate?)]() var stubbedStartDataTask: Response! + var startDataTaskThrowError: Error? func startDataTask( _ task: URLSessionDataTask, @@ -22,6 +23,9 @@ final class DataRequestHandlerMock: NSObject, IDataRequestHandler { invokedStartDataTaskCount += 1 invokedStartDataTaskParameters = (task, session, delegate) invokedStartDataTaskParametersList.append((task, session, delegate)) + if let error = startDataTaskThrowError { + throw error + } return stubbedStartDataTask } } diff --git a/Tests/NetworkLayerTests/Mocks/RequestMock.swift b/Tests/NetworkLayerTests/Mocks/RequestMock.swift new file mode 100644 index 0000000..36cc932 --- /dev/null +++ b/Tests/NetworkLayerTests/Mocks/RequestMock.swift @@ -0,0 +1,89 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class RequestMock: IRequest { + var invokedDomainNameGetter = false + var invokedDomainNameGetterCount = 0 + var stubbedDomainName: String! = "" + + var domainName: String { + invokedDomainNameGetter = true + invokedDomainNameGetterCount += 1 + return stubbedDomainName + } + + var invokedPathGetter = false + var invokedPathGetterCount = 0 + var stubbedPath: String! = "" + + var path: String { + invokedPathGetter = true + invokedPathGetterCount += 1 + return stubbedPath + } + + var invokedHeadersGetter = false + var invokedHeadersGetterCount = 0 + var stubbedHeaders: [String: String]! + + var headers: [String: String]? { + invokedHeadersGetter = true + invokedHeadersGetterCount += 1 + return stubbedHeaders + } + + var invokedParametersGetter = false + var invokedParametersGetterCount = 0 + var stubbedParameters: [String: String]! + + var parameters: [String: String]? { + invokedParametersGetter = true + invokedParametersGetterCount += 1 + return stubbedParameters + } + + var invokedRequiresAuthenticationGetter = false + var invokedRequiresAuthenticationGetterCount = 0 + var stubbedRequiresAuthentication: Bool! = false + + var requiresAuthentication: Bool { + invokedRequiresAuthenticationGetter = true + invokedRequiresAuthenticationGetterCount += 1 + return stubbedRequiresAuthentication + } + + var invokedTimeoutIntervalGetter = false + var invokedTimeoutIntervalGetterCount = 0 + var stubbedTimeoutInterval: TimeInterval! + + var timeoutInterval: TimeInterval { + invokedTimeoutIntervalGetter = true + invokedTimeoutIntervalGetterCount += 1 + return stubbedTimeoutInterval + } + + var invokedHttpMethodGetter = false + var invokedHttpMethodGetterCount = 0 + var stubbedHttpMethod: HTTPMethod! + + var httpMethod: HTTPMethod { + invokedHttpMethodGetter = true + invokedHttpMethodGetterCount += 1 + return stubbedHttpMethod + } + + var invokedHttpBodyGetter = false + var invokedHttpBodyGetterCount = 0 + var stubbedHttpBody: [String: Any]! + + var httpBody: [String: Any]? { + invokedHttpBodyGetter = true + invokedHttpBodyGetterCount += 1 + return stubbedHttpBody + } +} diff --git a/Tests/NetworkLayerTests/Mocks/RetryPolicyServiceMock.swift b/Tests/NetworkLayerTests/Mocks/RetryPolicyServiceMock.swift deleted file mode 100644 index f4c1c97..0000000 --- a/Tests/NetworkLayerTests/Mocks/RetryPolicyServiceMock.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// network-layer -// Copyright © 2023 Space Code. All rights reserved. -// - -import Foundation -import Typhoon - -final class RetryPolicyServiceMock: IRetryPolicyService { - var invokedRetry = false - var invokedRetryCount = 0 - var invokedRetryParameters: (strategy: RetryPolicyStrategy?, closure: Void)? - var invokedRetryParametersList = [(strategy: RetryPolicyStrategy?, closure: Void)]() - var stubbedRetry: T! - - func retry(strategy: RetryPolicyStrategy?, _: () async throws -> T) async throws -> T { - invokedRetry = true - invokedRetryCount += 1 - invokedRetryParameters = (strategy, ()) - invokedRetryParametersList.append((strategy, ())) - return stubbedRetry as! T - } -} diff --git a/Tests/NetworkLayerTests/Stubs/AuthenticationCredentialStub.swift b/Tests/NetworkLayerTests/Stubs/AuthenticationCredentialStub.swift new file mode 100644 index 0000000..9e9b4f3 --- /dev/null +++ b/Tests/NetworkLayerTests/Stubs/AuthenticationCredentialStub.swift @@ -0,0 +1,15 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class AuthenticationCredentialStub: IAuthenticationCredential { + var stubbedRequiresRefresh: Bool! = false + + var requiresRefresh: Bool { + stubbedRequiresRefresh + } +} diff --git a/Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift b/Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift new file mode 100644 index 0000000..2041bd2 --- /dev/null +++ b/Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift @@ -0,0 +1,146 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import NetworkLayer +import NetworkLayerInterfaces +import XCTest + +final class AuthenticationInterceptorTests: XCTestCase { + // MARK: Properties + + private var authenticatorMock: AuthenticatorMock! + + private var sut: AuthenticatorInterceptor! + + // MARK: XCTestCase + + override func setUp() { + super.setUp() + authenticatorMock = AuthenticatorMock() + sut = AuthenticatorInterceptor( + authenticator: authenticatorMock, + credential: nil + ) + } + + override func tearDown() { + authenticatorMock = nil + sut = nil + super.tearDown() + } + + // MARK: Tests + + func test_thatAuthenticatorInterceptorThrowsAnErrorOnAdaptRequest_whenCredentialIsMissing() async throws { + // given + var requestMock = URLRequest.fake() + + // when + var receivedError: NSError? + do { + try await sut.adapt(request: &requestMock, for: .shared) + } catch { + receivedError = error as NSError + } + + // then + XCTAssertEqual(receivedError, AuthenticatorInterceptorError.missingCredential as NSError) + } + + func test_thatAuthenticatorInterceptorAdaptsRequest_whenCredentialIsNotMissingAndValid() async throws { + // given + let credentialStub = AuthenticationCredentialStub() + var requestMock = URLRequest.fake() + credentialStub.stubbedRequiresRefresh = false + + sut.credential = credentialStub + + // when + try await sut.adapt(request: &requestMock, for: .shared) + + // then + XCTAssertEqual(authenticatorMock.invokedApplyCount, 1) + } + + func test_thatAuthenticatorInterceptorAdaptsRequest_whenCredentialIsNotMissingAndNotValid() async throws { + // given + var requestMock = URLRequest.fake() + + let credentialStub = AuthenticationCredentialStub() + credentialStub.stubbedRequiresRefresh = true + + authenticatorMock.stubbedRefresh = AuthenticationCredentialStub() + sut.credential = credentialStub + + // when + try await sut.adapt(request: &requestMock, for: .shared) + + // then + XCTAssertEqual(authenticatorMock.invokedRefreshCount, 1) + } + + func test_thatAuthenticatorInterceptorRefreshesCredential() async throws { + // given + let requestMock = URLRequest.fake() + + sut.credential = AuthenticationCredentialStub() + authenticatorMock.stubbedRefresh = AuthenticationCredentialStub() + authenticatorMock.stubbedDidRequestResult = true + authenticatorMock.stubbedIsRequestResult = true + + // when + try await sut.refresh(requestMock, with: .init(), for: .shared, dutTo: URLError(.unknown)) + + // then + XCTAssertEqual(authenticatorMock.invokedRefreshCount, 1) + } + + func test_thatAuthenticatorInterceptorDoesNotRefreshCredential_whenRequestDidNotFailDueToAuthenticationError() async throws { + // given + let requestMock = URLRequest.fake() + + authenticatorMock.stubbedDidRequestResult = false + + // when + try await sut.refresh(requestMock, with: .init(), for: .shared, dutTo: URLError(.unknown)) + + // then + XCTAssertFalse(authenticatorMock.invokedRefresh) + XCTAssertEqual(authenticatorMock.invokedDidRequestCount, 1) + } + + func test_thatAuthenticatorInterceptorThrowsCredentialIsMissingError_whenCredentialIsNil() async throws { + // given + let requestMock = URLRequest.fake() + + authenticatorMock.stubbedDidRequestResult = true + + // when + var receivedError: NSError? + do { + try await sut.refresh(requestMock, with: .init(), for: .shared, dutTo: URLError(.unknown)) + } catch { + receivedError = error as NSError + } + + // then + XCTAssertFalse(authenticatorMock.invokedRefresh) + XCTAssertEqual(receivedError, AuthenticatorInterceptorError.missingCredential as NSError) + } + + func test_thatAuthenticatorInterceptorDoesNotRefreshCredential_whenRequestIsNotAuthenticatedWithCredential() async throws { + // given + let requestMock = URLRequest.fake() + + sut.credential = AuthenticationCredentialStub() + authenticatorMock.stubbedDidRequestResult = true + + // when + try await sut.refresh(requestMock, with: .init(), for: .shared, dutTo: URLError(.unknown)) + + // then + XCTAssertFalse(authenticatorMock.invokedRefresh) + } +} diff --git a/Tests/NetworkLayerTests/UnitTests/NetworkLayerTests.swift b/Tests/NetworkLayerTests/UnitTests/NetworkLayerTests.swift deleted file mode 100644 index a163b6f..0000000 --- a/Tests/NetworkLayerTests/UnitTests/NetworkLayerTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// network-layer -// Copyright © 2023 Space Code. All rights reserved. -// - -import Foundation -@testable import NetworkLayer -import XCTest - -// MARK: - RequestProcessorTests - -final class NetworkLayerTests: XCTestCase { - // MARK: XCTestCase - - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } -} diff --git a/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift b/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift index 06487da..3279163 100644 --- a/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift +++ b/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift @@ -5,6 +5,7 @@ @testable import NetworkLayer import NetworkLayerInterfaces +import Typhoon import XCTest final class RequestProcessorTests: XCTestCase { @@ -12,8 +13,9 @@ final class RequestProcessorTests: XCTestCase { private var requestBuilderMock: RequestBuilderMock! private var dataRequestHandler: DataRequestHandlerMock! - private var retryPolicyMock: RetryPolicyServiceMock! + private var retryPolicyMock: RetryPolicyService! private var delegateMock: RequestProcessorDelegateMock! + private var interceptorMock: AuthentificatorInterceptorMock! private var sut: RequestProcessor! @@ -23,8 +25,14 @@ final class RequestProcessorTests: XCTestCase { super.setUp() requestBuilderMock = RequestBuilderMock() dataRequestHandler = DataRequestHandlerMock() - retryPolicyMock = RetryPolicyServiceMock() + retryPolicyMock = RetryPolicyService( + strategy: .constant( + retry: 5, + duration: .seconds(.zero) + ) + ) delegateMock = RequestProcessorDelegateMock() + interceptorMock = AuthentificatorInterceptorMock() sut = RequestProcessor( configuration: Configuration( sessionConfiguration: .default, @@ -35,7 +43,8 @@ final class RequestProcessorTests: XCTestCase { requestBuilder: requestBuilderMock, dataRequestHandler: dataRequestHandler, retryPolicyService: retryPolicyMock, - delegate: delegateMock + delegate: delegateMock, + interceptor: interceptorMock ) } @@ -44,20 +53,82 @@ final class RequestProcessorTests: XCTestCase { dataRequestHandler = nil retryPolicyMock = nil delegateMock = nil + interceptorMock = nil sut = nil super.tearDown() } // MARK: Tests - func test_thatRequestProcessorInvokesDelegate_whenRequestWillPerform() async throws { + func test_thatRequestProcessorSignsRequest_whenRequestRequiresAuthentication() async { + // given + requestBuilderMock.stubbedBuildResult = URLRequest.fake() + dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: .init(), task: URLSessionTask()) + + let request = RequestMock() + request.stubbedRequiresAuthentication = true + + // when + do { + let _ = try await sut.send(request) as Int + } catch {} + + // then + XCTAssertTrue(interceptorMock.invokedAdapt) + XCTAssertFalse(interceptorMock.invokedRefresh) + } + + func test_thatRequestProcessorDoesNotSignRequest_whenRequestDoesNotRequireAuthentication() async { + // given + requestBuilderMock.stubbedBuildResult = URLRequest.fake() + dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: .init(), task: URLSessionTask()) + + let request = RequestMock() + request.stubbedRequiresAuthentication = false + + // when + do { + let _ = try await sut.send(request) as Int + } catch {} + + // then + XCTAssertFalse(interceptorMock.invokedAdapt) + XCTAssertFalse(interceptorMock.invokedRefresh) + } + + func test_thatRequestProcessorRefreshesCredential_whenCredentialIsNotValid() async { // given - let request = RequestStub() + requestBuilderMock.stubbedBuildResult = URLRequest.fake() + dataRequestHandler.startDataTaskThrowError = URLError(.unknown) + + let request = RequestMock() + request.stubbedRequiresAuthentication = true + + // when + do { + let _ = try await sut.send(request) as Int + } catch {} + + // then + XCTAssertTrue(interceptorMock.invokedAdapt) + XCTAssertTrue(interceptorMock.invokedRefresh) + } + + func test_thatRequestProcessorDoesNotRefreshesCredential_whenRequestDoesNotRequireAuthentication() async { + // given + requestBuilderMock.stubbedBuildResult = URLRequest.fake() + dataRequestHandler.startDataTaskThrowError = URLError(.unknown) + + let request = RequestMock() + request.stubbedRequiresAuthentication = false // when - let _ = try await sut.send(request) as Int + do { + let _ = try await sut.send(request) as Int + } catch {} // then -// XCTAssertEqual(request.re, <#T##expression2: Equatable##Equatable#>) + XCTAssertFalse(interceptorMock.invokedAdapt) + XCTAssertFalse(interceptorMock.invokedRefresh) } } From bafb5d78b252fe8ffee514bea44805ba0105c96d Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sun, 19 Nov 2023 16:51:12 +0300 Subject: [PATCH 11/26] Implement authentication logic --- .../AuthenticatorInterceptor.swift | 13 ++-- .../RequestProcessor/RequestProcessor.swift | 60 ++++++++++++------- .../Classes/DI/NetworkLayerAssembly.swift | 24 ++++---- .../Core/Authenticator/IAuthenticator.swift | 5 +- .../IAuthenticatorInterceptor.swift | 14 ++++- .../Classes/Core/Models/IRequest.swift | 29 +++++++++ .../Classes/DI/INetworkLayerAssembly.swift | 21 +++++-- 7 files changed, 116 insertions(+), 50 deletions(-) diff --git a/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift index 7782c85..45bb64d 100644 --- a/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift +++ b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift @@ -27,7 +27,7 @@ public final class AuthenticatorInterceptor: IAut // MARK: IAuthentificatorInterceptor - public func adapt(request: inout URLRequest, for session: URLSession) async throws { + public func adapt(request: URLRequest, for session: URLSession) async throws { guard let credential else { throw AuthenticatorInterceptorError.missingCredential } @@ -35,17 +35,16 @@ public final class AuthenticatorInterceptor: IAut if credential.requiresRefresh { try await refresh(credential, for: session) } else { - try await authenticator.apply(credential, to: &request) + try await authenticator.apply(credential, to: request) } } public func refresh( _ request: URLRequest, with response: HTTPURLResponse, - for session: URLSession, - dutTo error: Error + for session: URLSession ) async throws { - guard authenticator.didRequest(request, with: response, failDueToAuthenticationError: error) else { + guard isRequireRefresh(request, response: response) else { return } @@ -60,6 +59,10 @@ public final class AuthenticatorInterceptor: IAut try await refresh(credential, for: session) } + public func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool { + authenticator.didRequest(request, with: response) + } + // MARK: Private private func refresh(_ credential: Credential, for session: URLSession) async throws { diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index 27dcb4e..35d9b65 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -76,45 +76,57 @@ actor RequestProcessor { delegate: URLSessionDelegate?, configure: ((inout URLRequest) throws -> Void)? ) async throws -> Response { - guard var urlRequest = requestBuilder.build(request, configure) else { + guard let urlRequest = requestBuilder.build(request, configure) else { throw NetworkLayerError.badURL } + try await adapt(request, urlRequest: urlRequest, session: session) + return try await performRequest(strategy: strategy) { try await self.delegate?.requestProcessor(self, willSendRequest: urlRequest) - try await self.adapt(request, urlRequest: &urlRequest, session: session) let task = session.dataTask(with: urlRequest) do { - return try await dataRequestHandler.startDataTask(task, session: session, delegate: delegate) + let response = try await dataRequestHandler.startDataTask(task, session: session, delegate: delegate) + + if request.requiresAuthentication { + let isRefreshedCredential = try await refresh( + urlRequest: urlRequest, + response: response, + session: session + ) + + if isRefreshedCredential { + throw AuthenticatorInterceptorError.missingCredential + } + } + + return response } catch { - try await refreshCredentialIfNeeded( - request, - urlRequest: &urlRequest, - response: HTTPURLResponse(), - session: session, - error: error - ) throw error } } } - private func adapt(_ request: T, urlRequest: inout URLRequest, session: URLSession) async throws { + private func adapt(_ request: T, urlRequest: URLRequest, session: URLSession) async throws { guard request.requiresAuthentication else { return } - try await interceptor?.adapt(request: &urlRequest, for: session) + try await interceptor?.adapt(request: urlRequest, for: session) } - private func refreshCredentialIfNeeded( - _ request: T, - urlRequest: inout URLRequest, - response: HTTPURLResponse, - session: URLSession, - error: Error - ) async throws { - guard request.requiresAuthentication else { return } - try await interceptor?.refresh(urlRequest, with: response, for: session, dutTo: error) + private func refresh( + urlRequest: URLRequest, + response: Response, + session: URLSession + ) async throws -> Bool { + guard let interceptor, let response = response.response as? HTTPURLResponse else { return false } + + if interceptor.isRequireRefresh(urlRequest, response: response) { + try await interceptor.refresh(urlRequest, with: response, for: session) + return true + } + + return false } /// Performs a request with a retry policy. @@ -128,7 +140,11 @@ actor RequestProcessor { strategy: RetryPolicyStrategy? = nil, _ send: () async throws -> T ) async throws -> T { - try await retryPolicyService.retry(strategy: strategy, send) + do { + return try await send() + } catch { + return try await retryPolicyService.retry(strategy: strategy, send) + } } } diff --git a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift index 789a178..9f95df6 100644 --- a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift +++ b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift @@ -14,12 +14,10 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { private let configure: Configuration /// The request builder. private let requestBuilder: IRequestBuilder - /// The data request handler. - private let dataRequestHandler: IDataRequestHandler /// The retry policy service. - private let retryPolicyService: IRetryPolicyService + private let retryPolicyStrategy: RetryPolicyStrategy? /// The request processor delegate. - private let delegate: RequestProcessorDelegate + private let delegate: RequestProcessorDelegate? /// The authenticator interceptor. private let interceptor: IAuthenticatorInterceptor? @@ -28,15 +26,13 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { public init( configure: Configuration, requestBuilder: IRequestBuilder, - dataRequestHandler: IDataRequestHandler, - retryPolicyService: IRetryPolicyService, - delegate: RequestProcessorDelegate, + retryPolicyStrategy: RetryPolicyStrategy?, + delegate: RequestProcessorDelegate?, interceptor: IAuthenticatorInterceptor? ) { self.configure = configure self.requestBuilder = requestBuilder - self.dataRequestHandler = dataRequestHandler - self.retryPolicyService = retryPolicyService + self.retryPolicyStrategy = retryPolicyStrategy self.delegate = delegate self.interceptor = interceptor } @@ -47,10 +43,16 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { RequestProcessor( configuration: configure, requestBuilder: requestBuilder, - dataRequestHandler: dataRequestHandler, - retryPolicyService: retryPolicyService, + dataRequestHandler: DataRequestHandler(), + retryPolicyService: RetryPolicyService(strategy: retryPolicyStrategy ?? defaultStrategy), delegate: delegate, interceptor: interceptor ) } + + // MARK: Private + + private var defaultStrategy: RetryPolicyStrategy { + .constant(retry: 5, duration: .seconds(1)) + } } diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift index 68bbde9..7679cf2 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift @@ -14,7 +14,7 @@ public protocol IAuthenticator { /// - Parameters: /// - credential: The `Credential`. /// - urlRequest: The `URLRequest`. - func apply(_ credential: Credential, to urlRequest: inout URLRequest) async throws + func apply(_ credential: Credential, to urlRequest: URLRequest) async throws /// Refreshes the `Credential`. /// @@ -28,10 +28,9 @@ public protocol IAuthenticator { /// - Parameters: /// - urlRequest: The `URLRequest`. /// - response: The `HTTPURLResponse`. - /// - error: The `Error`. /// /// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise. - func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool + func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse) -> Bool /// Determines whether the `URLRequest` is authenticated with the `Credential`. /// diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift index ed4c9a5..7f6b5a7 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift @@ -12,13 +12,21 @@ public protocol IAuthenticatorInterceptor { /// - Parameters: /// - request: The URLRequest to be adapted. /// - session: The URLSession for which the request is being adapted. - func adapt(request: inout URLRequest, for session: URLSession) async throws + func adapt(request: URLRequest, for session: URLSession) async throws /// Refreshes credential for the request. /// /// - Parameters: /// - request: The URLRequest to be refreshed. /// - session: The URLSession for which the request is being refreshed. - /// - error: The error occurred during the request. - func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession, dutTo error: Error) async throws + func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) async throws + + /// <#Description#> + /// + /// - Parameters: + /// - request: <#request description#> + /// - response: <#response description#> + /// + /// - Returns: <#description#> + func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool } diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift index 2428c9e..469f146 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift @@ -5,6 +5,8 @@ import Foundation +// MARK: - IRequest + /// A type to which all requests must conform. public protocol IRequest { /// The base `URL` for the resource. @@ -31,3 +33,30 @@ public protocol IRequest { /// A dictonary that contains the request's body. var httpBody: [String: Any]? { get } } + +public extension IRequest { + /// A dictionary that contains the parameters to be encoded into the request's header. + var headers: [String: String]? { + nil + } + + /// A dictionary that contains the parameters to be encoded into the request. + var parameters: [String: String]? { + nil + } + + /// A Boolean value indicating whether authentication is required. + var requiresAuthentication: Bool { + false + } + + /// Request's timeout interval. + var timeoutInterval: TimeInterval { + 60 + } + + /// A dictonary that contains the request's body. + var httpBody: [String: Any]? { + nil + } +} diff --git a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift index c8a0ae6..4b56012 100644 --- a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift +++ b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift @@ -4,7 +4,9 @@ // import Foundation -import protocol Typhoon.IRetryPolicyService +import enum Typhoon.RetryPolicyStrategy + +// MARK: - INetworkLayerAssembly /// A type that represents a network layer assembly. public protocol INetworkLayerAssembly { @@ -13,16 +15,14 @@ public protocol INetworkLayerAssembly { /// - Parameters: /// - configure: The network layer's configuration. /// - requestBuilder: The request builder. - /// - dataRequestHandler: The data request handler. - /// - retryPolicyService: The retry policy service. + /// - retryPolicyStrategy: The retry policy strategy. /// - delegate: The request processor delegate. /// - interceptor: The authenticator interceptor. init( configure: Configuration, requestBuilder: IRequestBuilder, - dataRequestHandler: IDataRequestHandler, - retryPolicyService: IRetryPolicyService, - delegate: RequestProcessorDelegate, + retryPolicyStrategy: RetryPolicyStrategy?, + delegate: RequestProcessorDelegate?, interceptor: IAuthenticatorInterceptor? ) @@ -31,3 +31,12 @@ public protocol INetworkLayerAssembly { /// - Returns: A request processor. func assemble() -> IRequestProcessor } + +public extension INetworkLayerAssembly { + init( + configure: Configuration, + requestBuilder: IRequestBuilder + ) { + self.init(configure: configure, requestBuilder: requestBuilder, retryPolicyStrategy: nil, delegate: nil, interceptor: nil) + } +} From e98a648891767dd6c0e04cf0474dc6d527481324 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 20 Nov 2023 18:08:06 +0300 Subject: [PATCH 12/26] Implement `RequestBuilder` --- .../IRequestBodyEncoder.swift | 17 ++++++ .../RequestBodyEncoder.swift | 32 +++++++++++ .../RequestBuilder/RequestBuilder.swift | 57 +++++++++++++++++++ .../IRequestParametersEncoder.swift | 16 ++++++ .../RequestParametersEncoder.swift | 25 ++++++++ .../RequestProcessor/RequestProcessor.swift | 2 +- .../Classes/DI/NetworkLayerAssembly.swift | 25 ++++++-- .../Classes/Extensions/IRequest+.swift | 13 +++++ .../Core/Encoder/IParameterEncoder.swift | 13 +++++ .../Classes/Core/Models/HTTPMethod.swift | 22 +++---- .../Classes/Core/Models/IRequest.swift | 12 +++- .../Classes/Core/Models/RequestBody.swift | 12 ++++ .../Core/Services/IRequestBuilder.swift | 2 +- .../Classes/DI/INetworkLayerAssembly.swift | 11 ++-- .../Mocks/AuthenticatorMock.swift | 12 ++-- .../AuthentificatorInterceptorMock.swift | 26 +++++++-- .../AuthenticationInterceptorTests.swift | 20 +++---- .../UnitTests/RequestProcessorTests.swift | 3 +- 18 files changed, 271 insertions(+), 49 deletions(-) create mode 100644 Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBodyEncoder/IRequestBodyEncoder.swift create mode 100644 Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBodyEncoder/RequestBodyEncoder.swift create mode 100644 Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBuilder.swift create mode 100644 Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/IRequestParametersEncoder.swift create mode 100644 Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/RequestParametersEncoder.swift create mode 100644 Sources/NetworkLayer/Classes/Extensions/IRequest+.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Encoder/IParameterEncoder.swift create mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Models/RequestBody.swift diff --git a/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBodyEncoder/IRequestBodyEncoder.swift b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBodyEncoder/IRequestBodyEncoder.swift new file mode 100644 index 0000000..7bc4b0a --- /dev/null +++ b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBodyEncoder/IRequestBodyEncoder.swift @@ -0,0 +1,17 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +/// A type defines the interface for the request body encoder. +protocol IRequestBodyEncoder { + /// Encodes parameters into the request body. + /// + /// - Parameters: + /// - body: The parameters to be encoded. + /// - request: The request. + func encode(body: RequestBody, to request: inout URLRequest) throws +} diff --git a/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBodyEncoder/RequestBodyEncoder.swift b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBodyEncoder/RequestBodyEncoder.swift new file mode 100644 index 0000000..db481d4 --- /dev/null +++ b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBodyEncoder/RequestBodyEncoder.swift @@ -0,0 +1,32 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +struct RequestBodyEncoder: IRequestBodyEncoder { + // MARK: Properties + + private let jsonEncoder: JSONEncoder + + // MARK: Initialization + + init(jsonEncoder: JSONEncoder) { + self.jsonEncoder = jsonEncoder + } + + // MARK: IRequestBodyEncoder + + func encode(body: NetworkLayerInterfaces.RequestBody, to request: inout URLRequest) throws { + switch body { + case let .data(data): + request.httpBody = data + case let .encodable(encodable): + request.httpBody = try jsonEncoder.encode(encodable) + case let .dictonary(dictionary): + request.httpBody = try JSONSerialization.data(withJSONObject: dictionary) + } + } +} diff --git a/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBuilder.swift b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBuilder.swift new file mode 100644 index 0000000..8b18223 --- /dev/null +++ b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBuilder.swift @@ -0,0 +1,57 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class RequestBuilder: IRequestBuilder { + // MARK: Properties + + private let parametersEncoder: IRequestParametersEncoder + private let requestBodyEncoder: IRequestBodyEncoder + + // MARK: Initialization + + init( + parametersEncoder: IRequestParametersEncoder, + requestBodyEncoder: IRequestBodyEncoder + ) { + self.parametersEncoder = parametersEncoder + self.requestBodyEncoder = requestBodyEncoder + } + + // MARK: IRequestBuilder + + func build(_ request: NetworkLayerInterfaces.IRequest, _: ((inout URLRequest) throws -> Void)?) throws -> URLRequest? { + guard let url = URL(string: request.fullPath) else { + throw URLError(.badURL) + } + + var urlRequest = URLRequest( + url: url, + cachePolicy: request.cachePolicy, + timeoutInterval: request.timeoutInterval + ) + + urlRequest.httpMethod = request.httpMethod.rawValue + + setHeaders(to: &urlRequest, headers: request.headers) + + try parametersEncoder.encode(parameters: request.parameters ?? [:], to: &urlRequest) + + if let httpBody = request.httpBody { + try requestBodyEncoder.encode(body: httpBody, to: &urlRequest) + } + + return urlRequest + } + + // MARK: Private + + private func setHeaders(to request: inout URLRequest, headers: [String: String]?) { + guard let headers else { return } + headers.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) } + } +} diff --git a/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/IRequestParametersEncoder.swift b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/IRequestParametersEncoder.swift new file mode 100644 index 0000000..6b96812 --- /dev/null +++ b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/IRequestParametersEncoder.swift @@ -0,0 +1,16 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +/// A type defines the interface for the request parameters encoder. +protocol IRequestParametersEncoder { + /// Encodes parameters into the request query. + /// + /// - Parameters: + /// - parameters: The parameters to be encoded. + /// - request: The request. + func encode(parameters: [String: String], to request: inout URLRequest) throws +} diff --git a/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/RequestParametersEncoder.swift b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/RequestParametersEncoder.swift new file mode 100644 index 0000000..76963ef --- /dev/null +++ b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/RequestParametersEncoder.swift @@ -0,0 +1,25 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +struct RequestParametersEncoder: IRequestParametersEncoder { + func encode(parameters: [String: String], to request: inout URLRequest) throws { + guard let url = request.url else { + throw URLError(.badURL) + } + + let queries = parameters.map { URLQueryItem(name: $0.key, value: $0.value) } + var urlComponents = URLComponents(string: url.absoluteString) + + urlComponents?.queryItems = queries + + guard let url = urlComponents?.url else { + throw URLError(.badURL) + } + + request.url = url + } +} diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index 35d9b65..3ba54c1 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -76,7 +76,7 @@ actor RequestProcessor { delegate: URLSessionDelegate?, configure: ((inout URLRequest) throws -> Void)? ) async throws -> Response { - guard let urlRequest = requestBuilder.build(request, configure) else { + guard let urlRequest = try requestBuilder.build(request, configure) else { throw NetworkLayerError.badURL } diff --git a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift index 9f95df6..35e3fb7 100644 --- a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift +++ b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift @@ -12,29 +12,29 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { /// The network layer's configuration. private let configure: Configuration - /// The request builder. - private let requestBuilder: IRequestBuilder /// The retry policy service. private let retryPolicyStrategy: RetryPolicyStrategy? /// The request processor delegate. private let delegate: RequestProcessorDelegate? /// The authenticator interceptor. private let interceptor: IAuthenticatorInterceptor? + /// The json encoder. + private let jsonEncoder: JSONEncoder // MARK: Initialization public init( configure: Configuration, - requestBuilder: IRequestBuilder, retryPolicyStrategy: RetryPolicyStrategy?, delegate: RequestProcessorDelegate?, - interceptor: IAuthenticatorInterceptor? + interceptor: IAuthenticatorInterceptor?, + jsonEncoder: JSONEncoder ) { self.configure = configure - self.requestBuilder = requestBuilder self.retryPolicyStrategy = retryPolicyStrategy self.delegate = delegate self.interceptor = interceptor + self.jsonEncoder = jsonEncoder } // MARK: INetworkLayerAssembly @@ -55,4 +55,19 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { private var defaultStrategy: RetryPolicyStrategy { .constant(retry: 5, duration: .seconds(1)) } + + private var requestBuilder: IRequestBuilder { + RequestBuilder( + parametersEncoder: parametersEncoder, + requestBodyEncoder: requestBodyEncoder + ) + } + + private var parametersEncoder: IRequestParametersEncoder { + RequestParametersEncoder() + } + + private var requestBodyEncoder: IRequestBodyEncoder { + RequestBodyEncoder(jsonEncoder: jsonEncoder) + } } diff --git a/Sources/NetworkLayer/Classes/Extensions/IRequest+.swift b/Sources/NetworkLayer/Classes/Extensions/IRequest+.swift new file mode 100644 index 0000000..c525db2 --- /dev/null +++ b/Sources/NetworkLayer/Classes/Extensions/IRequest+.swift @@ -0,0 +1,13 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +extension IRequest { + var fullPath: String { + [domainName, path].joined(separator: "/") + } +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Encoder/IParameterEncoder.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Encoder/IParameterEncoder.swift new file mode 100644 index 0000000..fcd006f --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Encoder/IParameterEncoder.swift @@ -0,0 +1,13 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +public protocol IParameterEncoder { + func encode( + _ parameters: Parameters?, + into request: URLRequest + ) throws -> URLRequest +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/HTTPMethod.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/HTTPMethod.swift index 07a0b3c..f87a185 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Models/HTTPMethod.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/HTTPMethod.swift @@ -8,25 +8,25 @@ import Foundation /// Enum representing HTTP methods. /// /// See https://tools.ietf.org/html/rfc7231#section-4.3 -public enum HTTPMethod { +public enum HTTPMethod: String { /// `CONNECT` method. - case connect + case connect = "CONNECT" /// `DELETE` method. - case delete + case delete = "DELETE" /// `GET` method. - case get + case get = "GET" /// `HEAD` method. - case head + case head = "HEAD" /// `OPTIONS` method. - case options + case options = "OPTIONS" /// `PATCH` method. - case patch + case patch = "PATCH" /// `POST` method. - case post + case post = "POST" /// `PUT` method. - case put + case put = "PUT" /// `QUERY` method. - case query + case query = "QUERY" /// `TRACE` method. - case trace + case trace = "TRACE" } diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift index 469f146..91caaae 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift @@ -31,7 +31,10 @@ public protocol IRequest { var httpMethod: HTTPMethod { get } /// A dictonary that contains the request's body. - var httpBody: [String: Any]? { get } + var httpBody: RequestBody? { get } + + /// An alias for the cache policy. + var cachePolicy: URLRequest.CachePolicy { get } } public extension IRequest { @@ -56,7 +59,12 @@ public extension IRequest { } /// A dictonary that contains the request's body. - var httpBody: [String: Any]? { + var httpBody: RequestBody? { nil } + + /// An alias for the cache policy. + var cachePolicy: URLRequest.CachePolicy { + .useProtocolCachePolicy + } } diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/RequestBody.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/RequestBody.swift new file mode 100644 index 0000000..f884d5c --- /dev/null +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/RequestBody.swift @@ -0,0 +1,12 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +public enum RequestBody { + case data(Data) + case encodable(Encodable) + case dictonary([String: Any]) +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift index 00781f1..f47e8bf 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift @@ -12,5 +12,5 @@ public protocol IRequestBuilder { /// - Parameter request: The request object that defines the request details. /// /// - Returns: A `URLRequest` constructed based on the given data. - func build(_ request: IRequest, _ configure: ((inout URLRequest) throws -> Void)?) -> URLRequest? + func build(_ request: IRequest, _ configure: ((inout URLRequest) throws -> Void)?) throws -> URLRequest? } diff --git a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift index 4b56012..bab6cd0 100644 --- a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift +++ b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift @@ -14,16 +14,16 @@ public protocol INetworkLayerAssembly { /// /// - Parameters: /// - configure: The network layer's configuration. - /// - requestBuilder: The request builder. /// - retryPolicyStrategy: The retry policy strategy. /// - delegate: The request processor delegate. /// - interceptor: The authenticator interceptor. + /// - jsonEncoder: The json encoder. init( configure: Configuration, - requestBuilder: IRequestBuilder, retryPolicyStrategy: RetryPolicyStrategy?, delegate: RequestProcessorDelegate?, - interceptor: IAuthenticatorInterceptor? + interceptor: IAuthenticatorInterceptor?, + jsonEncoder: JSONEncoder ) /// Assembles a request processor. @@ -34,9 +34,8 @@ public protocol INetworkLayerAssembly { public extension INetworkLayerAssembly { init( - configure: Configuration, - requestBuilder: IRequestBuilder + configure: Configuration ) { - self.init(configure: configure, requestBuilder: requestBuilder, retryPolicyStrategy: nil, delegate: nil, interceptor: nil) + self.init(configure: configure, retryPolicyStrategy: nil, delegate: nil, interceptor: nil, jsonEncoder: JSONEncoder()) } } diff --git a/Tests/NetworkLayerTests/Mocks/AuthenticatorMock.swift b/Tests/NetworkLayerTests/Mocks/AuthenticatorMock.swift index a457382..4b1d42b 100644 --- a/Tests/NetworkLayerTests/Mocks/AuthenticatorMock.swift +++ b/Tests/NetworkLayerTests/Mocks/AuthenticatorMock.swift @@ -14,7 +14,7 @@ final class AuthenticatorMock: IAuthenticator { var invokedApplyParameters: (credential: Credential, urlRequest: URLRequest)? var invokedApplyParametersList = [(credential: Credential, urlRequest: URLRequest)]() - func apply(_ credential: Credential, to urlRequest: inout URLRequest) async throws { + func apply(_ credential: Credential, to urlRequest: URLRequest) async throws { invokedApply = true invokedApplyCount += 1 invokedApplyParameters = (credential, urlRequest) @@ -37,15 +37,15 @@ final class AuthenticatorMock: IAuthenticator { var invokedDidRequest = false var invokedDidRequestCount = 0 - var invokedDidRequestParameters: (urlRequest: URLRequest, response: HTTPURLResponse, error: Error)? - var invokedDidRequestParametersList = [(urlRequest: URLRequest, response: HTTPURLResponse, error: Error)]() + var invokedDidRequestParameters: (urlRequest: URLRequest, response: HTTPURLResponse)? + var invokedDidRequestParametersList = [(urlRequest: URLRequest, response: HTTPURLResponse)]() var stubbedDidRequestResult: Bool! = false - func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool { + func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse) -> Bool { invokedDidRequest = true invokedDidRequestCount += 1 - invokedDidRequestParameters = (urlRequest, response, error) - invokedDidRequestParametersList.append((urlRequest, response, error)) + invokedDidRequestParameters = (urlRequest, response) + invokedDidRequestParametersList.append((urlRequest, response)) return stubbedDidRequestResult } diff --git a/Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift b/Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift index 65c2158..b5561a2 100644 --- a/Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift +++ b/Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift @@ -12,7 +12,7 @@ final class AuthentificatorInterceptorMock: IAuthenticatorInterceptor { var invokedAdaptParameters: (request: URLRequest, session: URLSession)? var invokedAdaptParametersList = [(request: URLRequest, session: URLSession)]() - func adapt(request: inout URLRequest, for session: URLSession) async throws { + func adapt(request: URLRequest, for session: URLSession) { invokedAdapt = true invokedAdaptCount += 1 invokedAdaptParameters = (request, session) @@ -21,13 +21,27 @@ final class AuthentificatorInterceptorMock: IAuthenticatorInterceptor { var invokedRefresh = false var invokedRefreshCount = 0 - var invokedRefreshParameters: (request: URLRequest, response: HTTPURLResponse, session: URLSession, error: Error)? - var invokedRefreshParametersList = [(request: URLRequest, response: HTTPURLResponse, session: URLSession, error: Error)]() + var invokedRefreshParameters: (request: URLRequest, response: HTTPURLResponse, session: URLSession)? + var invokedRefreshParametersList = [(request: URLRequest, response: HTTPURLResponse, session: URLSession)]() - func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession, dutTo error: Error) async throws { + func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) { invokedRefresh = true invokedRefreshCount += 1 - invokedRefreshParameters = (request, response, session, error) - invokedRefreshParametersList.append((request, response, session, error)) + invokedRefreshParameters = (request, response, session) + invokedRefreshParametersList.append((request, response, session)) + } + + var invokedIsRequireRefresh = false + var invokedIsRequireRefreshCount = 0 + var invokedIsRequireRefreshParameters: (request: URLRequest, response: HTTPURLResponse)? + var invokedIsRequireRefreshParametersList = [(request: URLRequest, response: HTTPURLResponse)]() + var stubbedIsRequireRefreshResult: Bool! = false + + func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool { + invokedIsRequireRefresh = true + invokedIsRequireRefreshCount += 1 + invokedIsRequireRefreshParameters = (request, response) + invokedIsRequireRefreshParametersList.append((request, response)) + return stubbedIsRequireRefreshResult } } diff --git a/Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift b/Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift index 2041bd2..880aa25 100644 --- a/Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift +++ b/Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift @@ -35,12 +35,12 @@ final class AuthenticationInterceptorTests: XCTestCase { func test_thatAuthenticatorInterceptorThrowsAnErrorOnAdaptRequest_whenCredentialIsMissing() async throws { // given - var requestMock = URLRequest.fake() + let requestMock = URLRequest.fake() // when var receivedError: NSError? do { - try await sut.adapt(request: &requestMock, for: .shared) + try await sut.adapt(request: requestMock, for: .shared) } catch { receivedError = error as NSError } @@ -52,13 +52,13 @@ final class AuthenticationInterceptorTests: XCTestCase { func test_thatAuthenticatorInterceptorAdaptsRequest_whenCredentialIsNotMissingAndValid() async throws { // given let credentialStub = AuthenticationCredentialStub() - var requestMock = URLRequest.fake() + let requestMock = URLRequest.fake() credentialStub.stubbedRequiresRefresh = false sut.credential = credentialStub // when - try await sut.adapt(request: &requestMock, for: .shared) + try await sut.adapt(request: requestMock, for: .shared) // then XCTAssertEqual(authenticatorMock.invokedApplyCount, 1) @@ -66,7 +66,7 @@ final class AuthenticationInterceptorTests: XCTestCase { func test_thatAuthenticatorInterceptorAdaptsRequest_whenCredentialIsNotMissingAndNotValid() async throws { // given - var requestMock = URLRequest.fake() + let requestMock = URLRequest.fake() let credentialStub = AuthenticationCredentialStub() credentialStub.stubbedRequiresRefresh = true @@ -75,7 +75,7 @@ final class AuthenticationInterceptorTests: XCTestCase { sut.credential = credentialStub // when - try await sut.adapt(request: &requestMock, for: .shared) + try await sut.adapt(request: requestMock, for: .shared) // then XCTAssertEqual(authenticatorMock.invokedRefreshCount, 1) @@ -91,7 +91,7 @@ final class AuthenticationInterceptorTests: XCTestCase { authenticatorMock.stubbedIsRequestResult = true // when - try await sut.refresh(requestMock, with: .init(), for: .shared, dutTo: URLError(.unknown)) + try await sut.refresh(requestMock, with: .init(), for: .shared) // then XCTAssertEqual(authenticatorMock.invokedRefreshCount, 1) @@ -104,7 +104,7 @@ final class AuthenticationInterceptorTests: XCTestCase { authenticatorMock.stubbedDidRequestResult = false // when - try await sut.refresh(requestMock, with: .init(), for: .shared, dutTo: URLError(.unknown)) + try await sut.refresh(requestMock, with: .init(), for: .shared) // then XCTAssertFalse(authenticatorMock.invokedRefresh) @@ -120,7 +120,7 @@ final class AuthenticationInterceptorTests: XCTestCase { // when var receivedError: NSError? do { - try await sut.refresh(requestMock, with: .init(), for: .shared, dutTo: URLError(.unknown)) + try await sut.refresh(requestMock, with: .init(), for: .shared) } catch { receivedError = error as NSError } @@ -138,7 +138,7 @@ final class AuthenticationInterceptorTests: XCTestCase { authenticatorMock.stubbedDidRequestResult = true // when - try await sut.refresh(requestMock, with: .init(), for: .shared, dutTo: URLError(.unknown)) + try await sut.refresh(requestMock, with: .init(), for: .shared) // then XCTAssertFalse(authenticatorMock.invokedRefresh) diff --git a/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift b/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift index 3279163..f55e81f 100644 --- a/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift +++ b/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift @@ -99,7 +99,8 @@ final class RequestProcessorTests: XCTestCase { func test_thatRequestProcessorRefreshesCredential_whenCredentialIsNotValid() async { // given requestBuilderMock.stubbedBuildResult = URLRequest.fake() - dataRequestHandler.startDataTaskThrowError = URLError(.unknown) + dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: HTTPURLResponse(), task: URLSessionTask()) + interceptorMock.stubbedIsRequireRefreshResult = true let request = RequestMock() request.stubbedRequiresAuthentication = true From 2ca026e4f8eca16c0d2afddcc6289b958b13c35b Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 20 Nov 2023 19:04:25 +0300 Subject: [PATCH 13/26] Implement `UnitTests` - `RequestParametersEncoderTests` - `RequestBuilderTests` - `RequestBodyEncoderTests` --- .../AuthenticatorInterceptor.swift | 3 +- .../RequestBuilder/RequestBuilder.swift | 9 +- .../Classes/Extensions/IRequest+.swift | 7 +- .../Fakes/URLRequest+Fake.swift | 4 +- .../Mocks/RequestBodyEncoderMock.swift | 26 +++++ .../Mocks/RequestParametersEncoderMock.swift | 25 +++++ .../NetworkLayerTests/Stubs/RequestStub.swift | 63 ++++++++++++ .../UnitTests/RequestBodyEncoderTests.swift | 73 ++++++++++++++ .../UnitTests/RequestBuilderTests.swift | 95 +++++++++++++++++++ .../RequestParametersEncoderTests.swift | 68 +++++++++++++ 10 files changed, 366 insertions(+), 7 deletions(-) create mode 100644 Tests/NetworkLayerTests/Mocks/RequestBodyEncoderMock.swift create mode 100644 Tests/NetworkLayerTests/Mocks/RequestParametersEncoderMock.swift create mode 100644 Tests/NetworkLayerTests/Stubs/RequestStub.swift create mode 100644 Tests/NetworkLayerTests/UnitTests/RequestBodyEncoderTests.swift create mode 100644 Tests/NetworkLayerTests/UnitTests/RequestBuilderTests.swift create mode 100644 Tests/NetworkLayerTests/UnitTests/RequestParametersEncoderTests.swift diff --git a/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift index 45bb64d..f3b21e6 100644 --- a/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift +++ b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift @@ -7,7 +7,8 @@ import Atomic import Foundation import NetworkLayerInterfaces -/// <#Description#> +/// A custom AuthenticatorInterceptor implementation that works with a specific type +/// of Authenticator conforming to the IAuthenticator protocol. public final class AuthenticatorInterceptor: IAuthenticatorInterceptor { // MARK: Types diff --git a/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBuilder.swift b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBuilder.swift index 8b18223..48f581f 100644 --- a/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBuilder.swift +++ b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBuilder.swift @@ -24,8 +24,11 @@ final class RequestBuilder: IRequestBuilder { // MARK: IRequestBuilder - func build(_ request: NetworkLayerInterfaces.IRequest, _: ((inout URLRequest) throws -> Void)?) throws -> URLRequest? { - guard let url = URL(string: request.fullPath) else { + func build( + _ request: NetworkLayerInterfaces.IRequest, + _ configure: ((inout URLRequest) throws -> Void)? + ) throws -> URLRequest? { + guard let fullPath = request.fullPath, let url = URL(string: fullPath) else { throw URLError(.badURL) } @@ -45,6 +48,8 @@ final class RequestBuilder: IRequestBuilder { try requestBodyEncoder.encode(body: httpBody, to: &urlRequest) } + try configure?(&urlRequest) + return urlRequest } diff --git a/Sources/NetworkLayer/Classes/Extensions/IRequest+.swift b/Sources/NetworkLayer/Classes/Extensions/IRequest+.swift index c525db2..fc6fe74 100644 --- a/Sources/NetworkLayer/Classes/Extensions/IRequest+.swift +++ b/Sources/NetworkLayer/Classes/Extensions/IRequest+.swift @@ -7,7 +7,10 @@ import Foundation import NetworkLayerInterfaces extension IRequest { - var fullPath: String { - [domainName, path].joined(separator: "/") + var fullPath: String? { + if !domainName.isEmpty { + return [domainName, path].joined(separator: "/") + } + return nil } } diff --git a/Tests/NetworkLayerTests/Fakes/URLRequest+Fake.swift b/Tests/NetworkLayerTests/Fakes/URLRequest+Fake.swift index fd02280..1cfa8fa 100644 --- a/Tests/NetworkLayerTests/Fakes/URLRequest+Fake.swift +++ b/Tests/NetworkLayerTests/Fakes/URLRequest+Fake.swift @@ -6,7 +6,7 @@ import Foundation extension URLRequest { - static func fake() -> URLRequest { - URLRequest(url: URL(string: "https://google.com/")!) + static func fake(string: String = "https://google.com/") -> URLRequest { + URLRequest(url: URL(string: string)!) } } diff --git a/Tests/NetworkLayerTests/Mocks/RequestBodyEncoderMock.swift b/Tests/NetworkLayerTests/Mocks/RequestBodyEncoderMock.swift new file mode 100644 index 0000000..e6cc138 --- /dev/null +++ b/Tests/NetworkLayerTests/Mocks/RequestBodyEncoderMock.swift @@ -0,0 +1,26 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +@testable import NetworkLayer +import NetworkLayerInterfaces + +final class RequestBodyEncoderMock: IRequestBodyEncoder { + var invokedEncode = false + var invokedEncodeCount = 0 + var invokedEncodeParameters: (body: RequestBody, request: URLRequest)? + var invokedEncodeParametersList = [(body: RequestBody, request: URLRequest)]() + var stubbedEncodeError: Error? + + func encode(body: RequestBody, to request: inout URLRequest) throws { + invokedEncode = true + invokedEncodeCount += 1 + invokedEncodeParameters = (body, request) + invokedEncodeParametersList.append((body, request)) + if let error = stubbedEncodeError { + throw error + } + } +} diff --git a/Tests/NetworkLayerTests/Mocks/RequestParametersEncoderMock.swift b/Tests/NetworkLayerTests/Mocks/RequestParametersEncoderMock.swift new file mode 100644 index 0000000..1e6c9e4 --- /dev/null +++ b/Tests/NetworkLayerTests/Mocks/RequestParametersEncoderMock.swift @@ -0,0 +1,25 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +@testable import NetworkLayer + +final class RequestParametersEncoderMock: IRequestParametersEncoder { + var invokedEncode = false + var invokedEncodeCount = 0 + var invokedEncodeParameters: (parameters: [String: String], request: URLRequest)? + var invokedEncodeParametersList = [(parameters: [String: String], request: URLRequest)]() + var stubbedEncodeError: Error? + + func encode(parameters: [String: String], to request: inout URLRequest) throws { + invokedEncode = true + invokedEncodeCount += 1 + invokedEncodeParameters = (parameters, request) + invokedEncodeParametersList.append((parameters, request)) + if let error = stubbedEncodeError { + throw error + } + } +} diff --git a/Tests/NetworkLayerTests/Stubs/RequestStub.swift b/Tests/NetworkLayerTests/Stubs/RequestStub.swift new file mode 100644 index 0000000..148c675 --- /dev/null +++ b/Tests/NetworkLayerTests/Stubs/RequestStub.swift @@ -0,0 +1,63 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class RequestStub: IRequest { + var stubbedDomainName: String! = "" + + var domainName: String { + stubbedDomainName + } + + var stubbedPath: String! = "" + + var path: String { + stubbedPath + } + + var stubbedHeaders: [String: String]! + + var headers: [String: String]? { + stubbedHeaders + } + + var stubbedParameters: [String: String]! + + var parameters: [String: String]? { + stubbedParameters + } + + var stubbedRequiresAuthentication: Bool! = false + + var requiresAuthentication: Bool { + stubbedRequiresAuthentication + } + + var stubbedTimeoutInterval: TimeInterval = 60 + + var timeoutInterval: TimeInterval { + stubbedTimeoutInterval + } + + var stubbedHttpMethod: HTTPMethod = .get + + var httpMethod: HTTPMethod { + stubbedHttpMethod + } + + var stubbedHttpBody: RequestBody? = nil + + var httpBody: RequestBody? { + stubbedHttpBody + } + + var stubbedCachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy + + var cachePolicy: URLRequest.CachePolicy { + stubbedCachePolicy + } +} diff --git a/Tests/NetworkLayerTests/UnitTests/RequestBodyEncoderTests.swift b/Tests/NetworkLayerTests/UnitTests/RequestBodyEncoderTests.swift new file mode 100644 index 0000000..ad3c44c --- /dev/null +++ b/Tests/NetworkLayerTests/UnitTests/RequestBodyEncoderTests.swift @@ -0,0 +1,73 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +@testable import NetworkLayer +import XCTest + +// MARK: - RequestBodyEncoderTests + +final class RequestBodyEncoderTests: XCTestCase { + // MARK: Properties + + private var sut: RequestBodyEncoder! + + // MARK: XCTestCase + + override func setUp() { + super.setUp() + sut = RequestBodyEncoder(jsonEncoder: JSONEncoder()) + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: Tests + + func test_thatRequestBodyEncoderEncodesBodyIntoRequest_whenTypeIsData() throws { + // given + var requestFake = URLRequest.fake() + let data = Data() + + // when + try sut.encode(body: .data(data), to: &requestFake) + + // then + XCTAssertEqual(requestFake.httpBody, data) + } + + func test_thatRequestBodyEncoderEncodesBodyIntoRequest_whenTypeIsDictonary() throws { + // given + var requestFake = URLRequest.fake() + let dictonary = ["test": "test"] + + // when + try sut.encode(body: .dictonary(dictonary), to: &requestFake) + + // then + let data = try JSONSerialization.data(withJSONObject: dictonary) + XCTAssertEqual(requestFake.httpBody, data) + } + + func test_thatRequestBodyEncoderEncodesBodyIntoRequest_whenTypeIsEncodable() throws { + // given + var requestFake = URLRequest.fake() + let object = DummyObject() + + // when + try sut.encode(body: .encodable(object), to: &requestFake) + + // then + let data = try JSONEncoder().encode(object) + XCTAssertEqual(requestFake.httpBody, data) + } +} + +// MARK: RequestBodyEncoderTests.DummyObject + +private extension RequestBodyEncoderTests { + struct DummyObject: Encodable {} +} diff --git a/Tests/NetworkLayerTests/UnitTests/RequestBuilderTests.swift b/Tests/NetworkLayerTests/UnitTests/RequestBuilderTests.swift new file mode 100644 index 0000000..ce7c383 --- /dev/null +++ b/Tests/NetworkLayerTests/UnitTests/RequestBuilderTests.swift @@ -0,0 +1,95 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +@testable import NetworkLayer +import XCTest + +// MARK: - RequestBuilderTests + +final class RequestBuilderTests: XCTestCase { + // MARK: Properties + + private var parametersEncoderMock: RequestParametersEncoderMock! + private var requestBodyEncoderMock: RequestBodyEncoderMock! + + private var sut: RequestBuilder! + + // MARK: XCTestCase + + override func setUp() { + super.setUp() + parametersEncoderMock = RequestParametersEncoderMock() + requestBodyEncoderMock = RequestBodyEncoderMock() + sut = RequestBuilder( + parametersEncoder: parametersEncoderMock, + requestBodyEncoder: requestBodyEncoderMock + ) + } + + override func tearDown() { + parametersEncoderMock = nil + requestBodyEncoderMock = nil + sut = nil + super.tearDown() + } + + // MARK: Tests + + func test_thatRequestBuilderThrowsAnError_whenRequestIsNotValid() { + // given + let request = RequestStub() + + // when + var receivedError: NSError? + do { + _ = try sut.build(request, nil) + } catch { + receivedError = error as NSError + } + + // then + XCTAssertEqual(receivedError, URLError(.badURL) as NSError) + } + + func test_thatRequestBuilderBuildsARequest() throws { + // given + let requestStub = RequestStub() + requestStub.stubbedDomainName = .domainName + requestStub.stubbedHeaders = .contentType + requestStub.stubbedHttpMethod = .post + requestStub.stubbedHttpBody = .dictonary(.item) + requestStub.stubbedParameters = .contentType + + // when + var invokedConfigure = false + let request = try sut.build(requestStub) { _ in invokedConfigure = true } + + // then + XCTAssertTrue(invokedConfigure) + XCTAssertEqual(request?.allHTTPHeaderFields, .contentType) + XCTAssertEqual(request?.httpMethod, "POST") + XCTAssertEqual(parametersEncoderMock.invokedEncodeParameters?.parameters, .contentType) + + if case let .dictonary(dict) = requestBodyEncoderMock.invokedEncodeParameters?.body { + XCTAssertTrue(NSDictionary(dictionary: dict).isEqual(to: Dictionary.item)) + } else { + XCTFail("body should be equal to a dictionary") + } + } +} + +// MARK: - Constants + +private extension String { + static let domainName = "https://google.com" +} + +private extension Dictionary where Self.Key == String, Self.Value == String { + static let contentType = ["Content-Type": "application/json"] +} + +private extension Dictionary where Self.Key == String, Self.Value == Any { + static let item = ["Content-Type": "application/json"] +} diff --git a/Tests/NetworkLayerTests/UnitTests/RequestParametersEncoderTests.swift b/Tests/NetworkLayerTests/UnitTests/RequestParametersEncoderTests.swift new file mode 100644 index 0000000..9cb4a31 --- /dev/null +++ b/Tests/NetworkLayerTests/UnitTests/RequestParametersEncoderTests.swift @@ -0,0 +1,68 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +@testable import NetworkLayer +import XCTest + +// MARK: - RequestParametersEncoderTests + +final class RequestParametersEncoderTests: XCTestCase { + // MARK: Properties + + private var sut: RequestParametersEncoder! + + // MARK: XCTestCase + + override func setUp() { + super.setUp() + sut = RequestParametersEncoder() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: Tests + + func test_thatRequestParametersEncoderEncodesParametersIntoRequest() throws { + // given + var requestMock = URLRequest.fake(string: .domainName) + + // when + try sut.encode(parameters: .parameters, to: &requestMock) + + // then + XCTAssertEqual(requestMock.url?.absoluteString, "https://google.com?id=1") + } + + func test_thatRequestParametersEncoderThrowsAnError_whenURLIsNotValid() { + // given + var requestMock = URLRequest.fake(string: .wrongURL) + + // when + var receivedError: NSError? + do { + try sut.encode(parameters: .parameters, to: &requestMock) + } catch { + receivedError = error as NSError + } + + // then + XCTAssertEqual(receivedError, URLError(.badURL) as NSError) + } +} + +// MARK: - Constants + +private extension String { + static let domainName = "https://google.com" + static let wrongURL = "http://example.com:-80" +} + +private extension Dictionary where Self.Key == String, Self.Value == String { + static let parameters = ["id": "1"] +} From a4e64d88ed6e9bfeb7137fdaf9f4e4f64f7aa9e5 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 20 Nov 2023 19:15:30 +0300 Subject: [PATCH 14/26] Add comments --- .../Classes/Core/Authenticator/IAuthenticator.swift | 2 +- .../Authenticator/IAuthenticatorInterceptor.swift | 8 ++++---- .../Classes/Core/Encoder/IParameterEncoder.swift | 13 ------------- 3 files changed, 5 insertions(+), 18 deletions(-) delete mode 100644 Sources/NetworkLayerInterfaces/Classes/Core/Encoder/IParameterEncoder.swift diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift index 7679cf2..a67cd64 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift @@ -5,7 +5,7 @@ import Foundation -/// <#Description#> +/// A protocol defining the interface for an authenticator type. public protocol IAuthenticator { associatedtype Credential: IAuthenticationCredential diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift index 7f6b5a7..b90c4f0 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift @@ -21,12 +21,12 @@ public protocol IAuthenticatorInterceptor { /// - session: The URLSession for which the request is being refreshed. func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) async throws - /// <#Description#> + /// Determines whether a request requires a credential refresh. /// /// - Parameters: - /// - request: <#request description#> - /// - response: <#response description#> + /// - request: The URLRequest to check. + /// - response: The HTTPURLResponse received for the request. /// - /// - Returns: <#description#> + /// - Returns: A boolean indicating whether a credential refresh is required. func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool } diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Encoder/IParameterEncoder.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Encoder/IParameterEncoder.swift deleted file mode 100644 index fcd006f..0000000 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Encoder/IParameterEncoder.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// network-layer -// Copyright © 2023 Space Code. All rights reserved. -// - -import Foundation - -public protocol IParameterEncoder { - func encode( - _ parameters: Parameters?, - into request: URLRequest - ) throws -> URLRequest -} From 948ee3882cba9d30adde91a9c76271e842e1ae87 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 20 Nov 2023 19:29:50 +0300 Subject: [PATCH 15/26] Return a `Response` object instead of `` --- .../RequestProcessor/RequestProcessor.swift | 5 ++--- .../Classes/Extensions/Response+Map.swift | 13 +++++++++++++ .../Classes/Core/Models/Response.swift | 14 ++++++++++++++ .../Classes/Core/Services/IRequestProcessor.swift | 4 ++-- .../UnitTests/RequestProcessorTests.swift | 8 ++++---- 5 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 Sources/NetworkLayer/Classes/Extensions/Response+Map.swift diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index 3ba54c1..ed520c7 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -156,9 +156,8 @@ extension RequestProcessor: IRequestProcessor { strategy: RetryPolicyStrategy? = nil, delegate: URLSessionDelegate? = nil, configure: ((inout URLRequest) throws -> Void)? = nil - ) async throws -> M { + ) async throws -> Response { let response = try await performRequest(request, strategy: strategy, delegate: delegate, configure: configure) - let item = try configuration.jsonDecoder.decode(M.self, from: response.data) - return item + return try response.map { data in try self.configuration.jsonDecoder.decode(M.self, from: data) } } } diff --git a/Sources/NetworkLayer/Classes/Extensions/Response+Map.swift b/Sources/NetworkLayer/Classes/Extensions/Response+Map.swift new file mode 100644 index 0000000..36242ec --- /dev/null +++ b/Sources/NetworkLayer/Classes/Extensions/Response+Map.swift @@ -0,0 +1,13 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +extension Response { + func map(_ closure: @escaping (T) throws -> U) rethrows -> Response { + try Response(data: closure(data), response: response, task: task) + } +} diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift index d54dfe2..1eaeba7 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift @@ -5,12 +5,26 @@ import Foundation +/// A generic struct representing an HTTP response. public struct Response { + /// The data associated with the response. public let data: T + + /// The URL response received. public let response: URLResponse + + /// The URLSessionTask associated with the response. public let task: URLSessionTask + + /// The HTTP status code of the response, if available. public let statusCode: Int? + /// Initializes a new instance of `Response`. + /// + /// - Parameters: + /// - data: The data associated with the response. + /// - response: The URL response received. + /// - task: The URLSessionTask associated with the response. public init(data: T, response: URLResponse, task: URLSessionTask) { self.data = data self.response = response diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift index 0b2dafa..c3a6561 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift @@ -23,7 +23,7 @@ public protocol IRequestProcessor { strategy: RetryPolicyStrategy?, delegate: URLSessionDelegate?, configure: ((inout URLRequest) throws -> Void)? - ) async throws -> M + ) async throws -> Response } extension IRequestProcessor { @@ -34,7 +34,7 @@ extension IRequestProcessor { func send( _ request: T, strategy: RetryPolicyStrategy? - ) async throws -> M { + ) async throws -> Response { try await send(request, strategy: strategy, delegate: nil, configure: nil) } } diff --git a/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift b/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift index f55e81f..96035dd 100644 --- a/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift +++ b/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift @@ -70,7 +70,7 @@ final class RequestProcessorTests: XCTestCase { // when do { - let _ = try await sut.send(request) as Int + let _ = try await sut.send(request) as Response } catch {} // then @@ -88,7 +88,7 @@ final class RequestProcessorTests: XCTestCase { // when do { - let _ = try await sut.send(request) as Int + let _ = try await sut.send(request) as Response } catch {} // then @@ -107,7 +107,7 @@ final class RequestProcessorTests: XCTestCase { // when do { - let _ = try await sut.send(request) as Int + let _ = try await sut.send(request) as Response } catch {} // then @@ -125,7 +125,7 @@ final class RequestProcessorTests: XCTestCase { // when do { - let _ = try await sut.send(request) as Int + let _ = try await sut.send(request) as Response } catch {} // then From 86d8aa01d20298703f65032c3b7708640c273df0 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 20 Nov 2023 19:33:19 +0300 Subject: [PATCH 16/26] Integrate `CodeCov` --- codecov.yml | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..8bb858a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,48 @@ +codecov: + # Require CI to pass to show coverage, default yes + require_ci_to_pass: yes + notify: + # Codecov should wait for all CI statuses to complete, default yes + wait_for_ci: yes + +coverage: + # Coverage precision range 0-5, default 2 + precision: 2 + + # Direction to round the coverage value - up, down, nearest, default down + round: nearest + + # Value range for red...green, default 70...100 + range: "70...90" + + status: + # Overall project coverage, compare against pull request base + project: + default: + # The required coverage value + target: 50% + + # The leniency in hitting the target. Allow coverage to drop by X% + threshold: 5% + + # Only measure lines adjusted in the pull request or single commit, if the commit in not in the pr + patch: + default: + # The required coverage value + target: 85% + + # Allow coverage to drop by X% + threshold: 50% + changes: no + +comment: + # Pull request Codecov comment format. + # diff: coverage diff of the pull request + # files: a list of files impacted by the pull request (coverage changes, file is new or removed) + layout: "diff, files" + + # Update Codecov comment, if exists. Otherwise post new + behavior: default + + # If true, only post the Codecov comment if coverage changes + require_changes: false \ No newline at end of file From dc625ecf1aa5967309b2bb6e612362e65f2facfa Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 30 Nov 2023 15:13:06 +0100 Subject: [PATCH 17/26] Implement tests - Add unit tests - Add integration tests --- Package.resolved | 9 + Package.swift | 5 + Package@swift-5.7.swift | 9 +- README.md | 6 +- .../AuthenticatorInterceptor.swift | 2 +- .../RequestParametersEncoder.swift | 4 + .../DataRequestHandler.swift | 1 - .../RequestProcessor/RequestProcessor.swift | 32 ++- .../IAuthenticatorInterceptor.swift | 2 +- .../Core/Services/IDataRequestHandler.swift | 4 +- .../Services/RequestProcessorDelegate.swift | 20 ++ .../Helpers/Fakes/HTTPURLResponse+Fake.swift | 12 ++ .../Helpers}/Fakes/URLRequest+Fake.swift | 0 .../Fakes/URLSessionDataTask+Fake.swift | 13 ++ .../Helpers/Fakes/URLSessionTask+Fake.swift | 13 ++ .../Helpers/Helpers/DynamicStubs.swift | 23 +++ .../Helpers/RequestProcessor+Mock.swift | 46 +++++ .../Helpers}/Mocks/AuthenticatorMock.swift | 0 .../AuthentificatorInterceptorMock.swift | 2 +- .../Mocks/DataRequestHandlerMock.swift | 26 ++- .../Mocks/RequestBodyEncoderMock.swift | 0 .../Helpers}/Mocks/RequestBuilderMock.swift | 0 .../Helpers}/Mocks/RequestMock.swift | 0 .../Mocks/RequestParametersEncoderMock.swift | 0 .../Mocks/RequestProcessorDelegateMock.swift | 52 +++++ .../Mocks/URLSessionDelegateMock.swift | 192 ++++++++++++++++++ .../Stubs/AuthenticationCredentialStub.swift | 0 .../Helpers}/Stubs/RequestStub.swift | 0 .../Classes/Helpers/Stubs/StubResponse.swift | 13 ++ .../Classes/Models/MockedData.swift | 10 + .../Classes/Models/User.swift | 13 ++ .../RequestProcessorAuthenticationTests.swift | 170 ++++++++++++++++ .../RequestProcessorRequestTests.swift | 82 ++++++++ .../AuthenticationInterceptorTests.swift | 12 +- .../UnitTests/DataRequestHanderTests.swift | 105 ++++++++++ .../UnitTests/RequestBodyEncoderTests.swift | 0 .../UnitTests/RequestBuilderTests.swift | 0 .../RequestParametersEncoderTests.swift | 0 .../UnitTests/RequestProcessorTests.swift | 6 +- .../Mocks/RequestProcessorDelegateMock.swift | 21 -- .../Resources/JSONs/user.json | 6 + 41 files changed, 862 insertions(+), 49 deletions(-) create mode 100644 Tests/NetworkLayerTests/Classes/Helpers/Fakes/HTTPURLResponse+Fake.swift rename Tests/NetworkLayerTests/{ => Classes/Helpers}/Fakes/URLRequest+Fake.swift (100%) create mode 100644 Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLSessionDataTask+Fake.swift create mode 100644 Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLSessionTask+Fake.swift create mode 100644 Tests/NetworkLayerTests/Classes/Helpers/Helpers/DynamicStubs.swift create mode 100644 Tests/NetworkLayerTests/Classes/Helpers/Helpers/RequestProcessor+Mock.swift rename Tests/NetworkLayerTests/{ => Classes/Helpers}/Mocks/AuthenticatorMock.swift (100%) rename Tests/NetworkLayerTests/{ => Classes/Helpers}/Mocks/AuthentificatorInterceptorMock.swift (96%) rename Tests/NetworkLayerTests/{ => Classes/Helpers}/Mocks/DataRequestHandlerMock.swift (51%) rename Tests/NetworkLayerTests/{ => Classes/Helpers}/Mocks/RequestBodyEncoderMock.swift (100%) rename Tests/NetworkLayerTests/{ => Classes/Helpers}/Mocks/RequestBuilderMock.swift (100%) rename Tests/NetworkLayerTests/{ => Classes/Helpers}/Mocks/RequestMock.swift (100%) rename Tests/NetworkLayerTests/{ => Classes/Helpers}/Mocks/RequestParametersEncoderMock.swift (100%) create mode 100644 Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestProcessorDelegateMock.swift create mode 100644 Tests/NetworkLayerTests/Classes/Helpers/Mocks/URLSessionDelegateMock.swift rename Tests/NetworkLayerTests/{ => Classes/Helpers}/Stubs/AuthenticationCredentialStub.swift (100%) rename Tests/NetworkLayerTests/{ => Classes/Helpers}/Stubs/RequestStub.swift (100%) create mode 100644 Tests/NetworkLayerTests/Classes/Helpers/Stubs/StubResponse.swift create mode 100644 Tests/NetworkLayerTests/Classes/Models/MockedData.swift create mode 100644 Tests/NetworkLayerTests/Classes/Models/User.swift create mode 100644 Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorAuthenticationTests.swift create mode 100644 Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorRequestTests.swift rename Tests/NetworkLayerTests/{ => Classes/Tests}/UnitTests/AuthenticationInterceptorTests.swift (92%) create mode 100644 Tests/NetworkLayerTests/Classes/Tests/UnitTests/DataRequestHanderTests.swift rename Tests/NetworkLayerTests/{ => Classes/Tests}/UnitTests/RequestBodyEncoderTests.swift (100%) rename Tests/NetworkLayerTests/{ => Classes/Tests}/UnitTests/RequestBuilderTests.swift (100%) rename Tests/NetworkLayerTests/{ => Classes/Tests}/UnitTests/RequestParametersEncoderTests.swift (100%) rename Tests/NetworkLayerTests/{ => Classes/Tests}/UnitTests/RequestProcessorTests.swift (96%) delete mode 100644 Tests/NetworkLayerTests/Mocks/RequestProcessorDelegateMock.swift create mode 100644 Tests/NetworkLayerTests/Resources/JSONs/user.json diff --git a/Package.resolved b/Package.resolved index ef0df90..418af9a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,6 +9,15 @@ "version" : "1.0.0" } }, + { + "identity" : "mocker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WeTransfer/Mocker", + "state" : { + "revision" : "4384e015cae4916a6828252467a4437173c7ae17", + "version" : "3.0.1" + } + }, { "identity" : "typhoon", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index dee16f0..dc0e560 100644 --- a/Package.swift +++ b/Package.swift @@ -19,6 +19,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/space-code/atomic", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/space-code/typhoon", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/WeTransfer/Mocker", .upToNextMajor(from: "3.0.1")), ], targets: [ .target( @@ -39,7 +40,11 @@ let package = Package( name: "NetworkLayerTests", dependencies: [ "NetworkLayer", + .product(name: "Mocker", package: "Mocker"), .product(name: "Typhoon", package: "typhoon"), + ], + resources: [ + .copy("Resources"), ] ), ] diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index 73b0944..33e0c71 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -18,6 +18,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/space-code/atomic", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/space-code/typhoon", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/WeTransfer/Mocker", .upToNextMajor(from: "3.0.1")), ], targets: [ .target( @@ -30,13 +31,19 @@ let package = Package( ), .target( name: "NetworkLayerInterfaces", - dependencies: [] + dependencies: [ + .product(name: "Typhoon", package: "typhoon"), + ] ), .testTarget( name: "NetworkLayerTests", dependencies: [ "NetworkLayer", + .product(name: "Mocker", package: "Mocker"), .product(name: "Typhoon", package: "typhoon"), + ], + resources: [ + .copy("Resources"), ] ), ] diff --git a/README.md b/README.md index 0553f66..4439987 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@

License -5.7 +Swift Compability +Platform Compability CI +

## Description @@ -53,4 +55,4 @@ Please feel free to help out with this project! If you see something that could Nikita Vasilev, nv3212@gmail.com ## License -network-layer is available under the MIT license. See the LICENSE file for more info. \ No newline at end of file +network-layer is available under the MIT license. See the LICENSE file for more info. diff --git a/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift index f3b21e6..5ea6ab3 100644 --- a/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift +++ b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift @@ -28,7 +28,7 @@ public final class AuthenticatorInterceptor: IAut // MARK: IAuthentificatorInterceptor - public func adapt(request: URLRequest, for session: URLSession) async throws { + public func adapt(request: inout URLRequest, for session: URLSession) async throws { guard let credential else { throw AuthenticatorInterceptorError.missingCredential } diff --git a/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/RequestParametersEncoder.swift b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/RequestParametersEncoder.swift index 76963ef..863e8f2 100644 --- a/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/RequestParametersEncoder.swift +++ b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/RequestParametersEncoder.swift @@ -11,6 +11,10 @@ struct RequestParametersEncoder: IRequestParametersEncoder { throw URLError(.badURL) } + if parameters.isEmpty { + return + } + let queries = parameters.map { URLQueryItem(name: $0.key, value: $0.value) } var urlComponents = URLComponents(string: url.absoluteString) diff --git a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift index 7b6e102..64f3400 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift @@ -35,7 +35,6 @@ final class DataRequestHandler: NSObject { extension DataRequestHandler: IDataRequestHandler { func startDataTask( _ task: URLSessionDataTask, - session _: URLSession, delegate: URLSessionDelegate? ) async throws -> Response { try await withTaskCancellationHandler(operation: { diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index ed520c7..a4a7f41 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -51,6 +51,7 @@ actor RequestProcessor { self.retryPolicyService = retryPolicyService self.delegate = delegate self.interceptor = interceptor + self.dataRequestHandler.urlSessionDelegate = configuration.sessionDelegate session = URLSession( configuration: configuration.sessionConfiguration, delegate: dataRequestHandler, @@ -76,11 +77,11 @@ actor RequestProcessor { delegate: URLSessionDelegate?, configure: ((inout URLRequest) throws -> Void)? ) async throws -> Response { - guard let urlRequest = try requestBuilder.build(request, configure) else { + guard var urlRequest = try requestBuilder.build(request, configure) else { throw NetworkLayerError.badURL } - try await adapt(request, urlRequest: urlRequest, session: session) + try await adapt(request, urlRequest: &urlRequest, session: session) return try await performRequest(strategy: strategy) { try await self.delegate?.requestProcessor(self, willSendRequest: urlRequest) @@ -88,7 +89,7 @@ actor RequestProcessor { let task = session.dataTask(with: urlRequest) do { - let response = try await dataRequestHandler.startDataTask(task, session: session, delegate: delegate) + let response = try await dataRequestHandler.startDataTask(task, delegate: delegate) if request.requiresAuthentication { let isRefreshedCredential = try await refresh( @@ -102,6 +103,8 @@ actor RequestProcessor { } } + try self.validate(response) + return response } catch { throw error @@ -109,11 +112,25 @@ actor RequestProcessor { } } - private func adapt(_ request: T, urlRequest: URLRequest, session: URLSession) async throws { + /// Adapts an initial request. + /// + /// - Parameters: + /// - request: The request model. + /// - urlRequest: The request that needs to be authenticated. + /// - session: The URLSession for which the request is being refreshed. + private func adapt(_ request: T, urlRequest: inout URLRequest, session: URLSession) async throws { guard request.requiresAuthentication else { return } - try await interceptor?.adapt(request: urlRequest, for: session) + try await interceptor?.adapt(request: &urlRequest, for: session) } + /// Refreshes credential. + /// + /// - Parameters: + /// - urlRequest: The request that needs to be authenticated. + /// - response: The metadata associated with the response to an HTTP protocol URL load request. + /// - session: The URLSession for which the request is being refreshed. + /// + /// - Returns: `true` if the request's token is refreshed, false otherwise. private func refresh( urlRequest: URLRequest, response: Response, @@ -146,6 +163,11 @@ actor RequestProcessor { return try await retryPolicyService.retry(strategy: strategy, send) } } + + private func validate(_ response: Response) throws { + guard let urlResponse = response.response as? HTTPURLResponse else { return } + try delegate?.requestProcessor(self, validateResponse: urlResponse, data: response.data, task: response.task) + } } // MARK: IRequestProcessor diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift index b90c4f0..63a8cfd 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift @@ -12,7 +12,7 @@ public protocol IAuthenticatorInterceptor { /// - Parameters: /// - request: The URLRequest to be adapted. /// - session: The URLSession for which the request is being adapted. - func adapt(request: URLRequest, for session: URLSession) async throws + func adapt(request: inout URLRequest, for session: URLSession) async throws /// Refreshes credential for the request. /// diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IDataRequestHandler.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IDataRequestHandler.swift index 8baa9d6..7fb0299 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IDataRequestHandler.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IDataRequestHandler.swift @@ -7,17 +7,17 @@ import Foundation /// A protocol for handling data requests. public protocol IDataRequestHandler: URLSessionTaskDelegate & URLSessionDataDelegate { + var urlSessionDelegate: URLSessionDelegate? { get set } + /// Starts a data task for handling network requests. /// /// - Parameters: /// - task: The `URLSessionDataTask` representing the network task to be initiated. - /// - session: The `URLSession` to use for the data task. /// - delegate: An optional `URLSessionDelegate` for handling `URLSession` events and callbacks. Pass `nil` if not needed. /// /// - Returns: An asynchronous task that will result in a Response object containing data when the request is completed. func startDataTask( _ task: URLSessionDataTask, - session: URLSession, delegate: URLSessionDelegate? ) async throws -> Response } diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift index 24f6ea2..f1bc2f1 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift @@ -5,6 +5,8 @@ import Foundation +// MARK: - RequestProcessorDelegate + /// A protocol to define the delegate methods for handling network requests. public protocol RequestProcessorDelegate: AnyObject { /// Notifies the delegate that the request processor is about to send a request. @@ -13,4 +15,22 @@ public protocol RequestProcessorDelegate: AnyObject { /// - requestProcessor: The request processor responsible for handling the request. /// - request: The URLRequest about to be sent. func requestProcessor(_ requestProcessor: IRequestProcessor, willSendRequest request: URLRequest) async throws + + /// Notifies the delegate that the request processor received a response and provides an opportunity to validate it. + /// + /// - Parameters: + /// - requestProcessor: The request processor responsible for handling the request. + /// - response: The HTTPURLResponse received from the server. + /// - data: The data received in the response. + /// - task: The URLSessionTask associated with the request. + func requestProcessor( + _ requestProcessor: IRequestProcessor, + validateResponse response: HTTPURLResponse, + data: Data, + task: URLSessionTask + ) throws +} + +public extension RequestProcessorDelegate { + func requestProcessor(_: IRequestProcessor, willSendRequest _: URLRequest) async throws {} } diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Fakes/HTTPURLResponse+Fake.swift b/Tests/NetworkLayerTests/Classes/Helpers/Fakes/HTTPURLResponse+Fake.swift new file mode 100644 index 0000000..ec46c14 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Helpers/Fakes/HTTPURLResponse+Fake.swift @@ -0,0 +1,12 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +extension HTTPURLResponse { + static func fake() -> HTTPURLResponse { + HTTPURLResponse() + } +} diff --git a/Tests/NetworkLayerTests/Fakes/URLRequest+Fake.swift b/Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLRequest+Fake.swift similarity index 100% rename from Tests/NetworkLayerTests/Fakes/URLRequest+Fake.swift rename to Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLRequest+Fake.swift diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLSessionDataTask+Fake.swift b/Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLSessionDataTask+Fake.swift new file mode 100644 index 0000000..d289807 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLSessionDataTask+Fake.swift @@ -0,0 +1,13 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +extension URLSessionDataTask { + @objc override dynamic + class func fake() -> URLSessionDataTask { + URLSession.shared.dataTask(with: .fake()) + } +} diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLSessionTask+Fake.swift b/Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLSessionTask+Fake.swift new file mode 100644 index 0000000..25801c3 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLSessionTask+Fake.swift @@ -0,0 +1,13 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +extension URLSessionTask { + @objc dynamic + class func fake() -> URLSessionTask { + URLSession.shared.dataTask(with: .fake()) + } +} diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Helpers/DynamicStubs.swift b/Tests/NetworkLayerTests/Classes/Helpers/Helpers/DynamicStubs.swift new file mode 100644 index 0000000..20c7904 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Helpers/Helpers/DynamicStubs.swift @@ -0,0 +1,23 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import Mocker + +final class DynamicStubs { + static func register(stubs: [StubResponse], prefix: String = "https://github.com", statusCode: Int = 200) { + for stub in stubs { + let mock = Mock( + url: URL(string: [prefix, stub.name].joined(separator: "/"))!, + dataType: .json, + statusCode: statusCode, + data: [ + stub.httpMethod: try! Data(contentsOf: stub.fileURL), + ] + ) + mock.register() + } + } +} diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Helpers/RequestProcessor+Mock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Helpers/RequestProcessor+Mock.swift new file mode 100644 index 0000000..c974399 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Helpers/Helpers/RequestProcessor+Mock.swift @@ -0,0 +1,46 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import Mocker +@testable import NetworkLayer +import NetworkLayerInterfaces +import Typhoon + +extension RequestProcessor { + static func mock( + requestProcessorDelegate: RequestProcessorDelegate? = nil, + interceptor: IAuthenticatorInterceptor? = nil + ) -> RequestProcessor { + RequestProcessor( + configuration: .init( + sessionConfiguration: sessionConfiguration, + sessionDelegate: nil, + sessionDelegateQueue: nil, + jsonDecoder: jsonDecoder + ), + requestBuilder: RequestBuilder( + parametersEncoder: RequestParametersEncoder(), + requestBodyEncoder: RequestBodyEncoder(jsonEncoder: JSONEncoder()) + ), + dataRequestHandler: DataRequestHandler(), + retryPolicyService: RetryPolicyService(strategy: .constant(retry: 1, duration: .seconds(0))), + delegate: requestProcessorDelegate, + interceptor: interceptor + ) + } + + private static var sessionConfiguration: URLSessionConfiguration { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockingURLProtocol.self] + return configuration + } + + private static var jsonDecoder: JSONDecoder { + let jsonDecoder = JSONDecoder() + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + return jsonDecoder + } +} diff --git a/Tests/NetworkLayerTests/Mocks/AuthenticatorMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthenticatorMock.swift similarity index 100% rename from Tests/NetworkLayerTests/Mocks/AuthenticatorMock.swift rename to Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthenticatorMock.swift diff --git a/Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift similarity index 96% rename from Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift rename to Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift index b5561a2..1acfc67 100644 --- a/Tests/NetworkLayerTests/Mocks/AuthentificatorInterceptorMock.swift +++ b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift @@ -12,7 +12,7 @@ final class AuthentificatorInterceptorMock: IAuthenticatorInterceptor { var invokedAdaptParameters: (request: URLRequest, session: URLSession)? var invokedAdaptParametersList = [(request: URLRequest, session: URLSession)]() - func adapt(request: URLRequest, for session: URLSession) { + func adapt(request: inout URLRequest, for session: URLSession) { invokedAdapt = true invokedAdaptCount += 1 invokedAdaptParameters = (request, session) diff --git a/Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/DataRequestHandlerMock.swift similarity index 51% rename from Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift rename to Tests/NetworkLayerTests/Classes/Helpers/Mocks/DataRequestHandlerMock.swift index 3d3c379..8889a5c 100644 --- a/Tests/NetworkLayerTests/Mocks/DataRequestHandlerMock.swift +++ b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/DataRequestHandlerMock.swift @@ -7,22 +7,38 @@ import Foundation import NetworkLayerInterfaces final class DataRequestHandlerMock: NSObject, IDataRequestHandler { + var invokedUrlSessionGetDelegate = false + var invokedUrlSessionGetDelegateCount = 0 + var invokedUrlSessionSetDelegate = false + var invokedUrlSessionSetDelegateCount = 0 + var stubbedUrlSessionDelegate: URLSessionDelegate? + var urlSessionDelegate: URLSessionDelegate? { + get { + invokedUrlSessionGetDelegate = true + invokedUrlSessionGetDelegateCount += 1 + return stubbedUrlSessionDelegate + } + set { + invokedUrlSessionSetDelegate = true + invokedUrlSessionSetDelegateCount += 1 + } + } + var invokedStartDataTask = false var invokedStartDataTaskCount = 0 - var invokedStartDataTaskParameters: (task: URLSessionDataTask, session: URLSession, delegate: URLSessionDelegate?)? - var invokedStartDataTaskParametersList = [(task: URLSessionDataTask, session: URLSession, delegate: URLSessionDelegate?)]() + var invokedStartDataTaskParameters: (task: URLSessionDataTask, delegate: URLSessionDelegate?)? + var invokedStartDataTaskParametersList = [(task: URLSessionDataTask, delegate: URLSessionDelegate?)]() var stubbedStartDataTask: Response! var startDataTaskThrowError: Error? func startDataTask( _ task: URLSessionDataTask, - session: URLSession, delegate: URLSessionDelegate? ) async throws -> Response { invokedStartDataTask = true invokedStartDataTaskCount += 1 - invokedStartDataTaskParameters = (task, session, delegate) - invokedStartDataTaskParametersList.append((task, session, delegate)) + invokedStartDataTaskParameters = (task, delegate) + invokedStartDataTaskParametersList.append((task, delegate)) if let error = startDataTaskThrowError { throw error } diff --git a/Tests/NetworkLayerTests/Mocks/RequestBodyEncoderMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestBodyEncoderMock.swift similarity index 100% rename from Tests/NetworkLayerTests/Mocks/RequestBodyEncoderMock.swift rename to Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestBodyEncoderMock.swift diff --git a/Tests/NetworkLayerTests/Mocks/RequestBuilderMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestBuilderMock.swift similarity index 100% rename from Tests/NetworkLayerTests/Mocks/RequestBuilderMock.swift rename to Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestBuilderMock.swift diff --git a/Tests/NetworkLayerTests/Mocks/RequestMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestMock.swift similarity index 100% rename from Tests/NetworkLayerTests/Mocks/RequestMock.swift rename to Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestMock.swift diff --git a/Tests/NetworkLayerTests/Mocks/RequestParametersEncoderMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestParametersEncoderMock.swift similarity index 100% rename from Tests/NetworkLayerTests/Mocks/RequestParametersEncoderMock.swift rename to Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestParametersEncoderMock.swift diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestProcessorDelegateMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestProcessorDelegateMock.swift new file mode 100644 index 0000000..0e248e5 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestProcessorDelegateMock.swift @@ -0,0 +1,52 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +final class RequestProcessorDelegateMock: RequestProcessorDelegate { + var invokedRequestProcessor = false + var invokedRequestProcessorCount = 0 + var invokedRequestProcessorParameters: (requestProcessor: IRequestProcessor, request: URLRequest)? + var invokedRequestProcessorParametersList = [(requestProcessor: IRequestProcessor, request: URLRequest)]() + + func requestProcessor(_ requestProcessor: IRequestProcessor, willSendRequest request: URLRequest) async throws { + invokedRequestProcessor = true + invokedRequestProcessorCount += 1 + invokedRequestProcessorParameters = (requestProcessor, request) + invokedRequestProcessorParametersList.append((requestProcessor, request)) + } + + var invokedRequestProcessorValidateResponse = false + var invokedRequestProcessorValidateResponseCount = 0 + var invokedRequestProcessorValidateResponseParameters: ( + requestProcessor: IRequestProcessor, + response: HTTPURLResponse, + data: Data, + task: URLSessionTask + )? + var invokedRequestProcessorValidateResponseParametersList = [( + requestProcessor: IRequestProcessor, + response: HTTPURLResponse, + data: Data, + task: URLSessionTask + )]() + var stubbedRequestProcessorValidateResponseError: Error? + + func requestProcessor( + _ requestProcessor: IRequestProcessor, + validateResponse response: HTTPURLResponse, + data: Data, + task: URLSessionTask + ) throws { + invokedRequestProcessorValidateResponse = true + invokedRequestProcessorValidateResponseCount += 1 + invokedRequestProcessorValidateResponseParameters = (requestProcessor, response, data, task) + invokedRequestProcessorValidateResponseParametersList.append((requestProcessor, response, data, task)) + if let error = stubbedRequestProcessorValidateResponseError { + throw error + } + } +} diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Mocks/URLSessionDelegateMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/URLSessionDelegateMock.swift new file mode 100644 index 0000000..98e2694 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/URLSessionDelegateMock.swift @@ -0,0 +1,192 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +final class URLSessionDelegateMock: NSObject, URLSessionDataDelegate { + var invokedUrlSessionDidBecomeInvalidWithError = false + var invokedUrlSessionDidBecomeInvalidWithErrorCount = 0 + var invokedUrlSessionDidBecomeInvalidWithErrorParameters: (session: URLSession, error: Error?)? + var invokedUrlSessionDidBecomeInvalidWithErrorParametersList = [(session: URLSession, error: Error?)]() + + func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + invokedUrlSessionDidBecomeInvalidWithError = true + invokedUrlSessionDidBecomeInvalidWithErrorCount += 1 + invokedUrlSessionDidBecomeInvalidWithErrorParameters = (session, error) + invokedUrlSessionDidBecomeInvalidWithErrorParametersList.append((session, error)) + } + + var invokedUrlSessionWillCacheResponse = false + var invokedUrlSessionWillCacheResponseCount = 0 + var invokedUrlSessionWillCacheResponseParameters: ( + session: URLSession, + dataTask: URLSessionDataTask, + proposedResponse: CachedURLResponse, + completionHandler: (CachedURLResponse?) -> Void + )? + var invokedUrlSessionWillCacheResponseParametersList = [( + session: URLSession, + dataTask: URLSessionDataTask, + proposedResponse: CachedURLResponse, + completionHandler: (CachedURLResponse?) -> Void + )]() + func urlSession( + _ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse, + completionHandler: @escaping (CachedURLResponse?) -> Void + ) { + invokedUrlSessionWillCacheResponse = true + invokedUrlSessionWillCacheResponseCount += 1 + invokedUrlSessionWillCacheResponseParameters = (session, dataTask, proposedResponse, completionHandler) + invokedUrlSessionWillCacheResponseParametersList.append((session, dataTask, proposedResponse, completionHandler)) + } + + var invokedUrlSessionDidFinishCollectingMetrics = false + var invokedUrlSessionDidFinishCollectingMetricsCount = 0 + var invokedUrlSessionDidFinishCollectingMetricsParameters: (session: URLSession, task: URLSessionTask, metrics: URLSessionTaskMetrics)? + var invokedUrlSessionDidFinishCollectingMetricsParametersList = [( + session: URLSession, + task: URLSessionTask, + metrics: URLSessionTaskMetrics + )]() + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + invokedUrlSessionDidFinishCollectingMetrics = true + invokedUrlSessionDidFinishCollectingMetricsCount += 1 + invokedUrlSessionDidFinishCollectingMetricsParameters = (session, task, metrics) + invokedUrlSessionDidFinishCollectingMetricsParametersList.append((session, task, metrics)) + } + + var invokedUrlSessionWillPerformHTTPRedirection = false + var invokedUrlSessionWillPerformHTTPRedirectionCount = 0 + var invokedUrlSessionWillPerformHTTPRedirectionParameters: ( + session: URLSession, + task: URLSessionTask, + response: HTTPURLResponse, + request: URLRequest, + completionHandler: (URLRequest?) -> Void + )? + var invokedUrlSessionWillPerformHTTPRedirectionParametersList = [( + session: URLSession, + task: URLSessionTask, + response: HTTPURLResponse, + request: URLRequest, + completionHandler: (URLRequest?) -> Void + )]() + func urlSession( + _ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void + ) { + invokedUrlSessionWillPerformHTTPRedirection = true + invokedUrlSessionWillPerformHTTPRedirectionCount += 1 + invokedUrlSessionWillPerformHTTPRedirectionParameters = (session, task, response, request, completionHandler) + invokedUrlSessionWillPerformHTTPRedirectionParametersList = [(session, task, response, request, completionHandler)] + } + + var invokedUrlSessionTaskIsWaitingForConnectivity = false + var invokedUrlSessionTaskIsWaitingForConnectivityCount = 0 + var invokedUrlSessionTaskIsWaitingForConnectivityParameters: (session: URLSession, task: URLSessionTask)? + var invokedUrlSessionTaskIsWaitingForConnectivityParametersList = [(session: URLSession, task: URLSessionTask)]() + func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + invokedUrlSessionTaskIsWaitingForConnectivity = true + invokedUrlSessionTaskIsWaitingForConnectivityCount += 1 + invokedUrlSessionTaskIsWaitingForConnectivityParameters = (session, task) + invokedUrlSessionTaskIsWaitingForConnectivityParametersList.append((session, task)) + } + + var invokedUrlSessionDidReceiveChallenge = false + var invokedUrlSessionDidReceiveChallengeCount = 0 + var invokedUrlSessionDidReceiveChallengeParamters: ( + session: URLSession, + challenge: URLAuthenticationChallenge, + completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + )? + var invokedUrlSessionDidReceiveChallengeParamtersList = [( + session: URLSession, + challenge: URLAuthenticationChallenge, + completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + )]() + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + invokedUrlSessionDidReceiveChallenge = true + invokedUrlSessionDidReceiveChallengeCount += 1 + invokedUrlSessionDidReceiveChallengeParamters = (session, challenge, completionHandler) + invokedUrlSessionDidReceiveChallengeParamtersList.append((session, challenge, completionHandler)) + } + + var invokedUrlSessionWillBeginDelayedRequest = false + var invokedUrlSessionWillBeginDelayedRequestCount = 0 + var invokedUrlSessionWillBeginDelayedRequestParameters: ( + session: URLSession, + task: URLSessionTask, + request: URLRequest, + completionHandler: (URLSession.DelayedRequestDisposition, URLRequest?) -> Void + )? + var invokedUrlSessionWillBeginDelayedRequestParametersList = [( + session: URLSession, + task: URLSessionTask, + request: URLRequest, + completionHandler: (URLSession.DelayedRequestDisposition, URLRequest?) -> Void + )]() + func urlSession( + _ session: URLSession, + task: URLSessionTask, + willBeginDelayedRequest request: URLRequest, + completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void + ) { + invokedUrlSessionWillBeginDelayedRequest = true + invokedUrlSessionWillBeginDelayedRequestCount += 1 + invokedUrlSessionWillBeginDelayedRequestParameters = (session, task, request, completionHandler) + invokedUrlSessionWillBeginDelayedRequestParametersList.append((session, task, request, completionHandler)) + } + + var invokedUrlSessionDidSendBodyData = false + var invokedUrlSessionDidSendBodyDataCount = 0 + var invokedUrlSessionDidSendBodyDataParameters: ( + session: URLSession, + task: URLSessionTask, + bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64 + )? + var invokedUrlSessionDidSendBodyDataParametersList = [ + ( + session: URLSession, + task: URLSessionTask, + bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64 + ) + ]() + func urlSession( + _ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64 + ) { + invokedUrlSessionDidSendBodyData = true + invokedUrlSessionDidSendBodyDataCount += 1 + invokedUrlSessionDidSendBodyDataParameters = (session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend) + invokedUrlSessionDidSendBodyDataParametersList = [(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)] + } + + var invokedUrlSessionDidCompleteTaskWithError = false + var invokedUrlSessionDidCompleteTaskWithErrorCount = 0 + var invokedUrlSessionDidCompleteTaskWithErrorParameters: (session: URLSession, task: URLSessionTask, error: Error?)? + var invokedUrlSessionDidCompleteTaskWithErrorParametersList = [(session: URLSession, task: URLSessionTask, error: Error?)]() + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + invokedUrlSessionDidCompleteTaskWithError = true + invokedUrlSessionDidCompleteTaskWithErrorCount += 1 + invokedUrlSessionDidCompleteTaskWithErrorParameters = (session, task, error) + invokedUrlSessionDidCompleteTaskWithErrorParametersList.append((session, task, error)) + } +} diff --git a/Tests/NetworkLayerTests/Stubs/AuthenticationCredentialStub.swift b/Tests/NetworkLayerTests/Classes/Helpers/Stubs/AuthenticationCredentialStub.swift similarity index 100% rename from Tests/NetworkLayerTests/Stubs/AuthenticationCredentialStub.swift rename to Tests/NetworkLayerTests/Classes/Helpers/Stubs/AuthenticationCredentialStub.swift diff --git a/Tests/NetworkLayerTests/Stubs/RequestStub.swift b/Tests/NetworkLayerTests/Classes/Helpers/Stubs/RequestStub.swift similarity index 100% rename from Tests/NetworkLayerTests/Stubs/RequestStub.swift rename to Tests/NetworkLayerTests/Classes/Helpers/Stubs/RequestStub.swift diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Stubs/StubResponse.swift b/Tests/NetworkLayerTests/Classes/Helpers/Stubs/StubResponse.swift new file mode 100644 index 0000000..97f4471 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Helpers/Stubs/StubResponse.swift @@ -0,0 +1,13 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import Mocker + +struct StubResponse { + let name: String + let fileURL: URL + let httpMethod: Mock.HTTPMethod +} diff --git a/Tests/NetworkLayerTests/Classes/Models/MockedData.swift b/Tests/NetworkLayerTests/Classes/Models/MockedData.swift new file mode 100644 index 0000000..3a44797 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Models/MockedData.swift @@ -0,0 +1,10 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +public enum MockedData { + public static let userJSON: URL = Bundle.module.url(forResource: "Resources/JSONs/user", withExtension: "json")! +} diff --git a/Tests/NetworkLayerTests/Classes/Models/User.swift b/Tests/NetworkLayerTests/Classes/Models/User.swift new file mode 100644 index 0000000..eb7e938 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Models/User.swift @@ -0,0 +1,13 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +struct User: Codable { + let id: Int + let login: String? + let avatarUrl: String? + let type: String? +} diff --git a/Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorAuthenticationTests.swift b/Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorAuthenticationTests.swift new file mode 100644 index 0000000..eed185e --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorAuthenticationTests.swift @@ -0,0 +1,170 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +@testable import NetworkLayer +import NetworkLayerInterfaces +import Typhoon +import XCTest + +// MARK: - RequestProcessorAuthenicationTests + +final class RequestProcessorAuthenicationTests: XCTestCase { + // MARK: Tests + + func test_thatRequestProcessorAuthenticatesRequest_whenTokenIsCorrect() async throws { + // given + let interceptor = AuthInterceptor() + let sut = RequestProcessor.mock(interceptor: interceptor) + + interceptor.token = .init(token: .token, expiresDate: Date()) + + DynamicStubs.register(stubs: [.user], statusCode: 200) + + let request = makeRequest(.user) + + // when + let _: Response = try await sut.send(request) + } + + func test_thatRequestProcessorAuthenticatesRequest_whenTokenIsInvalid() async throws { + // given + let interceptor = AuthInterceptor() + let sut = RequestProcessor.mock(interceptor: interceptor) + + interceptor.token = .init(token: .token, expiresDate: Date()) + + DynamicStubs.register(stubs: [.user], statusCode: 401) + + let request = makeRequest(.user) + + // when + let _: Response = try await sut.send(request) + } + + func test_thatRequestProcessorAuthenticatesRequest_whenTokenExpired() async throws { + // given + let interceptor = AuthInterceptor() + let sut = RequestProcessor.mock(interceptor: interceptor) + + interceptor.token = .init(token: .token, expiresDate: Date(timeIntervalSinceNow: 1001)) + + DynamicStubs.register(stubs: [.user], statusCode: 200) + + let request = makeRequest(.user) + + // when + let _: Response = try await sut.send(request) + } + + func test_thatRequestProcessorThrowsAnError_whenInterceptorAdaptDidFail() async throws { + try await test_failAuthentication(adaptError: URLError(.unknown), refreshError: nil, expectedError: URLError(.unknown)) + } + + func test_thatRequestProcessorThrowsAnError_whenInterceptorRefreshDidFail() async throws { + try await test_failAuthentication( + adaptError: nil, + refreshError: URLError(.unknown), + expectedError: RetryPolicyError.retryLimitExceeded + ) + } + + // MARK: Private + + private func test_failAuthentication(adaptError: Error?, refreshError: Error?, expectedError: Error) async throws { + class FailInterceptor: IAuthenticatorInterceptor { + let adaptError: Error? + let refreshError: Error? + + init(adaptError: Error?, refreshError: Error?) { + self.adaptError = adaptError + self.refreshError = refreshError + } + + func adapt(request _: inout URLRequest, for _: URLSession) async throws { + guard let adaptError = adaptError else { return } + throw adaptError + } + + func refresh(_: URLRequest, with _: HTTPURLResponse, for _: URLSession) async throws { + guard let refreshError = refreshError else { return } + throw refreshError + } + + func isRequireRefresh(_: URLRequest, response _: HTTPURLResponse) -> Bool { + true + } + } + + // given + let interceptor = FailInterceptor(adaptError: adaptError, refreshError: refreshError) + let sut = RequestProcessor.mock(interceptor: interceptor) + + let request = makeRequest(.user) + + DynamicStubs.register(stubs: [.user], statusCode: 200) + + // when + do { + let _: Response = try await sut.send(request) + } catch { + XCTAssertEqual(error as NSError, expectedError as NSError) + } + } + + private func makeRequest(_ path: String) -> IRequest { + let request = RequestStub() + request.stubbedDomainName = "https://github.com" + request.stubbedPath = path + request.stubbedRequiresAuthentication = true + return request + } +} + +// MARK: - AuthInterceptor + +private final class AuthInterceptor: IAuthenticatorInterceptor { + var token: Token! + + private var attempts = 0 + + func adapt(request: inout URLRequest, for _: URLSession) async throws { + request.addValue("Bearer \(token.token)", forHTTPHeaderField: "Authorization") + } + + func refresh(_: URLRequest, with _: HTTPURLResponse, for _: URLSession) async throws { + if token.expiresDate < Date() { + token = Token(token: .token, expiresDate: Date(timeIntervalSinceNow: 1000)) + } + } + + func isRequireRefresh(_: URLRequest, response: HTTPURLResponse) -> Bool { + if response.statusCode == 401, attempts == 0 { + attempts += 1 + return true + } + return false + } +} + +// MARK: - Token + +private struct Token { + let token: String + let expiresDate: Date +} + +// MARK: - Stubs + +private extension StubResponse { + static let user = StubResponse(name: .user, fileURL: MockedData.userJSON, httpMethod: .get) +} + +// MARK: - Constants + +private extension String { + static let user = "user" + static let token = "token" +} diff --git a/Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorRequestTests.swift b/Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorRequestTests.swift new file mode 100644 index 0000000..8548539 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorRequestTests.swift @@ -0,0 +1,82 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation +import Mocker +@testable import NetworkLayer +import NetworkLayerInterfaces +import Typhoon +import XCTest + +// MARK: - RequestProcessorRequestTests + +final class RequestProcessorRequestTests: XCTestCase { + // MARK: Tests + + func test_thatRequestProcessorSendsASimpleRequest() async throws { + // given + DynamicStubs.register(stubs: [.user]) + + let sut = RequestProcessor.mock() + let request = makeRequest(.user) + + // when + let user: Response = try await sut.send(request) + + // then + XCTAssertEqual(user.data.id, 1) + XCTAssertNotNil(user.data.avatarUrl) + } + + func test_thatRequestProcessorThrowsRretryLimitExceededError_whenRequestDidFail() async { + // given + DynamicStubs.register(stubs: [.user], statusCode: 500) + + let delegate = GitHubDelegate() + let sut = RequestProcessor.mock(requestProcessorDelegate: delegate) + let request = makeRequest(.user) + + // when + do { + let _: Response = try await sut.send(request) + } catch { + XCTAssertEqual(error as NSError, RetryPolicyError.retryLimitExceeded as NSError) + } + } + + // MARK: Private + + private func makeRequest(_ path: String) -> IRequest { + let request = RequestStub() + request.stubbedDomainName = "https://github.com" + request.stubbedPath = path + return request + } +} + +// MARK: - GitHubDelegate + +private final class GitHubDelegate: RequestProcessorDelegate { + func requestProcessor( + _: NetworkLayerInterfaces.IRequestProcessor, + validateResponse response: HTTPURLResponse, + data _: Data, + task _: URLSessionTask + ) throws { + if !(200 ..< 300).contains(response.statusCode) { + throw URLError(.unknown) + } + } +} + +// MARK: - Stubs + +private extension StubResponse { + static let user = StubResponse(name: .user, fileURL: MockedData.userJSON, httpMethod: .get) +} + +private extension String { + static let user = "user" +} diff --git a/Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/AuthenticationInterceptorTests.swift similarity index 92% rename from Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift rename to Tests/NetworkLayerTests/Classes/Tests/UnitTests/AuthenticationInterceptorTests.swift index 880aa25..c27bffa 100644 --- a/Tests/NetworkLayerTests/UnitTests/AuthenticationInterceptorTests.swift +++ b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/AuthenticationInterceptorTests.swift @@ -35,12 +35,12 @@ final class AuthenticationInterceptorTests: XCTestCase { func test_thatAuthenticatorInterceptorThrowsAnErrorOnAdaptRequest_whenCredentialIsMissing() async throws { // given - let requestMock = URLRequest.fake() + var requestMock = URLRequest.fake() // when var receivedError: NSError? do { - try await sut.adapt(request: requestMock, for: .shared) + try await sut.adapt(request: &requestMock, for: .shared) } catch { receivedError = error as NSError } @@ -52,13 +52,13 @@ final class AuthenticationInterceptorTests: XCTestCase { func test_thatAuthenticatorInterceptorAdaptsRequest_whenCredentialIsNotMissingAndValid() async throws { // given let credentialStub = AuthenticationCredentialStub() - let requestMock = URLRequest.fake() + var requestMock = URLRequest.fake() credentialStub.stubbedRequiresRefresh = false sut.credential = credentialStub // when - try await sut.adapt(request: requestMock, for: .shared) + try await sut.adapt(request: &requestMock, for: .shared) // then XCTAssertEqual(authenticatorMock.invokedApplyCount, 1) @@ -66,7 +66,7 @@ final class AuthenticationInterceptorTests: XCTestCase { func test_thatAuthenticatorInterceptorAdaptsRequest_whenCredentialIsNotMissingAndNotValid() async throws { // given - let requestMock = URLRequest.fake() + var requestMock = URLRequest.fake() let credentialStub = AuthenticationCredentialStub() credentialStub.stubbedRequiresRefresh = true @@ -75,7 +75,7 @@ final class AuthenticationInterceptorTests: XCTestCase { sut.credential = credentialStub // when - try await sut.adapt(request: requestMock, for: .shared) + try await sut.adapt(request: &requestMock, for: .shared) // then XCTAssertEqual(authenticatorMock.invokedRefreshCount, 1) diff --git a/Tests/NetworkLayerTests/Classes/Tests/UnitTests/DataRequestHanderTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/DataRequestHanderTests.swift new file mode 100644 index 0000000..0dcb032 --- /dev/null +++ b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/DataRequestHanderTests.swift @@ -0,0 +1,105 @@ +// +// network-layer +// Copyright © 2023 Space Code. All rights reserved. +// + +@testable import NetworkLayer +import XCTest + +final class DataRequestHanderTests: XCTestCase { + // MARK: Properties + + private var delegateMock: URLSessionDelegateMock! + + private var sut: DataRequestHandler! + + // MARK: XCTestCase + + override func setUp() { + super.setUp() + delegateMock = URLSessionDelegateMock() + sut = DataRequestHandler() + sut.urlSessionDelegate = delegateMock + } + + override func tearDown() { + delegateMock = nil + sut = nil + super.tearDown() + } + + // MARK: Tests + + func test_thatDataRequestHandlerTriggersDelegate_whenSessionDidBecomeInvalidWithError() { + // given + let errorMock = URLError(.unknown) + + // when + sut.urlSession(.shared, didBecomeInvalidWithError: errorMock) + + // then + XCTAssertTrue(delegateMock.invokedUrlSessionDidBecomeInvalidWithError) + XCTAssertEqual(delegateMock.invokedUrlSessionDidBecomeInvalidWithErrorParameters?.error as? URLError, errorMock) + } + + func test_thatDataRequestHandlerTriggersDelegate_whenSessionWillCacheResponseWithCompletionHandler() { + // when + sut.urlSession( + .shared, + dataTask: .fake(), + willCacheResponse: .init(), + completionHandler: { _ in } + ) + + // then + XCTAssertTrue(delegateMock.invokedUrlSessionWillCacheResponse) + } + + func test_thatDataRequestHandlerTriggersDelegate_whenSessionDidFinishCollectingMetrics() { + // when + sut.urlSession(.shared, task: .fake(), didFinishCollecting: .init()) + + // then + XCTAssertTrue(delegateMock.invokedUrlSessionDidFinishCollectingMetrics) + } + + func test_thatDataRequestHandlerTriggersDelegate_whenSessionWillPerformHTTPRedirection() { + // when + sut.urlSession(.shared, task: .fake(), willPerformHTTPRedirection: .fake(), newRequest: .fake(), completionHandler: { _ in }) + + // then + XCTAssertTrue(delegateMock.invokedUrlSessionWillPerformHTTPRedirection) + } + + func test_thatDataRequestHandlerTriggersDelegate_whenSessionTaskIsWaitingForConnectivity() { + // when + sut.urlSession(.shared, taskIsWaitingForConnectivity: .fake()) + + // then + XCTAssertTrue(delegateMock.invokedUrlSessionTaskIsWaitingForConnectivity) + } + + func test_thatDataRequestHandlerTriggersDelegate_whenSessionDidReceiveChallenge() { + // when + sut.urlSession(.shared, didReceive: .init(), completionHandler: { _, _ in }) + + // then + XCTAssertTrue(delegateMock.invokedUrlSessionDidReceiveChallenge) + } + + func test_thatDataRequestHandlerTriggersDelegate_whenSessionWillBeginDelayedRequest() { + // when + sut.urlSession(.shared, task: .fake(), willBeginDelayedRequest: .fake(), completionHandler: { _, _ in }) + + // then + XCTAssertTrue(delegateMock.invokedUrlSessionWillBeginDelayedRequest) + } + + func test_thatDataRequestHandlerTriggersDelegate_whenSessionDidSendBodyData() { + // when + sut.urlSession(.shared, task: .fake(), didSendBodyData: .zero, totalBytesSent: .zero, totalBytesExpectedToSend: .zero) + + // then + XCTAssertTrue(delegateMock.invokedUrlSessionDidSendBodyData) + } +} diff --git a/Tests/NetworkLayerTests/UnitTests/RequestBodyEncoderTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestBodyEncoderTests.swift similarity index 100% rename from Tests/NetworkLayerTests/UnitTests/RequestBodyEncoderTests.swift rename to Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestBodyEncoderTests.swift diff --git a/Tests/NetworkLayerTests/UnitTests/RequestBuilderTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestBuilderTests.swift similarity index 100% rename from Tests/NetworkLayerTests/UnitTests/RequestBuilderTests.swift rename to Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestBuilderTests.swift diff --git a/Tests/NetworkLayerTests/UnitTests/RequestParametersEncoderTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestParametersEncoderTests.swift similarity index 100% rename from Tests/NetworkLayerTests/UnitTests/RequestParametersEncoderTests.swift rename to Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestParametersEncoderTests.swift diff --git a/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestProcessorTests.swift similarity index 96% rename from Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift rename to Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestProcessorTests.swift index 96035dd..a46d5a8 100644 --- a/Tests/NetworkLayerTests/UnitTests/RequestProcessorTests.swift +++ b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestProcessorTests.swift @@ -63,7 +63,7 @@ final class RequestProcessorTests: XCTestCase { func test_thatRequestProcessorSignsRequest_whenRequestRequiresAuthentication() async { // given requestBuilderMock.stubbedBuildResult = URLRequest.fake() - dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: .init(), task: URLSessionTask()) + dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: .init(), task: .fake()) let request = RequestMock() request.stubbedRequiresAuthentication = true @@ -81,7 +81,7 @@ final class RequestProcessorTests: XCTestCase { func test_thatRequestProcessorDoesNotSignRequest_whenRequestDoesNotRequireAuthentication() async { // given requestBuilderMock.stubbedBuildResult = URLRequest.fake() - dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: .init(), task: URLSessionTask()) + dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: .init(), task: .fake()) let request = RequestMock() request.stubbedRequiresAuthentication = false @@ -99,7 +99,7 @@ final class RequestProcessorTests: XCTestCase { func test_thatRequestProcessorRefreshesCredential_whenCredentialIsNotValid() async { // given requestBuilderMock.stubbedBuildResult = URLRequest.fake() - dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: HTTPURLResponse(), task: URLSessionTask()) + dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: HTTPURLResponse(), task: .fake()) interceptorMock.stubbedIsRequireRefreshResult = true let request = RequestMock() diff --git a/Tests/NetworkLayerTests/Mocks/RequestProcessorDelegateMock.swift b/Tests/NetworkLayerTests/Mocks/RequestProcessorDelegateMock.swift deleted file mode 100644 index 33e6be4..0000000 --- a/Tests/NetworkLayerTests/Mocks/RequestProcessorDelegateMock.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// network-layer -// Copyright © 2023 Space Code. All rights reserved. -// - -import Foundation -import NetworkLayerInterfaces - -final class RequestProcessorDelegateMock: RequestProcessorDelegate { - var invokedRequestProcessor = false - var invokedRequestProcessorCount = 0 - var invokedRequestProcessorParameters: (requestProcessor: IRequestProcessor, request: URLRequest)? - var invokedRequestProcessorParametersList = [(requestProcessor: IRequestProcessor, request: URLRequest)]() - - func requestProcessor(_ requestProcessor: IRequestProcessor, willSendRequest request: URLRequest) async throws { - invokedRequestProcessor = true - invokedRequestProcessorCount += 1 - invokedRequestProcessorParameters = (requestProcessor, request) - invokedRequestProcessorParametersList.append((requestProcessor, request)) - } -} diff --git a/Tests/NetworkLayerTests/Resources/JSONs/user.json b/Tests/NetworkLayerTests/Resources/JSONs/user.json new file mode 100644 index 0000000..21764ac --- /dev/null +++ b/Tests/NetworkLayerTests/Resources/JSONs/user.json @@ -0,0 +1,6 @@ +{ + "id": 1, + "login": "octocat", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "type": "User", +} From 91c3b8f7e53bb65157ba3a80f9fd163910fee035 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 30 Nov 2023 15:18:07 +0100 Subject: [PATCH 18/26] Update `README.md` --- README.md | 2 ++ Resources/network-layer.png | Bin 0 -> 315987 bytes 2 files changed, 2 insertions(+) create mode 100644 Resources/network-layer.png diff --git a/README.md b/README.md index 4439987..17b12f4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![NetworkLayer: Network communication made easy](https://raw.githubusercontent.com/space-code/network-layer/dev/Resources/network-layer.png) +

network-layer

diff --git a/Resources/network-layer.png b/Resources/network-layer.png new file mode 100644 index 0000000000000000000000000000000000000000..19d77b8f4b6569c4ae1c74b151e7391731f62aeb GIT binary patch literal 315987 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ⅈ(){dGgRyTvJ-$u^^~r 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 ^ikA4x83Re2!Rj|Y~Vmkg=_<7Km8GnK0 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 literal 0 HcmV?d00001 From 4eff40d0fb4c0d91558f2586137f65c800240ab2 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 30 Nov 2023 15:33:16 +0100 Subject: [PATCH 19/26] Change scheme name --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4224308..94f0d67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,19 +35,19 @@ jobs: include: - destination: "OS=16.1,name=iPhone 14 Pro" name: "iOS" - scheme: "network-layer" + scheme: "NetworkLayer-Package" sdk: iphonesimulator - destination: "OS=16.1,name=Apple TV" name: "tvOS" - scheme: "network-layer" + scheme: "NetworkLayer-Package" sdk: appletvsimulator - destination: "OS=9.1,name=Apple Watch Series 8 (45mm)" name: "watchOS" - scheme: "network-layer" + scheme: "NetworkLayer-Package" sdk: watchsimulator - destination: "platform=macOS" name: "macOS" - scheme: "network-layer" + scheme: "NetworkLayer-Package" sdk: macosx steps: - uses: actions/checkout@v3 From adf3317e9c3d1065372c711b75d873f928eb1ea0 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Fri, 1 Dec 2023 14:38:11 +0100 Subject: [PATCH 20/26] Update `Package.swift` --- .../xcschemes/NetworkLayer-Package.xcscheme | 116 ------------------ .../xcschemes/NetworkLayer.xcscheme | 24 ++++ ...es.xcscheme => NetworkLayerTests.xcscheme} | 33 ++--- Package.swift | 2 +- Package@swift-5.7.swift | 2 +- .../Classes/Models/MockedData.swift | 2 +- 6 files changed, 44 insertions(+), 135 deletions(-) delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme rename .swiftpm/xcode/xcshareddata/xcschemes/{NetworkLayerInterfaces.xcscheme => NetworkLayerTests.xcscheme} (70%) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme deleted file mode 100644 index cabe72c..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer-Package.xcscheme +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme index 914e95b..fa6397a 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme @@ -20,6 +20,20 @@ ReferencedContainer = "container:"> + + + + + + + + + buildForRunning = "NO" + buildForProfiling = "NO" + buildForArchiving = "NO" + buildForAnalyzing = "NO"> @@ -28,6 +28,16 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + - - - - diff --git a/Package.swift b/Package.swift index dc0e560..c9f72ca 100644 --- a/Package.swift +++ b/Package.swift @@ -44,7 +44,7 @@ let package = Package( .product(name: "Typhoon", package: "typhoon"), ], resources: [ - .copy("Resources"), + .process("Resources"), ] ), ] diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index 33e0c71..0a132cd 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -43,7 +43,7 @@ let package = Package( .product(name: "Typhoon", package: "typhoon"), ], resources: [ - .copy("Resources"), + .process("Resources"), ] ), ] diff --git a/Tests/NetworkLayerTests/Classes/Models/MockedData.swift b/Tests/NetworkLayerTests/Classes/Models/MockedData.swift index 3a44797..993d77d 100644 --- a/Tests/NetworkLayerTests/Classes/Models/MockedData.swift +++ b/Tests/NetworkLayerTests/Classes/Models/MockedData.swift @@ -6,5 +6,5 @@ import Foundation public enum MockedData { - public static let userJSON: URL = Bundle.module.url(forResource: "Resources/JSONs/user", withExtension: "json")! + public static let userJSON: URL = Bundle.module.url(forResource: "user", withExtension: "json")! } From eddfbfb6b6d8c4d6433e53efe491750a4ec088e1 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Fri, 1 Dec 2023 14:45:39 +0100 Subject: [PATCH 21/26] Update `schemes` --- .github/workflows/ci.yml | 10 +-- .../xcschemes/NetworkLayerInterfaces.xcscheme | 67 +++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94f0d67..619319c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,24 +35,24 @@ jobs: include: - destination: "OS=16.1,name=iPhone 14 Pro" name: "iOS" - scheme: "NetworkLayer-Package" + scheme: "NetworkLayer" sdk: iphonesimulator - destination: "OS=16.1,name=Apple TV" name: "tvOS" - scheme: "NetworkLayer-Package" + scheme: "NetworkLayer" sdk: appletvsimulator - destination: "OS=9.1,name=Apple Watch Series 8 (45mm)" name: "watchOS" - scheme: "NetworkLayer-Package" + scheme: "NetworkLayer" sdk: watchsimulator - destination: "platform=macOS" name: "macOS" - scheme: "NetworkLayer-Package" + scheme: "NetworkLayer" sdk: macosx steps: - uses: actions/checkout@v3 - name: ${{ matrix.name }} - run: xcodebuild test -scheme "${{ matrix.scheme }}" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "./${{ matrix.sdk }}.xcresult" | xcpretty -r junit + run: xcodebuild test -scheme "${{ matrix.scheme }}" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "./${{ matrix.sdk }}.xcresult" - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3.1.0 with: diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme new file mode 100644 index 0000000..2395097 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerInterfaces.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + From 102b484efba2243e829d5ef88310bd08d38d9234 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Fri, 1 Dec 2023 14:54:37 +0100 Subject: [PATCH 22/26] Update `xcscheme` --- .../xcshareddata/xcschemes/NetworkLayer.xcscheme | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme index fa6397a..a82d203 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme @@ -40,7 +40,18 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + From aad65537754c431ed14416c242d6b1efd4bd5a5d Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 4 Dec 2023 16:10:31 +0100 Subject: [PATCH 23/26] Implement documentation --- ....swift => AuthenticationInterceptor.swift} | 26 ++++++- .../RequestProcessor/RequestProcessor.swift | 4 +- .../Classes/DI/NetworkLayerAssembly.swift | 17 +++-- .../Articles/Authentication.md | 72 ++++++++++++++++++ .../Articles/GettingStarted.md | 76 +++++++++++++++++++ .../NetworkLayer.docc/Articles/Retry.md | 31 ++++++++ .../NetworkLayer.docc/NetworkLayer.md | 60 +++++++++++++++ .../AuthenticatorInterceptorError.swift | 2 +- ...swift => IAuthenticationInterceptor.swift} | 2 +- .../Classes/DI/INetworkLayerAssembly.swift | 2 +- .../Helpers/RequestProcessor+Mock.swift | 2 +- .../AuthentificatorInterceptorMock.swift | 2 +- .../RequestProcessorAuthenticationTests.swift | 4 +- .../AuthenticationInterceptorTests.swift | 4 +- 14 files changed, 285 insertions(+), 19 deletions(-) rename Sources/NetworkLayer/Classes/Core/Authentification/{AuthenticatorInterceptor.swift => AuthenticationInterceptor.swift} (64%) create mode 100644 Sources/NetworkLayer/NetworkLayer.docc/Articles/Authentication.md create mode 100644 Sources/NetworkLayer/NetworkLayer.docc/Articles/GettingStarted.md create mode 100644 Sources/NetworkLayer/NetworkLayer.docc/Articles/Retry.md create mode 100644 Sources/NetworkLayer/NetworkLayer.docc/NetworkLayer.md rename Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/{IAuthenticatorInterceptor.swift => IAuthenticationInterceptor.swift} (96%) diff --git a/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticationInterceptor.swift similarity index 64% rename from Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift rename to Sources/NetworkLayer/Classes/Core/Authentification/AuthenticationInterceptor.swift index 5ea6ab3..b017286 100644 --- a/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticatorInterceptor.swift +++ b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticationInterceptor.swift @@ -7,9 +7,9 @@ import Atomic import Foundation import NetworkLayerInterfaces -/// A custom AuthenticatorInterceptor implementation that works with a specific type +/// A custom AuthenticationInterceptor implementation that works with a specific type /// of Authenticator conforming to the IAuthenticator protocol. -public final class AuthenticatorInterceptor: IAuthenticatorInterceptor { +public final class AuthenticationInterceptor: IAuthenticationInterceptor { // MARK: Types public typealias Credential = Authenticator.Credential @@ -21,6 +21,11 @@ public final class AuthenticatorInterceptor: IAut // MARK: Initialization + /// Creates a new instance of `AuthenticationInterceptor`. + /// + /// - Parameters: + /// - authenticator: The authenticator. + /// - credential: The credential. public init(authenticator: Authenticator, credential: Credential? = nil) { self.authenticator = authenticator self.credential = credential @@ -28,6 +33,11 @@ public final class AuthenticatorInterceptor: IAut // MARK: IAuthentificatorInterceptor + /// Adapts the request with credentials. + /// + /// - Parameters: + /// - request: The URLRequest to be adapted. + /// - session: The URLSession for which the request is being adapted. public func adapt(request: inout URLRequest, for session: URLSession) async throws { guard let credential else { throw AuthenticatorInterceptorError.missingCredential @@ -40,6 +50,11 @@ public final class AuthenticatorInterceptor: IAut } } + /// Refreshes credential for the request. + /// + /// - Parameters: + /// - request: The URLRequest to be refreshed. + /// - session: The URLSession for which the request is being refreshed. public func refresh( _ request: URLRequest, with response: HTTPURLResponse, @@ -60,6 +75,13 @@ public final class AuthenticatorInterceptor: IAut try await refresh(credential, for: session) } + /// Determines whether a request requires a credential refresh. + /// + /// - Parameters: + /// - request: The URLRequest to check. + /// - response: The HTTPURLResponse received for the request. + /// + /// - Returns: A boolean indicating whether a credential refresh is required. public func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool { authenticator.didRequest(request, with: response) } diff --git a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift index a4a7f41..8754162 100644 --- a/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift +++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift @@ -24,7 +24,7 @@ actor RequestProcessor { /// The retry policy service. private let retryPolicyService: IRetryPolicyService /// The authenticator interceptor. - private let interceptor: IAuthenticatorInterceptor? + private let interceptor: IAuthenticationInterceptor? /// The delegate. private weak var delegate: RequestProcessorDelegate? @@ -43,7 +43,7 @@ actor RequestProcessor { dataRequestHandler: any IDataRequestHandler, retryPolicyService: IRetryPolicyService, delegate: RequestProcessorDelegate?, - interceptor: IAuthenticatorInterceptor? + interceptor: IAuthenticationInterceptor? ) { self.configuration = configuration self.requestBuilder = requestBuilder diff --git a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift index 35e3fb7..18e0bc2 100644 --- a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift +++ b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift @@ -17,18 +17,23 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly { /// The request processor delegate. private let delegate: RequestProcessorDelegate? /// The authenticator interceptor. - private let interceptor: IAuthenticatorInterceptor? + private let interceptor: IAuthenticationInterceptor? /// The json encoder. private let jsonEncoder: JSONEncoder // MARK: Initialization public init( - configure: Configuration, - retryPolicyStrategy: RetryPolicyStrategy?, - delegate: RequestProcessorDelegate?, - interceptor: IAuthenticatorInterceptor?, - jsonEncoder: JSONEncoder + configure: Configuration = .init( + sessionConfiguration: .default, + sessionDelegate: nil, + sessionDelegateQueue: nil, + jsonDecoder: JSONDecoder() + ), + retryPolicyStrategy: RetryPolicyStrategy? = nil, + delegate: RequestProcessorDelegate? = nil, + interceptor: IAuthenticationInterceptor? = nil, + jsonEncoder: JSONEncoder = JSONEncoder() ) { self.configure = configure self.retryPolicyStrategy = retryPolicyStrategy diff --git a/Sources/NetworkLayer/NetworkLayer.docc/Articles/Authentication.md b/Sources/NetworkLayer/NetworkLayer.docc/Articles/Authentication.md new file mode 100644 index 0000000..c44f16c --- /dev/null +++ b/Sources/NetworkLayer/NetworkLayer.docc/Articles/Authentication.md @@ -0,0 +1,72 @@ +# Authentication + +Learn how to implement authentication. + +## Overview + +The `network-layer` includes an ``AuthenticationInterceptor`` responsible for handling authentication options. + +## Access Tokens + +``AuthenticationInterceptor`` requires passing an `IAuthenticator` as initialization parameters that contain logic for updating the access token. + +A credential object must conform to `IAuthenticationCredential` protocol that indicates whether the credential is valid. + +```swift +import NetworkLayerInterfaces + +// Defines a credential model +struct Credential: IAuthenticationCredential { + let expires: Date + let requiresRefresh: Bool { expires < Date() } +} +``` + +Define a `Authenticator` object that confrorms to `IAuthenticator` and implement your own logic for validation and refreshing an access token. + +```swift +import NetworkLayerInterfaces + +struct Authenticator: IAuthenticator { + + /// Applies the `Credential` to the `URLRequest`. + /// + /// - Parameters: + /// - credential: The `Credential`. + /// - urlRequest: The `URLRequest`. + func apply(_ credential: Credential, to urlRequest: URLRequest) async throws { + request.addValue("Bearer ", forHTTPHeaderField: "Authorization") + } + + /// Refreshes the `Credential`. + /// + /// - Parameters: + /// - credential: The `Credential` to refresh. + /// - session: The `URLSession` requiring the refresh. + func refresh(_ credential: Credential, for session: URLSession) async throws -> Credential { + // Token refresh logic here + } + + /// Determines whether the `URLRequest` failed due to an authentication error based on the `HTTPURLResponse`. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest`. + /// - response: The `HTTPURLResponse`. + /// + /// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise. + func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse) -> Bool { + response.statusCode == 401 + } + + /// Determines whether the `URLRequest` is authenticated with the `Credential`. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest`. + /// - credential: The `Credential`. + /// + /// - Returns: `true` if the `URLRequest` is authenticated with the `Credential`, `false` otherwise. + func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool { + true + } +} +``` diff --git a/Sources/NetworkLayer/NetworkLayer.docc/Articles/GettingStarted.md b/Sources/NetworkLayer/NetworkLayer.docc/Articles/GettingStarted.md new file mode 100644 index 0000000..f3ccf75 --- /dev/null +++ b/Sources/NetworkLayer/NetworkLayer.docc/Articles/GettingStarted.md @@ -0,0 +1,76 @@ +# Getting Started with Network Layer + +## Defining a Request + +Before sending a request to a web server, it is necessary to define a request model. For this, define a new `struct` object that conforms to `IRequest` protocol. + +```swift +import NetworkLayerInterfaces + +struct UserRequest: IRequest { + // MARK: Properties + + private let id: Int + + // MARK: Initialization + + init(id: Int) { + self.id = id + } + + // MARK: IRequest + + /// The base `URL` for the resource. + var domainName: String { + "https://example.com" + } + + /// The endpoint path. + var path: String { + "user" + } + + /// A dictionary that contains the parameters to be encoded into the request. + var parameters: [String: String]? { + ["user_id": id] + } + + /// A Boolean value indicating whether authentication is required. + var requiresAuthentication: Bool { + true + } + + /// The HTTP method. + var httpMethod: HTTPMethod { + .get + } +} +``` + +## Defining a Response Model + +While the `network-layer` returns a `Response` object that expects a decodable object, it is necessary to define a response model. + +```swift +import NetworkLayerInterfaces + +struct UserResponse: Decodable { + let id: Int + let userName: String +} +``` + +## Usage + +```swift +import NetworkLayerInterfaces + +let request = UserRequest(id: 1) + +do { + let user = try await requestProcessor.send(request) +} catch { + // Catch an error here +} + +``` diff --git a/Sources/NetworkLayer/NetworkLayer.docc/Articles/Retry.md b/Sources/NetworkLayer/NetworkLayer.docc/Articles/Retry.md new file mode 100644 index 0000000..8d07ba1 --- /dev/null +++ b/Sources/NetworkLayer/NetworkLayer.docc/Articles/Retry.md @@ -0,0 +1,31 @@ +# Retry + +Learn how to retry failed requests. + +## Overview + +The `network-layer` is implemented using the `Typhoon` framework to handle the retrying of failed requests. You can read more about `Typhoon` framework [here](https://github.com/space-code/typhoon/). + +## Retrying Failed Requests + +By default, the `network-layer` attempts to resend a failed request five times. If you wish to customize this behavior, you can pass the desired value to an assembly of the `RequestProcessor`. + +```swift +import NetworkLayer +import NetworkLayerInterfaces + +let requestProcessor = NetworkLayerAssembly.assemble(retryPolicyStrategy: .constant(retry: 10, duration: .seconds(1))) +``` + +> Tip: `typhoon` framework provides different strategies for retrying failed request. You can read more [here](https://github.com/space-code/typhoon). + +This behavior will be applied to all future requests. + +In case you desire to customize a particular request, you can pass the desired strategy to that specific request: + +```swift +import NetworkLayerInterfaces + +let request = UserRequest(id: 1) +let user: Response = try await requestProcessor.send(request, strategy: .constant(retry: 10, duration: .seconds(1))) +``` diff --git a/Sources/NetworkLayer/NetworkLayer.docc/NetworkLayer.md b/Sources/NetworkLayer/NetworkLayer.docc/NetworkLayer.md new file mode 100644 index 0000000..c007f2f --- /dev/null +++ b/Sources/NetworkLayer/NetworkLayer.docc/NetworkLayer.md @@ -0,0 +1,60 @@ +# ``NetworkLayer`` + +The library for network communication. + +## Overview + +The `network-layer` provides a simple interface for communication, making it very easy to send a request to a web server. + +```swift +import NetworkLayer + +let requestProcessor = NetworkLayerAssembly().assemble() +let user: User = try await requestProcessor.send(request) +``` + +The `network-layer` separates into two modules: `NetworkLayer`, which contains core functionality, and `NetworkLayerInterfaces`, which only contains public protocols for this framework. + +The library supports authentication, retrying requests, and more. + +## License + +network-layer is available under the MIT license. See the LICENSE file for more info. + +## Topics + +### Essentials + +- +- +- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/AuthenticatorInterceptorError.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/AuthenticatorInterceptorError.swift index d189575..7313fb4 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/AuthenticatorInterceptorError.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/AuthenticatorInterceptorError.swift @@ -5,7 +5,7 @@ import Foundation -/// `AuthenticatorInterceptorError` is the error type returned by AuthenticatorInterceptor. +/// `AuthenticatorInterceptorError` is the error type returned by AuthenticationInterceptor. public enum AuthenticatorInterceptorError: Swift.Error { /// The credential was not found. case missingCredential diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticationInterceptor.swift similarity index 96% rename from Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift rename to Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticationInterceptor.swift index 63a8cfd..1ac679b 100644 --- a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticatorInterceptor.swift +++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticationInterceptor.swift @@ -6,7 +6,7 @@ import Foundation /// A type defines the authenticator interceptor interface. -public protocol IAuthenticatorInterceptor { +public protocol IAuthenticationInterceptor { /// Adapts the request with credentials. /// /// - Parameters: diff --git a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift index bab6cd0..b02c60f 100644 --- a/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift +++ b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift @@ -22,7 +22,7 @@ public protocol INetworkLayerAssembly { configure: Configuration, retryPolicyStrategy: RetryPolicyStrategy?, delegate: RequestProcessorDelegate?, - interceptor: IAuthenticatorInterceptor?, + interceptor: IAuthenticationInterceptor?, jsonEncoder: JSONEncoder ) diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Helpers/RequestProcessor+Mock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Helpers/RequestProcessor+Mock.swift index c974399..5385dae 100644 --- a/Tests/NetworkLayerTests/Classes/Helpers/Helpers/RequestProcessor+Mock.swift +++ b/Tests/NetworkLayerTests/Classes/Helpers/Helpers/RequestProcessor+Mock.swift @@ -12,7 +12,7 @@ import Typhoon extension RequestProcessor { static func mock( requestProcessorDelegate: RequestProcessorDelegate? = nil, - interceptor: IAuthenticatorInterceptor? = nil + interceptor: IAuthenticationInterceptor? = nil ) -> RequestProcessor { RequestProcessor( configuration: .init( diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift index 1acfc67..4313e21 100644 --- a/Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift +++ b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift @@ -6,7 +6,7 @@ import Foundation import NetworkLayerInterfaces -final class AuthentificatorInterceptorMock: IAuthenticatorInterceptor { +final class AuthentificatorInterceptorMock: IAuthenticationInterceptor { var invokedAdapt = false var invokedAdaptCount = 0 var invokedAdaptParameters: (request: URLRequest, session: URLSession)? diff --git a/Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorAuthenticationTests.swift b/Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorAuthenticationTests.swift index eed185e..51821df 100644 --- a/Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorAuthenticationTests.swift +++ b/Tests/NetworkLayerTests/Classes/Tests/IntegrationTests/RequestProcessorAuthenticationTests.swift @@ -74,7 +74,7 @@ final class RequestProcessorAuthenicationTests: XCTestCase { // MARK: Private private func test_failAuthentication(adaptError: Error?, refreshError: Error?, expectedError: Error) async throws { - class FailInterceptor: IAuthenticatorInterceptor { + class FailInterceptor: IAuthenticationInterceptor { let adaptError: Error? let refreshError: Error? @@ -125,7 +125,7 @@ final class RequestProcessorAuthenicationTests: XCTestCase { // MARK: - AuthInterceptor -private final class AuthInterceptor: IAuthenticatorInterceptor { +private final class AuthInterceptor: IAuthenticationInterceptor { var token: Token! private var attempts = 0 diff --git a/Tests/NetworkLayerTests/Classes/Tests/UnitTests/AuthenticationInterceptorTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/AuthenticationInterceptorTests.swift index c27bffa..0365ebe 100644 --- a/Tests/NetworkLayerTests/Classes/Tests/UnitTests/AuthenticationInterceptorTests.swift +++ b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/AuthenticationInterceptorTests.swift @@ -12,14 +12,14 @@ final class AuthenticationInterceptorTests: XCTestCase { private var authenticatorMock: AuthenticatorMock! - private var sut: AuthenticatorInterceptor! + private var sut: AuthenticationInterceptor! // MARK: XCTestCase override func setUp() { super.setUp() authenticatorMock = AuthenticatorMock() - sut = AuthenticatorInterceptor( + sut = AuthenticationInterceptor( authenticator: authenticatorMock, credential: nil ) From d6133ec8ecd5884632e2eb768bcbe1e43abe35ae Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 4 Dec 2023 18:41:45 +0100 Subject: [PATCH 24/26] Update `README.md` --- README.md | 33 ++++++++++++++++++- .../Articles/GettingStarted.md | 1 + .../NetworkLayer.docc/NetworkLayer.md | 32 +----------------- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 17b12f4..547390a 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@

## Description -`network-layer` description. +`network-layer` is a library for network communication. - [Usage](#usage) +- [Documentation](#documentation) - [Requirements](#requirements) - [Installation](#installation) - [Communication](#communication) @@ -24,7 +25,37 @@ ## Usage +```swift +import NetworkLayer +import NetworkLayerInterfaces + +struct Request: IRequest { + var domainName: String { + "https://example.com" + } + + var path: String { + "user" + } + + var httpMethod: HTTPMethod { + .get + } +} + +let request = Request() +let requestProcessor = NetworkLayerAssembly().assemble() +let user: User = try await requestProcessor.send(request) +``` + +## Documentation + +Check out [network-layer documentation](https://github.com/space-code/network-layer/blob/main/Sources/NetworkLayer/NetworkLayer.docc/NetworkLayer.md). + ## Requirements +- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 7.0+ / visionOS 1.0+ +- Xcode 14.0 +- Swift 5.7 ## Installation ### Swift Package Manager diff --git a/Sources/NetworkLayer/NetworkLayer.docc/Articles/GettingStarted.md b/Sources/NetworkLayer/NetworkLayer.docc/Articles/GettingStarted.md index f3ccf75..51cfec4 100644 --- a/Sources/NetworkLayer/NetworkLayer.docc/Articles/GettingStarted.md +++ b/Sources/NetworkLayer/NetworkLayer.docc/Articles/GettingStarted.md @@ -74,3 +74,4 @@ do { } ``` + diff --git a/Sources/NetworkLayer/NetworkLayer.docc/NetworkLayer.md b/Sources/NetworkLayer/NetworkLayer.docc/NetworkLayer.md index c007f2f..8ecdf01 100644 --- a/Sources/NetworkLayer/NetworkLayer.docc/NetworkLayer.md +++ b/Sources/NetworkLayer/NetworkLayer.docc/NetworkLayer.md @@ -27,34 +27,4 @@ network-layer is available under the MIT license. See the LICENSE file for more - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +- Date: Mon, 4 Dec 2023 18:42:53 +0100 Subject: [PATCH 25/26] Update `CHANGELOG.md` --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e9885a..86c4648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,12 @@ # Change Log -All notable changes to this project will be documented in this file. \ No newline at end of file +All notable changes to this project will be documented in this file. + +#### 1.x Releases +- `1.0.x` Releases - [1.0.0](#100) + +## [1.0.0](https://github.com/space-code/network-layer/releases/tag/1.0.0) +Released on 2023-12-04. + +#### Added +- Initial release of `network-layer`. + - Added by [Nikita Vasilev](https://github.com/nik3212). From f4d9370696e1bc649cbc627a72b56c9df2788a4c Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 4 Dec 2023 18:51:42 +0100 Subject: [PATCH 26/26] Add a `Dependencies` section to `README.md` --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 547390a..6c45a18 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - [Communication](#communication) - [Contributing](#contributing) - [Author](#author) +- [Dependencies](#dependencies) - [License](#license) ## Usage @@ -87,5 +88,12 @@ Please feel free to help out with this project! If you see something that could ## Author Nikita Vasilev, nv3212@gmail.com +## Dependencies +This project uses several open-source packages: + +* [Atomic](https://github.com/space-code/atomic) is a Swift property wrapper designed to make values thread-safe. +* [Typhoon](https://github.com/space-code/typhoon) is a service for retry policies. +* [Mocker](https://github.com/WeTransfer/Mocker) is a library written in Swift which makes it possible to mock data requests using a custom `URLProtocol`. + ## License network-layer is available under the MIT license. See the LICENSE file for more info.