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..619319c
--- /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: "NetworkLayer"
+ sdk: iphonesimulator
+ - destination: "OS=16.1,name=Apple TV"
+ name: "tvOS"
+ scheme: "NetworkLayer"
+ sdk: appletvsimulator
+ - destination: "OS=9.1,name=Apple Watch Series 8 (45mm)"
+ name: "watchOS"
+ scheme: "NetworkLayer"
+ sdk: watchsimulator
+ - destination: "platform=macOS"
+ name: "macOS"
+ 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"
+ - 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..297f875
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,133 @@
+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
+ - array_init
+ - attributes
+ - closure_body_length
+ - closure_end_indentation
+ - closure_spacing
+ - collection_alignment
+ - 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
+ - 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/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme
new file mode 100644
index 0000000..a82d203
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayer.xcscheme
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerTests.xcscheme
new file mode 100644
index 0000000..af4fec8
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/NetworkLayerTests.xcscheme
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..86c4648
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Change Log
+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).
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.resolved b/Package.resolved
new file mode 100644
index 0000000..418af9a
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,32 @@
+{
+ "pins" : [
+ {
+ "identity" : "atomic",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/space-code/atomic",
+ "state" : {
+ "revision" : "53fae2fc8216bb5c27c87b245f893176d0d290eb",
+ "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",
+ "location" : "https://github.com/space-code/typhoon",
+ "state" : {
+ "revision" : "c0e2970812f4ec837fc61afd72a7ca1f0aa39306",
+ "version" : "1.0.0"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..c9f72ca
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,51 @@
+// 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"]),
+ ],
+ 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(
+ name: "NetworkLayer",
+ dependencies: [
+ "NetworkLayerInterfaces",
+ .product(name: "Atomic", package: "atomic"),
+ .product(name: "Typhoon", package: "typhoon"),
+ ]
+ ),
+ .target(
+ name: "NetworkLayerInterfaces",
+ dependencies: [
+ .product(name: "Typhoon", package: "typhoon"),
+ ]
+ ),
+ .testTarget(
+ name: "NetworkLayerTests",
+ dependencies: [
+ "NetworkLayer",
+ .product(name: "Mocker", package: "Mocker"),
+ .product(name: "Typhoon", package: "typhoon"),
+ ],
+ resources: [
+ .process("Resources"),
+ ]
+ ),
+ ]
+)
diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift
new file mode 100644
index 0000000..0a132cd
--- /dev/null
+++ b/Package@swift-5.7.swift
@@ -0,0 +1,50 @@
+// 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"]),
+ ],
+ 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(
+ name: "NetworkLayer",
+ dependencies: [
+ "NetworkLayerInterfaces",
+ .product(name: "Atomic", package: "atomic"),
+ .product(name: "Typhoon", package: "typhoon"),
+ ]
+ ),
+ .target(
+ name: "NetworkLayerInterfaces",
+ dependencies: [
+ .product(name: "Typhoon", package: "typhoon"),
+ ]
+ ),
+ .testTarget(
+ name: "NetworkLayerTests",
+ dependencies: [
+ "NetworkLayer",
+ .product(name: "Mocker", package: "Mocker"),
+ .product(name: "Typhoon", package: "typhoon"),
+ ],
+ resources: [
+ .process("Resources"),
+ ]
+ ),
+ ]
+)
diff --git a/README.md b/README.md
index 21c4160..6c45a18 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,99 @@
-# network-layer
\ No newline at end of file
+![NetworkLayer: Network communication made easy](https://raw.githubusercontent.com/space-code/network-layer/dev/Resources/network-layer.png)
+
+
network-layer
+
+
+
+
+
+
+
+
+
+
+## Description
+`network-layer` is a library for network communication.
+
+- [Usage](#usage)
+- [Documentation](#documentation)
+- [Requirements](#requirements)
+- [Installation](#installation)
+- [Communication](#communication)
+- [Contributing](#contributing)
+- [Author](#author)
+- [Dependencies](#dependencies)
+- [License](#license)
+
+## 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
+
+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
+
+## 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.
diff --git a/Resources/network-layer.png b/Resources/network-layer.png
new file mode 100644
index 0000000..19d77b8
Binary files /dev/null and b/Resources/network-layer.png differ
diff --git a/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticationInterceptor.swift b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticationInterceptor.swift
new file mode 100644
index 0000000..b017286
--- /dev/null
+++ b/Sources/NetworkLayer/Classes/Core/Authentification/AuthenticationInterceptor.swift
@@ -0,0 +1,95 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Atomic
+import Foundation
+import NetworkLayerInterfaces
+
+/// A custom AuthenticationInterceptor implementation that works with a specific type
+/// of Authenticator conforming to the IAuthenticator protocol.
+public final class AuthenticationInterceptor: IAuthenticationInterceptor {
+ // MARK: Types
+
+ public typealias Credential = Authenticator.Credential
+
+ // MARK: Private
+
+ private let authenticator: Authenticator
+ @Atomic public var credential: Credential?
+
+ // 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
+ }
+
+ // 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
+ }
+
+ if credential.requiresRefresh {
+ try await refresh(credential, for: session)
+ } else {
+ try await authenticator.apply(credential, to: request)
+ }
+ }
+
+ /// 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,
+ for session: URLSession
+ ) async throws {
+ guard isRequireRefresh(request, response: response) else {
+ return
+ }
+
+ guard let credential = credential else {
+ throw AuthenticatorInterceptorError.missingCredential
+ }
+
+ guard authenticator.isRequest(request, authenticatedWith: credential) else {
+ return
+ }
+
+ 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)
+ }
+
+ // 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/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..48f581f
--- /dev/null
+++ b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBuilder.swift
@@ -0,0 +1,62 @@
+//
+// 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,
+ _ configure: ((inout URLRequest) throws -> Void)?
+ ) throws -> URLRequest? {
+ guard let fullPath = request.fullPath, let url = URL(string: 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)
+ }
+
+ try configure?(&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..863e8f2
--- /dev/null
+++ b/Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestParameterEncoder/RequestParametersEncoder.swift
@@ -0,0 +1,29 @@
+//
+// 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)
+ }
+
+ if parameters.isEmpty {
+ return
+ }
+
+ 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/DataRequestHandler/DataRequestHandler.swift b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift
new file mode 100644
index 0000000..64f3400
--- /dev/null
+++ b/Sources/NetworkLayer/Classes/Core/Services/DataRequestHandler/DataRequestHandler.swift
@@ -0,0 +1,190 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Atomic
+import Foundation
+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
+ }
+ }
+}
+
+// MARK: IDataRequestHandler
+
+extension DataRequestHandler: IDataRequestHandler {
+ func startDataTask(
+ _ task: URLSessionDataTask,
+ delegate: URLSessionDelegate?
+ ) async throws -> Response {
+ try await withTaskCancellationHandler(operation: {
+ try await withUnsafeThrowingContinuation { continuation in
+ let dataTaskHandler = DataTaskHandler(delegate: delegate)
+ 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)
+ }
+
+ 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(_ 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 {
+ if let response = task.response {
+ let data = handler.data ?? Data()
+ let response = Response(data: data, response: response, task: task)
+ handler.completion?(.success(response))
+ } else {
+ handler.completion?(.failure(URLError(.unknown)))
+ }
+ }
+ }
+
+ 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
+
+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/RequestProcessor/RequestProcessor.swift b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift
new file mode 100644
index 0000000..8754162
--- /dev/null
+++ b/Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift
@@ -0,0 +1,185 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+import NetworkLayerInterfaces
+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
+ /// The authenticator interceptor.
+ private let interceptor: IAuthenticationInterceptor?
+ /// The delegate.
+ private weak var delegate: RequestProcessorDelegate?
+
+ // 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,
+ dataRequestHandler: any IDataRequestHandler,
+ retryPolicyService: IRetryPolicyService,
+ delegate: RequestProcessorDelegate?,
+ interceptor: IAuthenticationInterceptor?
+ ) {
+ self.configuration = configuration
+ self.requestBuilder = requestBuilder
+ self.dataRequestHandler = dataRequestHandler
+ self.retryPolicyService = retryPolicyService
+ self.delegate = delegate
+ self.interceptor = interceptor
+ self.dataRequestHandler.urlSessionDelegate = configuration.sessionDelegate
+ session = URLSession(
+ configuration: configuration.sessionConfiguration,
+ delegate: dataRequestHandler,
+ delegateQueue: configuration.sessionDelegateQueue
+ )
+ }
+
+ // 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,
+ delegate: URLSessionDelegate?,
+ configure: ((inout URLRequest) throws -> Void)?
+ ) async throws -> Response {
+ guard var urlRequest = try 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)
+
+ let task = session.dataTask(with: urlRequest)
+
+ do {
+ let response = try await dataRequestHandler.startDataTask(task, delegate: delegate)
+
+ if request.requiresAuthentication {
+ let isRefreshedCredential = try await refresh(
+ urlRequest: urlRequest,
+ response: response,
+ session: session
+ )
+
+ if isRefreshedCredential {
+ throw AuthenticatorInterceptorError.missingCredential
+ }
+ }
+
+ try self.validate(response)
+
+ return response
+ } catch {
+ throw error
+ }
+ }
+ }
+
+ /// 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)
+ }
+
+ /// 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,
+ 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.
+ ///
+ /// - 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
+ ) async throws -> T {
+ do {
+ return try await send()
+ } catch {
+ 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
+
+extension RequestProcessor: IRequestProcessor {
+ func send(
+ _ request: T,
+ strategy: RetryPolicyStrategy? = nil,
+ delegate: URLSessionDelegate? = nil,
+ configure: ((inout URLRequest) throws -> Void)? = nil
+ ) async throws -> Response {
+ let response = try await performRequest(request, strategy: strategy, delegate: delegate, configure: configure)
+ return try response.map { data in try self.configuration.jsonDecoder.decode(M.self, from: data) }
+ }
+}
diff --git a/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift
new file mode 100644
index 0000000..18e0bc2
--- /dev/null
+++ b/Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift
@@ -0,0 +1,78 @@
+//
+// 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 retry policy service.
+ private let retryPolicyStrategy: RetryPolicyStrategy?
+ /// The request processor delegate.
+ private let delegate: RequestProcessorDelegate?
+ /// The authenticator interceptor.
+ private let interceptor: IAuthenticationInterceptor?
+ /// The json encoder.
+ private let jsonEncoder: JSONEncoder
+
+ // MARK: Initialization
+
+ public init(
+ 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
+ self.delegate = delegate
+ self.interceptor = interceptor
+ self.jsonEncoder = jsonEncoder
+ }
+
+ // MARK: INetworkLayerAssembly
+
+ public func assemble() -> IRequestProcessor {
+ RequestProcessor(
+ configuration: configure,
+ requestBuilder: requestBuilder,
+ dataRequestHandler: DataRequestHandler(),
+ retryPolicyService: RetryPolicyService(strategy: retryPolicyStrategy ?? defaultStrategy),
+ delegate: delegate,
+ interceptor: interceptor
+ )
+ }
+
+ // MARK: Private
+
+ 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..fc6fe74
--- /dev/null
+++ b/Sources/NetworkLayer/Classes/Extensions/IRequest+.swift
@@ -0,0 +1,16 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+import NetworkLayerInterfaces
+
+extension IRequest {
+ var fullPath: String? {
+ if !domainName.isEmpty {
+ return [domainName, path].joined(separator: "/")
+ }
+ return nil
+ }
+}
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/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..51cfec4
--- /dev/null
+++ b/Sources/NetworkLayer/NetworkLayer.docc/Articles/GettingStarted.md
@@ -0,0 +1,77 @@
+# 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..8ecdf01
--- /dev/null
+++ b/Sources/NetworkLayer/NetworkLayer.docc/NetworkLayer.md
@@ -0,0 +1,30 @@
+# ``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
+
+-
+-
+- Bool
+}
diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift
new file mode 100644
index 0000000..a67cd64
--- /dev/null
+++ b/Sources/NetworkLayerInterfaces/Classes/Core/Authenticator/IAuthenticator.swift
@@ -0,0 +1,43 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+/// A protocol defining the interface for an authenticator type.
+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: 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`.
+ ///
+ /// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise.
+ func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse) -> 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/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/NetworkLayerInterfaces/Classes/Core/Models/HTTPMethod.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/HTTPMethod.swift
new file mode 100644
index 0000000..f87a185
--- /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: String {
+ /// `CONNECT` method.
+ case connect = "CONNECT"
+ /// `DELETE` method.
+ case delete = "DELETE"
+ /// `GET` method.
+ case get = "GET"
+ /// `HEAD` method.
+ case head = "HEAD"
+ /// `OPTIONS` method.
+ case options = "OPTIONS"
+ /// `PATCH` method.
+ case patch = "PATCH"
+ /// `POST` method.
+ case post = "POST"
+ /// `PUT` method.
+ case put = "PUT"
+ /// `QUERY` method.
+ case query = "QUERY"
+ /// `TRACE` method.
+ case trace = "TRACE"
+}
diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift
new file mode 100644
index 0000000..91caaae
--- /dev/null
+++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift
@@ -0,0 +1,70 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - IRequest
+
+/// A type to which all requests must conform.
+public protocol IRequest {
+ /// The base `URL` for the resource.
+ var domainName: String { get }
+
+ /// The endpoint path.
+ var path: String { get }
+
+ /// 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 to be encoded into the request.
+ var parameters: [String: String]? { get }
+
+ /// A Boolean value indicating whether authentication is required.
+ var requiresAuthentication: Bool { get }
+
+ /// 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: RequestBody? { get }
+
+ /// An alias for the cache policy.
+ var cachePolicy: URLRequest.CachePolicy { 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: RequestBody? {
+ nil
+ }
+
+ /// An alias for the cache policy.
+ var cachePolicy: URLRequest.CachePolicy {
+ .useProtocolCachePolicy
+ }
+}
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/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/Models/Response.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift
new file mode 100644
index 0000000..1eaeba7
--- /dev/null
+++ b/Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift
@@ -0,0 +1,34 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+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
+ statusCode = (response as? HTTPURLResponse)?.statusCode
+ self.task = task
+ }
+}
diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IDataRequestHandler.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IDataRequestHandler.swift
new file mode 100644
index 0000000..7fb0299
--- /dev/null
+++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IDataRequestHandler.swift
@@ -0,0 +1,23 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+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.
+ /// - 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,
+ delegate: URLSessionDelegate?
+ ) async throws -> Response
+}
diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestBuilder.swift
new file mode 100644
index 0000000..f47e8bf
--- /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, _ configure: ((inout URLRequest) throws -> Void)?) throws -> 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..c3a6561
--- /dev/null
+++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift
@@ -0,0 +1,40 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+import Typhoon
+
+// MARK: - IRequestProcessor
+
+/// A type capable of performing network requests.
+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,
+ strategy: RetryPolicyStrategy?,
+ delegate: URLSessionDelegate?,
+ configure: ((inout URLRequest) throws -> Void)?
+ ) async throws -> Response
+}
+
+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,
+ strategy: RetryPolicyStrategy?
+ ) async throws -> Response {
+ try await send(request, strategy: strategy, delegate: nil, configure: nil)
+ }
+}
diff --git a/Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift b/Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift
new file mode 100644
index 0000000..f1bc2f1
--- /dev/null
+++ b/Sources/NetworkLayerInterfaces/Classes/Core/Services/RequestProcessorDelegate.swift
@@ -0,0 +1,36 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+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.
+ ///
+ /// - 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
+
+ /// 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/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift
new file mode 100644
index 0000000..b02c60f
--- /dev/null
+++ b/Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift
@@ -0,0 +1,41 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+import enum Typhoon.RetryPolicyStrategy
+
+// MARK: - INetworkLayerAssembly
+
+/// A type that represents a network layer assembly.
+public protocol INetworkLayerAssembly {
+ /// Creates a new `INetworkLayerAssembly` instance.
+ ///
+ /// - Parameters:
+ /// - configure: The network layer's configuration.
+ /// - retryPolicyStrategy: The retry policy strategy.
+ /// - delegate: The request processor delegate.
+ /// - interceptor: The authenticator interceptor.
+ /// - jsonEncoder: The json encoder.
+ init(
+ configure: Configuration,
+ retryPolicyStrategy: RetryPolicyStrategy?,
+ delegate: RequestProcessorDelegate?,
+ interceptor: IAuthenticationInterceptor?,
+ jsonEncoder: JSONEncoder
+ )
+
+ /// Assembles a request processor.
+ ///
+ /// - Returns: A request processor.
+ func assemble() -> IRequestProcessor
+}
+
+public extension INetworkLayerAssembly {
+ init(
+ configure: Configuration
+ ) {
+ self.init(configure: configure, retryPolicyStrategy: nil, delegate: nil, interceptor: nil, jsonEncoder: JSONEncoder())
+ }
+}
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/Classes/Helpers/Fakes/URLRequest+Fake.swift b/Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLRequest+Fake.swift
new file mode 100644
index 0000000..1cfa8fa
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Helpers/Fakes/URLRequest+Fake.swift
@@ -0,0 +1,12 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+extension URLRequest {
+ static func fake(string: String = "https://google.com/") -> URLRequest {
+ URLRequest(url: URL(string: string)!)
+ }
+}
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..5385dae
--- /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: IAuthenticationInterceptor? = 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/Classes/Helpers/Mocks/AuthenticatorMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthenticatorMock.swift
new file mode 100644
index 0000000..4b1d42b
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Helpers/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: 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)?
+ var invokedDidRequestParametersList = [(urlRequest: URLRequest, response: HTTPURLResponse)]()
+ var stubbedDidRequestResult: Bool! = false
+
+ func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse) -> Bool {
+ invokedDidRequest = true
+ invokedDidRequestCount += 1
+ invokedDidRequestParameters = (urlRequest, response)
+ invokedDidRequestParametersList.append((urlRequest, response))
+ 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/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift
new file mode 100644
index 0000000..4313e21
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift
@@ -0,0 +1,47 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+import NetworkLayerInterfaces
+
+final class AuthentificatorInterceptorMock: IAuthenticationInterceptor {
+ 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) {
+ 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)?
+ var invokedRefreshParametersList = [(request: URLRequest, response: HTTPURLResponse, session: URLSession)]()
+
+ func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) {
+ invokedRefresh = true
+ invokedRefreshCount += 1
+ 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/Classes/Helpers/Mocks/DataRequestHandlerMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/DataRequestHandlerMock.swift
new file mode 100644
index 0000000..8889a5c
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/DataRequestHandlerMock.swift
@@ -0,0 +1,47 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+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, delegate: URLSessionDelegate?)?
+ var invokedStartDataTaskParametersList = [(task: URLSessionDataTask, delegate: URLSessionDelegate?)]()
+ var stubbedStartDataTask: Response!
+ var startDataTaskThrowError: Error?
+
+ func startDataTask(
+ _ task: URLSessionDataTask,
+ delegate: URLSessionDelegate?
+ ) async throws -> Response {
+ invokedStartDataTask = true
+ invokedStartDataTaskCount += 1
+ invokedStartDataTaskParameters = (task, delegate)
+ invokedStartDataTaskParametersList.append((task, delegate))
+ if let error = startDataTaskThrowError {
+ throw error
+ }
+ return stubbedStartDataTask
+ }
+}
diff --git a/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestBodyEncoderMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestBodyEncoderMock.swift
new file mode 100644
index 0000000..e6cc138
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Helpers/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/Classes/Helpers/Mocks/RequestBuilderMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestBuilderMock.swift
new file mode 100644
index 0000000..bb845c2
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Helpers/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/Classes/Helpers/Mocks/RequestMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestMock.swift
new file mode 100644
index 0000000..36cc932
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Helpers/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/Classes/Helpers/Mocks/RequestParametersEncoderMock.swift b/Tests/NetworkLayerTests/Classes/Helpers/Mocks/RequestParametersEncoderMock.swift
new file mode 100644
index 0000000..1e6c9e4
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Helpers/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/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/Classes/Helpers/Stubs/AuthenticationCredentialStub.swift b/Tests/NetworkLayerTests/Classes/Helpers/Stubs/AuthenticationCredentialStub.swift
new file mode 100644
index 0000000..9e9b4f3
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Helpers/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/Classes/Helpers/Stubs/RequestStub.swift b/Tests/NetworkLayerTests/Classes/Helpers/Stubs/RequestStub.swift
new file mode 100644
index 0000000..148c675
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Helpers/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/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..993d77d
--- /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: "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..51821df
--- /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: IAuthenticationInterceptor {
+ 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: IAuthenticationInterceptor {
+ 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/Classes/Tests/UnitTests/AuthenticationInterceptorTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/AuthenticationInterceptorTests.swift
new file mode 100644
index 0000000..0365ebe
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Tests/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: AuthenticationInterceptor!
+
+ // MARK: XCTestCase
+
+ override func setUp() {
+ super.setUp()
+ authenticatorMock = AuthenticatorMock()
+ sut = AuthenticationInterceptor(
+ 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)
+
+ // 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)
+
+ // 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)
+ } 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)
+
+ // then
+ XCTAssertFalse(authenticatorMock.invokedRefresh)
+ }
+}
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/Classes/Tests/UnitTests/RequestBodyEncoderTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestBodyEncoderTests.swift
new file mode 100644
index 0000000..ad3c44c
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Tests/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/Classes/Tests/UnitTests/RequestBuilderTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestBuilderTests.swift
new file mode 100644
index 0000000..ce7c383
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Tests/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/Classes/Tests/UnitTests/RequestParametersEncoderTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestParametersEncoderTests.swift
new file mode 100644
index 0000000..9cb4a31
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Tests/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"]
+}
diff --git a/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestProcessorTests.swift b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestProcessorTests.swift
new file mode 100644
index 0000000..a46d5a8
--- /dev/null
+++ b/Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestProcessorTests.swift
@@ -0,0 +1,135 @@
+//
+// network-layer
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+@testable import NetworkLayer
+import NetworkLayerInterfaces
+import Typhoon
+import XCTest
+
+final class RequestProcessorTests: XCTestCase {
+ // MARK: Properties
+
+ private var requestBuilderMock: RequestBuilderMock!
+ private var dataRequestHandler: DataRequestHandlerMock!
+ private var retryPolicyMock: RetryPolicyService!
+ private var delegateMock: RequestProcessorDelegateMock!
+ private var interceptorMock: AuthentificatorInterceptorMock!
+
+ private var sut: RequestProcessor!
+
+ // MARK: XCTestCase
+
+ override func setUp() {
+ super.setUp()
+ requestBuilderMock = RequestBuilderMock()
+ dataRequestHandler = DataRequestHandlerMock()
+ retryPolicyMock = RetryPolicyService(
+ strategy: .constant(
+ retry: 5,
+ duration: .seconds(.zero)
+ )
+ )
+ delegateMock = RequestProcessorDelegateMock()
+ interceptorMock = AuthentificatorInterceptorMock()
+ sut = RequestProcessor(
+ configuration: Configuration(
+ sessionConfiguration: .default,
+ sessionDelegate: nil,
+ sessionDelegateQueue: nil,
+ jsonDecoder: JSONDecoder()
+ ),
+ requestBuilder: requestBuilderMock,
+ dataRequestHandler: dataRequestHandler,
+ retryPolicyService: retryPolicyMock,
+ delegate: delegateMock,
+ interceptor: interceptorMock
+ )
+ }
+
+ override func tearDown() {
+ requestBuilderMock = nil
+ dataRequestHandler = nil
+ retryPolicyMock = nil
+ delegateMock = nil
+ interceptorMock = nil
+ sut = nil
+ super.tearDown()
+ }
+
+ // MARK: Tests
+
+ func test_thatRequestProcessorSignsRequest_whenRequestRequiresAuthentication() async {
+ // given
+ requestBuilderMock.stubbedBuildResult = URLRequest.fake()
+ dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: .init(), task: .fake())
+
+ let request = RequestMock()
+ request.stubbedRequiresAuthentication = true
+
+ // when
+ do {
+ let _ = try await sut.send(request) as Response
+ } 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: .fake())
+
+ let request = RequestMock()
+ request.stubbedRequiresAuthentication = false
+
+ // when
+ do {
+ let _ = try await sut.send(request) as Response
+ } catch {}
+
+ // then
+ XCTAssertFalse(interceptorMock.invokedAdapt)
+ XCTAssertFalse(interceptorMock.invokedRefresh)
+ }
+
+ func test_thatRequestProcessorRefreshesCredential_whenCredentialIsNotValid() async {
+ // given
+ requestBuilderMock.stubbedBuildResult = URLRequest.fake()
+ dataRequestHandler.stubbedStartDataTask = .init(data: Data(), response: HTTPURLResponse(), task: .fake())
+ interceptorMock.stubbedIsRequireRefreshResult = true
+
+ let request = RequestMock()
+ request.stubbedRequiresAuthentication = true
+
+ // when
+ do {
+ let _ = try await sut.send(request) as Response
+ } 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
+ do {
+ let _ = try await sut.send(request) as Response
+ } catch {}
+
+ // then
+ XCTAssertFalse(interceptorMock.invokedAdapt)
+ XCTAssertFalse(interceptorMock.invokedRefresh)
+ }
+}
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",
+}
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
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