Showing preview only (494K chars total). Download the full file or copy to clipboard to get everything.
Repository: Kolos65/Mockable
Branch: main
Commit: bd20509c34ca
Files: 113
Total size: 458.9 KB
Directory structure:
gitextract_fbsz8xau/
├── .github/
│ └── workflows/
│ ├── doc.yml
│ └── pr.yml
├── .gitignore
├── .swiftlint.yml
├── LICENSE
├── Package.swift
├── README.md
├── Scripts/
│ ├── doc.sh
│ ├── open.sh
│ ├── test.sh
│ └── utils.sh
├── Sources/
│ ├── Mockable/
│ │ ├── Builder/
│ │ │ ├── Builder.swift
│ │ │ ├── FunctionBuilders/
│ │ │ │ ├── FunctionActionBuilder.swift
│ │ │ │ ├── FunctionReturnBuilder.swift
│ │ │ │ ├── FunctionVerifyBuilder.swift
│ │ │ │ ├── ThrowingFunctionActionBuilder.swift
│ │ │ │ ├── ThrowingFunctionReturnBuilder.swift
│ │ │ │ └── ThrowingFunctionVerifyBuilder.swift
│ │ │ ├── MockableService.swift
│ │ │ └── PropertyBuilders/
│ │ │ ├── PropertyActionBuilder.swift
│ │ │ ├── PropertyReturnBuilder.swift
│ │ │ └── PropertyVerifyBuilder.swift
│ │ ├── Documentation/
│ │ │ └── Documentation.docc/
│ │ │ ├── Configuration.md
│ │ │ ├── Installation.md
│ │ │ ├── Mockable.md
│ │ │ └── Usage.md
│ │ ├── Helpers/
│ │ │ ├── Async+Timeout.swift
│ │ │ ├── AsyncSubject.swift
│ │ │ └── LockedValue.swift
│ │ ├── Macro/
│ │ │ └── MockableMacro.swift
│ │ ├── Matcher/
│ │ │ └── Matcher.swift
│ │ ├── Mocker/
│ │ │ ├── CaseIdentifiable.swift
│ │ │ ├── Matchable.swift
│ │ │ ├── MemberAction.swift
│ │ │ ├── MemberReturn.swift
│ │ │ ├── Mocked.swift
│ │ │ ├── Mocker.swift
│ │ │ ├── MockerFallback.swift
│ │ │ ├── MockerPolicy.swift
│ │ │ └── MockerScope.swift
│ │ ├── Models/
│ │ │ ├── Count.swift
│ │ │ ├── GenericValue.swift
│ │ │ ├── Parameter+Match.swift
│ │ │ ├── Parameter.swift
│ │ │ ├── ReturnValue.swift
│ │ │ └── TimeoutDuration.swift
│ │ └── Utils/
│ │ └── Utils.swift
│ └── MockableMacro/
│ ├── Extensions/
│ │ ├── AttributeListSyntax+Extensions.swift
│ │ ├── AttributeSyntax+Extensions.swift
│ │ ├── DeclModifierListSyntax+Extensions.swift
│ │ ├── FunctionDeclSyntax+Extensions.swift
│ │ ├── FunctionParameterSyntax+Extensions.swift
│ │ ├── GenericArgumentSyntax+Extensions.swift
│ │ ├── ProtocolDeclSyntax+Extensions.swift
│ │ ├── String+Extensions.swift
│ │ ├── TokenSyntax+Extensions.swift
│ │ └── VariableDeclSyntax+Extensions.swift
│ ├── Factory/
│ │ ├── Buildable/
│ │ │ ├── Buildable.swift
│ │ │ ├── Function+Buildable.swift
│ │ │ └── Variable+Buildable.swift
│ │ ├── BuilderFactory.swift
│ │ ├── Caseable/
│ │ │ ├── Caseable.swift
│ │ │ ├── Function+Caseable.swift
│ │ │ └── Variable+Caseable.swift
│ │ ├── ConformanceFactory.swift
│ │ ├── EnumFactory.swift
│ │ ├── Factory.swift
│ │ ├── MemberFactory.swift
│ │ ├── MockFactory.swift
│ │ └── Mockable/
│ │ ├── Function+Mockable.swift
│ │ ├── Initializer+Mockable.swift
│ │ ├── Mockable.swift
│ │ └── Variable+Mockable.swift
│ ├── MockableMacro.swift
│ ├── MockableMacroError.swift
│ ├── MockableMacroWarning.swift
│ ├── Plugin.swift
│ ├── Requirements/
│ │ ├── FunctionRequirement.swift
│ │ ├── InitializerRequirement.swift
│ │ ├── Requirements.swift
│ │ └── VariableRequirement.swift
│ └── Utils/
│ ├── Availability.swift
│ ├── Messages.swift
│ ├── Namespace.swift
│ ├── SwiftVersionHelper.swift
│ └── TokenFinder.swift
└── Tests/
├── MockableMacroTests/
│ ├── AccessModifierTests.swift
│ ├── ActorConformanceTests.swift
│ ├── AssociatedTypeTests.swift
│ ├── AttributesTests.swift
│ ├── DocCommentsTests.swift
│ ├── ExoticParameterTests.swift
│ ├── FunctionEffectTests.swift
│ ├── GenericFunctionTests.swift
│ ├── InheritedTypeMappingTests.swift
│ ├── InitRequirementTests.swift
│ ├── NameCollisionTests.swift
│ ├── PropertyRequirementTests.swift
│ ├── TypedThrowsTests_Swift6.swift
│ └── Utils/
│ └── MockableMacroTestCase.swift
└── MockableTests/
├── ActionTests.swift
├── BuildTests.swift
├── ErrorShadowingCompileTests.swift
├── GivenTests.swift
├── GivenTests_Swift6.swift
├── Helpers/
│ └── Task+Sleep.swift
├── PolicyTests.swift
├── Protocols/
│ ├── Models/
│ │ ├── Product.swift
│ │ ├── User.swift
│ │ └── UserError.swift
│ ├── TestService.swift
│ └── TestService_Swift6.swift
└── VerifyTests.swift
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/doc.yml
================================================
name: update-documentation
on:
push:
branches:
- main
jobs:
update_docs:
name: Update Documentation
runs-on: macos-latest
env:
MOCKABLE_DOC: true
steps:
- name: Checkout main branch
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout documentation branch
run: |
git checkout -b documentation
- name: Update documentation
run: |
Scripts/doc.sh
git config user.name github-actions
git config user.email github-actions@github.com
git add docs
git commit -m 'chore: update documentation'
git push origin documentation --force
================================================
FILE: .github/workflows/pr.yml
================================================
name: pull-request-validation
on:
pull_request:
branches:
- "*"
jobs:
commit-lint:
name: Lint Commit Messages
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Node.js and NPM
uses: actions/setup-node@v4
with:
node-version: "14"
- name: Install commitlint
run: npm install -g @commitlint/cli@11.0.0 @commitlint/config-conventional@11.0.0
- name: Run commitlint
run: commitlint -x @commitlint/config-conventional --from=${{ github.event.pull_request.base.sha }} --to=${{ github.event.pull_request.head.sha }}
swift-lint:
name: SwiftLint
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install SwiftLint
run: brew install swiftlint
- name: Run SwiftLint
run: swiftlint lint --strict Sources Tests/MockableTests
linux-tests:
name: Build and Test on Linux
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
with:
enable_windows_checks: false
linux_env_vars: |
MOCKABLE_LINT=false
MOCKABLE_TEST=true
macOS-tests:
name: Build and Test on macos-latest
runs-on: macos-latest
env:
MOCKABLE_LINT: false
MOCKABLE_TEST: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Swift Version
run: |
swift -version
- name: Run Tests
run: |
swift build && swift test
================================================
FILE: .gitignore
================================================
.DS_Store
/.build
/Packages
Package.resolved
xcuserdata/
DerivedData/
.swiftpm
.netrc
.env
.build
================================================
FILE: .swiftlint.yml
================================================
disabled_rules:
- dynamic_inline
- private_unit_test
- type_body_length
- valid_ibinspectable
- function_default_parameter_at_end
- nesting
- switch_case_alignment
- void_function_in_ternary
- type_name
opt_in_rules:
- closure_spacing
- convenience_type
- discouraged_object_literal
- empty_string
- fallthrough
- fatal_error_message
- first_where
- multiline_arguments
- multiline_parameters
- overridden_super_call
- override_in_extension
- required_enum_case
- vertical_parameter_alignment_on_call
- yoda_condition
- array_init
- explicit_init
- function_default_parameter_at_end
- redundant_nil_coalescing
- closure_body_length
- collection_alignment
- conditional_returns_on_newline
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- contains_over_range_nil_comparison
- empty_collection_literal
- enum_case_associated_values_count
- file_name_no_space
- flatmap_over_map_reduce
- identical_operands
- joined_default_parameter
- last_where
- literal_expression_end_indentation
- no_extension_access_modifier
- nslocalizedstring_key
- operator_usage_whitespace
- private_action
- private_outlet
- prohibited_super_call
- reduce_into
- redundant_type_annotation
- sorted_first_last
- static_operator
- toggle_bool
- unavailable_function
- unneeded_parentheses_in_closure_argument
- vertical_whitespace_closing_braces
- closure_end_indentation
- lower_acl_than_parent
- force_unwrapping
- weak_delegate
analyzer_rules:
- unused_declaration
- unused_import
closing_brace:
severity: error
closure_body_length:
warning: 30
error: 40
closure_end_indentation:
severity: error
colon:
severity: error
flexible_right_spacing: false
apply_to_dictionaries: true
comma:
severity: error
conditional_returns_on_newline:
severity: error
if_only: true
control_statement:
severity: error
cyclomatic_complexity:
warning: 8
error: 9
ignores_case_statements: true
empty_parameters:
severity: error
explicit_init:
severity: error
file_length:
ignore_comment_only_lines: true
warning: 400
error: 500
force_cast:
severity: error
force_try:
severity: error
force_unwrapping:
severity: error
function_parameter_count:
warning: 5
error: 6
implicit_getter:
severity: error
large_tuple:
warning: 3
error: 5
leading_whitespace:
severity: error
legacy_cggeometry_functions:
severity: error
legacy_constant:
severity: error
legacy_constructor:
severity: error
legacy_nsgeometry_functions:
severity: error
line_length:
ignores_comments: true
warning: 120
error: 120
mark:
severity: error
opening_brace:
severity: error
operator_usage_whitespace:
severity: error
operator_whitespace:
severity: error
overridden_super_call:
severity: error
redundant_nil_coalescing:
severity: error
redundant_optional_initialization:
severity: error
redundant_string_enum_value:
severity: error
redundant_void_return:
severity: error
return_arrow_whitespace:
severity: error
syntactic_sugar:
severity: error
todo:
severity: warning
trailing_comma:
severity: error
trailing_newline:
severity: error
trailing_semicolon:
severity: error
trailing_whitespace:
severity: warning
unused_closure_parameter:
severity: error
identifier_name:
excluded:
- to
- id
allowed_symbols:
- _
vertical_parameter_alignment:
severity: error
vertical_whitespace:
severity: error
void_return:
severity: error
weak_delegate:
severity: error
lower_acl_than_parent:
severity: error
first_where:
severity: error
private_action:
severity: error
private_outlet:
severity: error
closure_spacing:
severity: error
array_init:
severity: error
statement_position:
severity: error
unused_enumerated:
severity: error
vertical_parameter_alignment_on_call:
severity: error
empty_string:
severity: error
unneeded_break_in_switch:
severity: error
fatal_error_message:
severity: error
empty_parentheses_with_trailing_closure:
severity: error
contains_over_first_not_nil:
severity: error
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Kolos Foltanyi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Package.swift
================================================
// swift-tools-version: 5.9
import PackageDescription
import CompilerPluginSupport
let test = Context.environment["MOCKABLE_TEST"].flatMap(Bool.init) ?? false
let lint = Context.environment["MOCKABLE_LINT"].flatMap(Bool.init) ?? false
let doc = Context.environment["MOCKABLE_DOC"].flatMap(Bool.init) ?? false
func when<T>(_ condition: Bool, _ list: [T]) -> [T] { condition ? list : [] }
let devDependencies: [Package.Dependency] = when(test, [
.package(url: "https://github.com/pointfreeco/swift-macro-testing", exact: "0.6.4")
]) + when(lint, [
.package(url: "https://github.com/realm/SwiftLint", exact: "0.57.1"),
]) + when(doc, [
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0")
])
let devPlugins: [Target.PluginUsage] = when(lint, [
.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")
])
let devTargets: [Target] = when(test, [
.testTarget(
name: "MockableTests",
dependencies: ["Mockable"],
swiftSettings: [
.define("MOCKING"),
.enableExperimentalFeature("StrictConcurrency"),
.enableUpcomingFeature("ExistentialAny")
],
plugins: devPlugins
),
.testTarget(
name: "MockableMacroTests",
dependencies: [
"MockableMacro",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
.product(name: "MacroTesting", package: "swift-macro-testing"),
],
swiftSettings: [.define("MOCKING")]
)
])
let package = Package(
name: "Mockable",
platforms: [.macOS(.v12), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
products: [
.library(
name: "Mockable",
targets: ["Mockable"]
)
],
dependencies: devDependencies + [
.package(url: "https://github.com/swiftlang/swift-syntax.git", "509.0.0"..<"603.0.0"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", .upToNextMajor(from: "1.6.0"))
],
targets: devTargets + [
.target(
name: "Mockable",
dependencies: [
"MockableMacro",
.product(name: "IssueReporting", package: "xctest-dynamic-overlay")
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
.enableUpcomingFeature("ExistentialAny")
],
plugins: devPlugins
),
.macro(
name: "MockableMacro",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
.enableUpcomingFeature("ExistentialAny")
],
plugins: devPlugins
)
],
swiftLanguageVersions: [.v5, .version("6")]
)
================================================
FILE: README.md
================================================
<p>
<img width="300" src="https://github.com/Kolos65/Mockable/assets/26504214/4e19e4fc-8453-4320-a061-e672dcc95023" alt="@Mockable"/>
</p>
**Mockable** is a [Swift macro](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/) driven testing framework that provides automatic mock implementations for your protocols. It offers an intuitive **declarative syntax** that simplifies the process of mocking services in unit tests. The generated mock implementations can be excluded from release builds using compile conditions.
## Table of Contents
- [Sponsorship](#Sponsorship)
- [Documentation](#Documentation)
- [Installation](#Installation)
- [Configuration](#Configuration)
- [Usage](#Usage)
- [Example](#Example)
- [Syntax](#Syntax)
- [Parameters](#Parameters)
- [Given](#Given)
- [When](#When)
- [Verify](#Verify)
- [Relaxed Mode](#Relaxed-Mode)
- [Non-equatable Types](#Working-with-non-equatable-Types)
- [Supported Features](#Supported-Features)
- [Limitations](#Limitations)
- [Contribution](#Contribution)
- [License](#License)
## Sponsorship
I'm working on **Mockable** as a fun side project. I believe in the power of open source, and I'm thrilled to share it with you. If **Mockable** saves you time and you’d like to show your appreciation, I would be incredibly grateful for your support.
<a href="https://www.buymeacoffee.com/kolos" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
## Documentation
Read **Mockable**'s [documentation](https://kolos65.github.io/Mockable/documentation/mockable) for detailed installation and configuration guides as well as usage examples.
## Installation
The library can be installed using Swift Package Manager.
Add the **Mockable** target to all targets that contain protocols you want to mock. **Mockable** does not depend on the `XCTest` framework so it can be added to any target.
When using the plugin for the first time, be sure to **trust and enable** it when prompted. For unattended usage (e.g. on the CI), macro validation can be disabled with either of the following:
* Using an **xcodebuild** flag: `-skipMacroValidation`
* Setting Xcode defaults: `defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES`
Read the [installation guide](https://kolos65.github.io/Mockable/documentation/mockable/installation/) of the documentation for more details on how to integrate **Mockable** with your project.
## Configuration
Since `@Mockable` is a [peer macro](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/attributes/#attached),
the generated code will always be at the same scope as the protocol it is attached to.
To solve this, the macro expansion is enclosed in a pre-defined compile-time flag called **`MOCKING`** that can be leveraged to exclude generated mock implementations from release builds.
> ⚠️ Since the **`MOCKING`** flag is not defined in your project by default, you won't be able to use mock implementations unless you configure it.
### When using framework modules or a non-modular project:
Define the flag in your target's build settings for debug build configuration(s):
1. Open your **Xcode project**.
2. Go to your target's **Build Settings**.
3. Find **Swift Compiler - Custom Flags**.
4. Add the **MOCKING** flag under the debug configuration(s).
### When using SPM modules or testing a package:
In your module's package manifest under the target definition, you can define the **`MOCKING`** compile-time condition if the build configuration is set to **`debug`**:
```swift
.target(
...
swiftSettings: [
.define("MOCKING", .when(configuration: .debug))
]
)
```
### When using [XcodeGen](https://github.com/yonaskolb/XcodeGen):
Define the flag in your **XcodeGen** specification:
```yml
settings:
...
configs:
debug:
SWIFT_ACTIVE_COMPILATION_CONDITIONS: MOCKING
```
### When using [Tuist](https://tuist.io/):
If you use Tuist, you can define the `MOCKING` flag in your target's settings under `configurations`.
```swift
.target(
...
settings: .settings(
configurations: [
.debug(
name: .debug,
settings: [
"SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING"
]
)
]
)
)
```
Read the [configuration guide](https://kolos65.github.io/Mockable/documentation/mockable/configuration/) of the documentation for more details on how to setup the **`MOCKING`** flag in your project.
## Usage
### Example
Given a protocol annotated with the `@Mockable` macro:
```swift
import Mockable
@Mockable
protocol ProductService {
var url: URL? { get set }
func fetch(for id: UUID) async throws -> Product
func checkout(with product: Product) throws
}
```
A mock implementation named `MockProductService` will be generated, that can be used in unit tests like:
```swift
import Mockable
lazy var productService = MockProductService()
lazy var cartService = CartServiceImpl(productService: productService)
func testCartService() async throws {
let mockURL = URL(string: "apple.com")
let mockError: ProductError = .notFound
let mockProduct = Product(name: "iPhone 15 Pro")
given(productService)
.fetch(for: .any).willReturn(mockProduct)
.checkout(with: .any).willThrow(mockError)
try await cartService.checkout(with: mockProduct, using: mockURL)
verify(productService)
.fetch(for: .value(mockProduct.id)).called(.atLeastOnce)
.checkout(with: .value(mockProduct)).called(.once)
.url(newValue: .value(mockURL)).setCalled(.once)
}
```
### Syntax
**Mockable** has a declarative syntax that utilizes builders to construct `given`, `when`, and `verify` clauses.
When constructing any of these clauses, you always follow the same syntax:
`clause type`(`service`).`function builder`.`behavior builder`
In the following example where we use the previously introduced product service:
```swift
let id = UUID()
let error: ProductError = .notFound
given(productService).fetch(for: .value(id)).willThrow(error)
```
We specify the following:
* **`given`**: we want to register **return values**
* **`(productService)`**: we specify what mockable service we want to register return values for
* **`.fetch(for: .value(id))`**: we want to mock the `fetch(for:)` method and constrain our behavior on calls with matching `id` parameters
* **`.willThrow(error)`**: if `fetch(for:)` is called with the specified parameter value, we want an **error** to be thrown
### Parameters
Function builders have **all parameters** from the original requirement but **encapsulate them** within the [`Parameter<Value>`](https://kolos65.github.io/Mockable/documentation/mockable/parameter) type.
When constructing mockable clauses, you have to **specify parameter conditions** for every parameter of a function. There are three available options:
* **`.any`**: Matches every call to the specified function, disregarding the actual parameter values.
* **`.value(Value)`**: Matches to calls with an identical value in the specified parameter.
* **`.matching((Value) -> Bool)`**: Uses the provided closure to filter functions calls.
> Computed properties have no parameters, but mutable properties get a `(newValue:)` parameter in function builders that can be used
to constraint functionality on property assignment with a match condition. These `newValue` conditions will only effect the `performOnGet`, `performOnSet`, `getCalled` and `setCalled`
clauses but will have no effect on return clauses.
Here are examples of using different parameter conditions:
```swift
// throw an error when `fetch(for:)` is called with `id`
given(productService).fetch(for: .value(id)).willThrow(error)
// print "Ouch!" if product service is called with a product named "iPhone 15 Pro"
when(productService)
.checkout(with: .matching { $0.name == "iPhone 15 Pro" })
.perform { print("Ouch!") }
// assert if the fetch(for:) was called exactly once regardless of what id parameter it was called with
verify(productService).fetch(for: .any).called(.once)
```
### Given
Return values can be specified using a `given(_ service:)` clause. There are three return builders available:
* [`willReturn(_ value:)`](https://kolos65.github.io/Mockable/documentation/mockable/functionreturnbuilder/willreturn(_:)): Will store the given return value and use it to mock subsequent calls.
* [`willThrow(_ error:)`](https://kolos65.github.io/Mockable/documentation/mockable/throwingfunctionreturnbuilder/willthrow(_:)): Will store the given error and throw it in subsequent calls. Only available for throwing functions and properties.
* [`willProduce(_ producer:)`](https://kolos65.github.io/Mockable/documentation/mockable/throwingfunctionreturnbuilder/willproduce(_:)): Will use the provided closure for mocking. The closure has the same signature as the mocked function, so for example a function that takes an integer returns a string and can throw will accept a closure of type `(Int) throws -> String`.
* [`willHandle(_ result:)`](https://kolos65.github.io/Mockable/documentation/mockable/throwingfunctionreturnbuilder/willhandle(_:)): Registers a result type that automatically emits a value when successful or throws a failure.
The provided return values are used up in FIFO order and the last one is always kept for any further calls. Here are examples of using return clauses:
```swift
// Throw an error for the first call and then return 'product' for every other call
given(productService)
.fetch(for: .any).willThrow(error)
.fetch(for: .any).willReturn(product)
// Throw an error if the id parameter ends with a 0, return a product otherwise
given(productService)
.fetch(for: .any).willProduce { id in
if id.uuidString.last == "0" {
throw error
} else {
return product
}
}
```
### When
Side effects can be added using `when(_ service:)` clauses. There are three kind of side effects:
* [`perform(_ action)`](https://kolos65.github.io/Mockable/documentation/mockable/functionactionbuilder/perform(_:)): Will register an operation to perform on invocations of the mocked function.
* [`performOnGet(_ action:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyactionbuilder/performonget(_:)): Available for mutable properties only, will perform the provided operation on property access.
* [`performOnSet(_ action:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyactionbuilder/performonset(_:)): Available for mutable properties only, will perform the provided operation on property assignment.
Some examples of using side effects are:
```swift
// log calls to fetch(for:)
when(productService).fetch(for: .any).perform {
print("fetch(for:) was called")
}
// log when url is accessed
when(productService).url().performOnGet {
print("url accessed")
}
// log when url is set to nil
when(productService).url(newValue: .value(nil)).performOnSet {
print("url set to nil")
}
```
### Verify
You can verify invocations of your mock service using the `verify(_ service:)` clause.
> **Mockable** supports both *XCTest* and *Swift Testing* by using Pointfree's [swift-issue-reporting](https://github.com/pointfreeco/swift-issue-reporting) to dynamically report test failures with the appropriate test framework.
There are three kind of verifications:
* [`called(_:)`](https://kolos65.github.io/Mockable/documentation/mockable/functionverifybuilder/called(_:fileid:filepath:line:column:)): Asserts invocation count based on the given value.
* [`getCalled(_:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyverifybuilder/getcalled(_:fileid:filepath:line:column:)): Available for mutable properties only, asserts property access count.
* [`setCalled(_:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyverifybuilder/setcalled(_:fileid:filepath:line:column:)): Available for mutable properties only, asserts property assignment count.
Here are some example assertions:
```swift
verify(productService)
// assert fetch(for:) was called between 1 and 5 times
.fetch(for: .any).called(.from(1, to: 5))
// assert checkout(with:) was called between exactly 10 times
.checkout(with: .any).called(10)
// assert url property was accessed at least 2 times
.url().getCalled(.moreOrEqual(to: 2))
// assert url property was never set to nil
.url(newValue: .value(nil)).setCalled(.never)
```
If you are testing asynchronous code and cannot write sync assertions you can use the async counterparts of the above verifications:
* [`calledEventually(_:before:)`](https://kolos65.github.io/Mockable/documentation/mockable/functionverifybuilder/calledeventually(_:before:fileid:filepath:line:column:)): Wait until timeout or invocation count satisfied.
* [`getCalledEventually(_:before:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyverifybuilder/getcalledeventually(_:before:fileid:filepath:line:column:)): Wait until timeout or property access count satisfied.
* [`setSalledEventually(_:before:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyverifybuilder/setcalledeventually(_:before:fileid:filepath:line:column:)): Wait until timeout or property assignment count satisfied.
Here are some examples of async verifications:
```swift
await verify(productService)
// assert fetch(for:) was called between 1 and 5 times before default timeout (1 second)
.fetch(for: .any).calledEventually(.from(1, to: 5))
// assert checkout(with:) was called between exactly 10 times before 3 seconds
.checkout(with: .any).calledEventually(10, before: .seconds(3))
// assert url property was accessed at least 2 times before default timeout (1 second)
.url().getCalledEventually(.moreOrEqual(to: 2))
// assert url property was set to nil once
.url(newValue: .value(nil)).setCalledEventually(.once)
```
### Relaxed Mode
By default, you must specify a return value for all requirements; otherwise, a fatal error will be thrown. The reason for this is to aid in the discovery (and thus the verification) of every called function when writing unit tests.
However, it is common to prefer avoiding this strict default behavior in favor of a more relaxed setting, where,
for example, void or optional return values do not need explicit `given` registration.
Use [`MockerPolicy`](https://kolos65.github.io/Mockable/documentation/mockable/mockerpolicy) (which is an [option set](https://developer.apple.com/documentation/swift/optionset)) to implicitly mock:
* only one kind of return value: `.relaxedMocked`
* construct a custom set of policies: `[.relaxedVoid, .relaxedOptional]`
* or opt for a fully relaxed mode: `.relaxed`.
You have two options to override the default strict behavior of the library:
* at **mock implementation level** you can override the mocker policy for each individual mock implementation in the initializer:
```swift
let relaxedMock = MockService(policy: [.relaxedOptional, .relaxedVoid])
```
* at **project level** you can set a custom default policy to use in every scenario by changing the default property of **MockerPolicy**:
```swift
MockerPolicy.default = .relaxedVoid
```
The `.relaxedMocked` policy in combination with the [`Mocked`](https://kolos65.github.io/Mockable/documentation/mockable/mocked) protocol can be used to set an implicit return value for custom (or even built in) types:
```swift
struct Car {
var name: String
var seats: Int
}
extension Car: Mocked {
static var mock: Car {
Car(name: "Mock Car", seats: 4)
}
// Defaults to [mock] but we can
// provide a custom array of cars:
static var mocks: [Car] {
[
Car(name: "Mock Car 1", seats: 4),
Car(name: "Mock Car 2", seats: 4)
]
}
}
@Mockable
protocol CarService {
func getCar() -> Car
func getCars() -> [Car]
}
func testCarService() {
func test() {
let mock = MockCarService(policy: .relaxedMocked)
// Implictly mocked without a given registration:
let car = mock.getCar()
let cars = mock.getCars()
}
}
```
> ⚠️ Relaxed mode will not work with generic return values as the type system is unable to locate the appropriate generic overload.
### Working with non-equatable Types
**Mockable** uses a [`Matcher`](https://kolos65.github.io/Mockable/documentation/mockable/matcher) internally to compare parameters.
By default the matcher is able to compare any custom type that conforms to `Equatable` (except when used in a generic function).
In special cases, when you
* have non-equatable parameter types
* need testing specific equality logic for a custom type
* have generic functions that are used with custom concrete types
you can register your custom types with the `Matcher.register()` functions.
Here is how to do it:
```swift
// register an equatable type to the matcher because we use it in a generic function
Matcher.register(SomeEquatableType.self)
// register a non-equatable type to the matcher
Matcher.register(Product.self, match: { $0.name == $1.name })
// register a meta-type to the matcher
Matcher.register(HomeViewController.Type.self)
// remove all previously registered custom types
Matcher.reset()
```
If you see an error during tests like:
> No comparator found for type "SomeType". All non-equatable types must be registered using Matcher.register(_).
Remember to add the noted type to your `Matcher` using the `register()` function.
## Supported Features
- [x] **Zero-boilerplate** mock generation</br>
- [x] **Exclude** mock implementations **from production** target</br>
- [x] Protocols with **associated types**</br>
- [x] Protocols with **constrained associated types**</br>
- [x] **Init** requirements</br>
- [x] **Generic function parameters** and **return values**</br>
- [x] Generic functions with **where clauses**</br>
- [x] Computed and mutable **property requirements**</br>
- [x] **@escaping closure** parameters</br>
- [x] **Implicitly unwrapped** optionals</br>
- [x] **throwing, rethrowing and async** requirements</br>
- [x] Custom **non-equatable** types
## Limitations
- [ ] **Static Requirements**: Static members cannot be used on protocols and are not supported.</br>
- [ ] **Protocol Inheritance**: Due to limitations of the macro system, inherited protocol requirements won't be implemented.</br>
- [ ] **Return Type Overloads**: Requirements with overloaded return types are not supported in favour of supporting generic return types.
- [ ] **Rethrows functions**: Rethrowing function requirements are always implemented with non-throwing functions.</br>
- [ ] **Non-escaping function parameters**: Non-escaping closure parameters cannot be stored and are not supported.</br>
- [ ] **Subscripts** are not supported (yet).</br>
- [ ] **Operators** are not supported (yet).</br>
## Contribution
If you encounter any issues with the project or have suggestions for improvement, please feel free to open an issue. I value your feedback and am committed to making this project as robust and user-friendly as possible.
The package manifest is set up to only contain test targets and test dependencies if an environment variable named `MOCKABLE_DEV` is set to true. This is done to prevent the overly zealous Swift Package Manager from downloading test dependencies and plugins, such as `swift-macro-testing` or `SwiftLint`, when someone uses **Mockable** as a package dependency.
To open the package with Xcode in "development mode", you need the `MOCKABLE_DEV=true` environment variable to be set. Use `Scripts/open.sh` to open the project (or copy its contents into your terminal) to be able to run tests and lint your code when contributing.
## License
**Mockable** is made available under the MIT License. Please see the [LICENSE](https://raw.githubusercontent.com/Kolos65/Mockable/main/LICENSE) file for more details.
================================================
FILE: Scripts/doc.sh
================================================
#!/bin/bash
swift package \
--allow-writing-to-directory ./docs \
generate-documentation \
--target Mockable \
--output-path ./docs \
--transform-for-static-hosting \
--hosting-base-path Mockable
================================================
FILE: Scripts/open.sh
================================================
#!/bin/bash
export MOCKABLE_LINT=true
export MOCKABLE_TEST=true
open Package.swift
================================================
FILE: Scripts/test.sh
================================================
#!/bin/bash
set -eo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
source $ROOT_DIR/Scripts/utils.sh
# When --vm is passed, the build and test commands are executed in a virtualized container.
if [[ " $* " == *" --vm "* ]]; then
CONTAINER_RUNTIME=$(get_container_runtime)
$CONTAINER_RUNTIME run --rm \
--volume "$ROOT_DIR:/package" \
--workdir "/package" \
-e MOCKABLE_TEST=$MOCKABLE_TEST \
swiftlang/swift:nightly-6.1-focal \
/bin/bash -c \
"swift build --build-path ./.build/linux"
else
swift build --package-path "$ROOT_DIR"
swift test --package-path "$ROOT_DIR"
fi
================================================
FILE: Scripts/utils.sh
================================================
#!/bin/bash
# Function to determine the container runtime
get_container_runtime() {
if command -v podman &> /dev/null; then
echo "podman"
elif command -v docker &> /dev/null; then
echo "docker"
else
echo "Neither podman nor docker is installed. Please install one of them to proceed." >&2
exit 1
fi
}
================================================
FILE: Sources/Mockable/Builder/Builder.swift
================================================
//
// Builder.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 21..
//
/// Used to specify members of a protocol when building
/// given or when clauses of a mock service.
public protocol Builder<Service> {
/// The mock service associated with the Builder.
associatedtype Service: MockableService
/// Initializes a new instance of the builder with the provided `Mocker`.
///
/// - Parameter mocker: The associated service's `Mocker` to provide for subsequent builders.
init(mocker: Mocker<Service>)
}
================================================
FILE: Sources/Mockable/Builder/FunctionBuilders/FunctionActionBuilder.swift
================================================
//
// FunctionActionBuilder.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 22..
//
/// A builder for specifying actions to be performed when mocking a function.
///
/// This builder is used within the context of a higher-level builder (e.g., an `ActionBuilder`)
/// to specify a desired action to perform when a particular function of a mock service is called.
public struct FunctionActionBuilder<T: MockableService, ParentBuilder: Builder<T>> {
/// Convenient type for the associated service's Member.
public typealias Member = T.Member
/// The member being mocked.
private var member: Member
/// The associated service's mocker.
private var mocker: Mocker<T>
/// Initializes a new instance of `FunctionActionBuilder`.
///
/// - Parameters:
/// - mocker: The `Mocker` instance of the associated mock service.
/// - kind: The member being mocked.
public init(_ mocker: Mocker<T>, kind member: Member) {
self.member = member
self.mocker = mocker
}
/// Registers an action to be performed when the provided member is called.
///
/// - Parameter action: The closure representing the action to be performed.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func perform(_ action: @escaping () -> Void) -> ParentBuilder {
mocker.addAction(action, for: member)
return .init(mocker: mocker)
}
}
================================================
FILE: Sources/Mockable/Builder/FunctionBuilders/FunctionReturnBuilder.swift
================================================
//
// FunctionReturnBuilder.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 14..
//
/// A builder for specifying return values or producers when mocking a function.
///
/// This builder is typically used within the context of a higher-level builder (e.g., a `ReturnBuilder`)
/// to specify the desired return value or a return value producer for a particular function of a mock service.
public struct FunctionReturnBuilder<T: MockableService, ParentBuilder: Builder<T>, ReturnType, ProduceType> {
/// Convenient type for the associated service's Member.
public typealias Member = T.Member
/// The member being mocked.
private var member: Member
/// The associated service's mocker.
private var mocker: Mocker<T>
/// Initializes a new instance of `FunctionReturnBuilder`.
///
/// - Parameters:
/// - mocker: The `Mocker` instance of the associated mock service.
/// - kind: The member being mocked.
public init(_ mocker: Mocker<T>, kind member: Member) {
self.member = member
self.mocker = mocker
}
/// Registers a return value to provide when the mocked member is called.
///
/// - Parameter value: The return value to use for mocking the specified member.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func willReturn(_ value: ReturnType) -> ParentBuilder {
mocker.addReturnValue(.return(value), for: member)
return .init(mocker: mocker)
}
/// Registers a return value producing closure to use when the mocked member is called.
///
/// - Parameter producer: A closure that produces a return value for the mocked member.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func willProduce(_ producer: ProduceType) -> ParentBuilder {
mocker.addReturnValue(.produce(producer), for: member)
return .init(mocker: mocker)
}
}
extension FunctionReturnBuilder where ReturnType == Void {
/// Specifies that the void function will return normally when the mocked member is called.
@discardableResult
public func willReturn() -> ParentBuilder {
mocker.addReturnValue(.return(()), for: member)
return .init(mocker: mocker)
}
}
================================================
FILE: Sources/Mockable/Builder/FunctionBuilders/FunctionVerifyBuilder.swift
================================================
//
// FunctionVerifyBuilder.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 22..
//
/// A builder for verifying the number of times a mocked member was called.
///
/// This builder is typically used within the context of a higher-level builder (e.g., a `VerifyBuilder`)
/// to verify the expected number of invocations for a particular function of a mock service.
public struct FunctionVerifyBuilder<T: MockableService, ParentBuilder: Builder<T>> {
/// Convenient type for the associated service's Member.
public typealias Member = T.Member
/// The associated service's mocker.
private var mocker: Mocker<T>
/// The member being verified.
private var member: Member
/// Initializes a new instance of `FunctionVerifyBuilder`.
///
/// - Parameters:
/// - mocker: The `Mocker` instance of the associated mock service.
/// - kind: The member being verified.
public init(_ mocker: Mocker<T>, kind member: Member) {
self.member = member
self.mocker = mocker
}
/// Asserts the number of invocations of the specified member using `count`.
///
/// - Parameter count: Specifies the expected invocation count.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func called(
_ count: Count,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column) -> ParentBuilder {
mocker.verify(
member: member,
count: count,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return .init(mocker: mocker)
}
/// Asynchronously waits at most `timeout` interval for a successfuly assertion of
/// `count` invocations, then fails.
///
/// - Parameters:
/// - count: Specifies the expected invocation count.
/// - timeout: The maximum time it will wait for assertion to be true. Default 1 second.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func calledEventually(_ count: Count,
before timeout: TimeoutDuration = .seconds(1),
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column) async -> ParentBuilder {
await mocker.verify(
member: member,
count: count,
timeout: timeout,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return .init(mocker: mocker)
}
}
================================================
FILE: Sources/Mockable/Builder/FunctionBuilders/ThrowingFunctionActionBuilder.swift
================================================
//
// ThrowingFunctionActionBuilder.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 22..
//
/// A builder for specifying actions to be performed when mocking a throwing function.
///
/// This builder is used within the context of a higher-level builder (e.g., an `ActionBuilder`)
/// to specify a desired action to perform when a particular throwing function of a mock service is called.
public typealias ThrowingFunctionActionBuilder<T: MockableService, ParentBuilder: Builder<T>>
= FunctionActionBuilder<T, ParentBuilder>
================================================
FILE: Sources/Mockable/Builder/FunctionBuilders/ThrowingFunctionReturnBuilder.swift
================================================
//
// ThrowingFunctionReturnBuilder.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 22..
//
/// A builder for specifying return values or producers when mocking a function.
///
/// This builder is typically used within the context of a higher-level builder (e.g., a `ReturnBuilder`)
/// to specify the desired return value or a return value producer for a throwing function of a mock service.
public struct ThrowingFunctionReturnBuilder<
T: MockableService,
ParentBuilder: Builder<T>,
ReturnType,
ErrorType: Error,
ProduceType
> {
/// Convenient type for the associated service's Member.
public typealias Member = T.Member
/// The member being mocked.
private var member: Member
/// The associated service's mocker.
private var mocker: Mocker<T>
/// Initializes a new instance of `FunctionReturnBuilder`.
///
/// - Parameters:
/// - mocker: The `Mocker` instance of the associated mock service.
/// - kind: The member being mocked.
public init(_ mocker: Mocker<T>, kind member: Member) {
self.member = member
self.mocker = mocker
}
/// Registers a return value to provide when the mocked member is called.
///
/// - Parameter value: The return value to use for mocking the specified member.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func willReturn(_ value: ReturnType) -> ParentBuilder {
mocker.addReturnValue(.return(value), for: member)
return .init(mocker: mocker)
}
/// Registers an error to be thrown when the mocked member is called.
///
/// - Parameter error: The error to be thrown for mocking the specified member.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func willThrow(_ error: ErrorType) -> ParentBuilder {
mocker.addReturnValue(.throw(error), for: member)
return .init(mocker: mocker)
}
/// Registers a return value producing closure to use when the mocked member is called.
///
/// - Parameter producer: A closure that produces a return value or throws an error.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func willProduce(_ producer: ProduceType) -> ParentBuilder {
mocker.addReturnValue(.produce(producer), for: member)
return .init(mocker: mocker)
}
/// Registers a result type that automatically emits a value when successful or throws a failure.
///
/// - Parameter result: The result type wrapping a value or error.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func willHandle(_ result: Result<ReturnType, ErrorType>) -> ParentBuilder {
switch result {
case let .success(value):
mocker.addReturnValue(.return(value), for: member)
case let .failure(error):
mocker.addReturnValue(.throw(error), for: member)
}
return .init(mocker: mocker)
}
}
extension ThrowingFunctionReturnBuilder where ReturnType == Void {
/// Specifies that the void function will return normally when the mocked member is called.
@discardableResult
public func willReturn() -> ParentBuilder {
mocker.addReturnValue(.return(()), for: member)
return .init(mocker: mocker)
}
}
================================================
FILE: Sources/Mockable/Builder/FunctionBuilders/ThrowingFunctionVerifyBuilder.swift
================================================
//
// ThrowingFunctionVerifyBuilder.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 22..
//
/// A builder for verifying the number of times a throwing function was called.
///
/// This builder is typically used within the context of a higher-level builder (e.g., a `VerifyBuilder`)
/// to verify the expected number of invocations for a throwing function of a mock service.
public typealias ThrowingFunctionVerifyBuilder<T: MockableService, ParentBuilder: Builder<T>>
= FunctionVerifyBuilder<T, ParentBuilder>
================================================
FILE: Sources/Mockable/Builder/MockableService.swift
================================================
//
// MockableService.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 13..
//
/// A protocol defining the structure for a mocked service.
///
/// Conforming types must provide a `Member` type representing their members
/// as well as builders for specifying return values, actions, and verifications.
public protocol MockableService {
/// A type representing the members of the mocked protocol.
associatedtype Member: Matchable, CaseIdentifiable, Sendable
/// A builder responsible for registering return values.
associatedtype ReturnBuilder: Builder<Self>
/// A builder responsible for registering side-effects.
associatedtype ActionBuilder: Builder<Self>
/// A builder responsible for asserting member invocations.
associatedtype VerifyBuilder: Builder<Self>
/// Encapsulates member return values.
typealias Return = MemberReturn<Member>
/// Encapsulates member side-effects.
typealias Action = MemberAction<Member>
/// A builder proxy for specifying return values.
var _given: ReturnBuilder { get }
/// A builder proxy for specifying actions.
var _when: ActionBuilder { get }
/// The builder proxy for verifying invocations.
var _verify: VerifyBuilder { get }
}
================================================
FILE: Sources/Mockable/Builder/PropertyBuilders/PropertyActionBuilder.swift
================================================
//
// PropertyActionBuilder.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 22..
//
/// A builder for specifying actions to be performed when mocking the getter and setter of a property.
///
/// This builder is typically used within the context of a higher-level builder (e.g., an `ActionBuilder`)
/// to specify the behavior of the getter and setter of a particular property of a mock.
public struct PropertyActionBuilder<T: MockableService, ParentBuilder: Builder<T>> {
/// Convenient type for the associated service's Member.
public typealias Member = T.Member
/// The member representing the getter of the property.
var getMember: Member
/// The member representing the setter of the property.
var setMember: Member
/// The associated service's mocker.
var mocker: Mocker<T>
/// Initializes a new instance of `PropertyActionBuilder`.
///
/// - Parameters:
/// - mocker: The `Mocker` instance of the associated mock service.
/// - getKind: The member representing the getter of the property.
/// - setKind: The member representing the setter of the property.
public init(_ mocker: Mocker<T>, kind getMember: Member, setKind setMember: Member) {
self.getMember = getMember
self.setMember = setMember
self.mocker = mocker
}
/// Specifies the action to be performed when the getter of the property is called.
///
/// - Parameter action: The closure representing the action to be performed.
/// - Returns: The parent builder, typically used for chaining additional specifications.
@discardableResult
public func performOnGet(_ action: @escaping () -> Void) -> ParentBuilder {
mocker.addAction(action, for: getMember)
return .init(mocker: mocker)
}
/// Specifies the action to be performed when the setter of the property is called.
///
/// - Parameter action: The closure representing the action to be performed.
/// - Returns: The parent builder, typically used for chaining additional specifications.
@discardableResult
public func performOnSet(_ action: @escaping () -> Void) -> ParentBuilder {
mocker.addAction(action, for: setMember)
return .init(mocker: mocker)
}
}
================================================
FILE: Sources/Mockable/Builder/PropertyBuilders/PropertyReturnBuilder.swift
================================================
//
// PropertyReturnBuilder.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 22..
//
/// A builder for specifying return values or producers when mocking the getter of a mutable property.
///
/// This builder is typically used within the context of a higher-level builder (e.g., a `ReturnBuilder`)
/// to specify the desired return value or a return value producer for the getter
/// of a particular property of a mock.
public struct PropertyReturnBuilder<T: MockableService, ParentBuilder: Builder<T>, ReturnType> {
/// Convenient type for the associated service's Member.
public typealias Member = T.Member
/// The member representing the getter of the property.
var getMember: Member
/// The associated service's mocker.
var mocker: Mocker<T>
/// Initializes a new instance of `PropertyReturnBuilder`.
///
/// - Parameters:
/// - mocker: The `Mocker` instance of the associated mock service.
/// - getKind: The member representing the getter of the property.
public init(_ mocker: Mocker<T>, kind getMember: Member) {
self.getMember = getMember
self.mocker = mocker
}
/// Specifies the return value when the getter of the property is called.
///
/// - Parameter value: The return value to use for mocking the specified getter.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func willReturn(_ value: ReturnType) -> ParentBuilder {
mocker.addReturnValue(.return(value), for: getMember)
return .init(mocker: mocker)
}
/// Specifies the return value producing closure to use when the getter of the property is called.
///
/// - Parameter producer: A closure that produces a return value for the getter.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func willProduce(_ producer: @escaping () -> ReturnType) -> ParentBuilder {
mocker.addReturnValue(.produce(producer), for: getMember)
return .init(mocker: mocker)
}
}
================================================
FILE: Sources/Mockable/Builder/PropertyBuilders/PropertyVerifyBuilder.swift
================================================
//
// PropertyVerifyBuilder.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 22..
//
/// A builder for verifying the number of times the getter and setter of a property are called.
///
/// This builder is typically used within the context of a higher-level builder (e.g., a `VerifyBuilder`)
/// to verify the expected number of invocations for the getter and setter of a particular property of a mock.
public struct PropertyVerifyBuilder<T: MockableService, ParentBuilder: Builder<T>> {
/// Convenient type for the associated service's Member.
public typealias Member = T.Member
/// The associated service's mocker.
var mocker: Mocker<T>
/// The member representing the getter of the property.
var getMember: Member
/// The member representing the setter of the property.
var setMember: Member
/// Initializes a new instance of `PropertyVerifyBuilder`.
///
/// - Parameters:
/// - mocker: The `Mocker` instance of the associated mock service.
/// - getKind: The member representing the getter of the property.
/// - setKind: The member representing the setter of the property.
public init(_ mocker: Mocker<T>,
kind getMember: Member,
setKind setMember: Member) {
self.getMember = getMember
self.setMember = setMember
self.mocker = mocker
}
/// Specifies the expected number of times the getter of the property should be called.
///
/// - Parameter count: The `Count` object specifying the expected invocation count.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func getCalled(
_ count: Count,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) -> ParentBuilder {
mocker.verify(
member: getMember,
count: count,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return .init(mocker: mocker)
}
/// Asynchronously waits at most `timeout` interval for a successfuly assertion of
/// `count` invocations of the property's getter, then fails.
///
/// - Parameters:
/// - count: Specifies the expected invocation count.
/// - timeout: The maximum time it will wait for assertion to be true. Default 1 second.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func getCalledEventually(_ count: Count,
before timeout: TimeoutDuration = .seconds(1),
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column) async -> ParentBuilder {
await mocker.verify(
member: getMember,
count: count,
timeout: timeout,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return .init(mocker: mocker)
}
/// Specifies the expected number of times the setter of the property should be called.
///
/// - Parameter count: The `Count` object specifying the expected invocation count.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func setCalled(
_ count: Count,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column) -> ParentBuilder {
mocker.verify(
member: setMember,
count: count,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return .init(mocker: mocker)
}
/// Asynchronously waits at most `timeout` interval for a successfuly assertion of
/// `count` invocations fot the property's setter, then fails.
///
/// - Parameters:
/// - count: Specifies the expected invocation count.
/// - timeout: The maximum time it will wait for assertion to be true. Default 1 second.
/// - Returns: The parent builder, used for chaining additional specifications.
@discardableResult
public func setCalledEventually(_ count: Count,
before timeout: TimeoutDuration = .seconds(1),
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column) async -> ParentBuilder {
await mocker.verify(
member: setMember,
count: count,
timeout: timeout,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return .init(mocker: mocker)
}
}
================================================
FILE: Sources/Mockable/Documentation/Documentation.docc/Configuration.md
================================================
# Configuration
Learn how to configure build settings so generated mock implementations are excluded from release builds.
## Overview
Since `@Mockable` is a peer macro the generated code will always be at the same scope as the protocol it is attached to. From **Mockable**'s perspective this is unfortunate as we don't want to include our generated mock implementations in the release bundle.
To solve this, the macro expansion is enclosed in a pre-defined compile-time flag called **`MOCKING`** that can be leveraged to exclude generated mock implementations from release builds:
```swift
#if MOCKING
public final class MockService: Service, Mockable {
// generated code...
}
#endif
```
> Since the **`MOCKING`** flag is not defined in your project by default, you won't be able to use mock implementations unless you configure it.
There are many ways to define the flag depending on how your project is set up or what tool you use for build setting generation. Below you can find how to define the `MOCKING` flag in three common scenarios.
## Define the flag using Xcode
If your porject relies on Xcode build settings, define the flag in your target's build settings for debug build configuration(s):
1. Open your **Xcode project**.
2. Go to your target's **Build Settings**.
3. Find **Swift Compiler - Custom Flags**.
4. Add the **MOCKING** flag under the debug build configuration(s).
5. Repeat these steps for all of your targets where you want to use the `@Mockable` macro.
## Using a Package.swift manifest
If you are using SPM modules or working with a package, define the **`MOCKING`** compile-time condition in your package manifest. Using a `.when(configuration:)` build setting condition you can define the flag only if the build configuration is set to `debug`.
```swift
.target(
...
swiftSettings: [
.define("MOCKING", .when(configuration: .debug))
]
)
```
## Using XcodeGen
If you use XcodeGen to generate you project, you can define the `MOCKING` flag in your yaml file under the `configs` definition.
```yml
settings:
...
configs:
debug:
SWIFT_ACTIVE_COMPILATION_CONDITIONS: MOCKING
```
## Using [Tuist](https://tuist.io/):
If you use Tuist, you can define the `MOCKING` flag in your target's settings under `configurations`.
```swift
.target(
...
settings: .settings(
configurations: [
.debug(
name: .debug,
settings: [
"SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING"
]
)
]
)
)
```
## Summary
By defining the `MOCKING` condition in the debug build configuration you ensured that generated mock implementations are excluded from release builds and kept available for your unit tests that use the debug configuration to build tested targets.
================================================
FILE: Sources/Mockable/Documentation/Documentation.docc/Installation.md
================================================
# Installation
Learn how to install **Mockable** and integrate into your targets.
## Overview
**Mockable** can be installed using Swift Package Manager.
Add the **Mockable** target to all targets that contain protocols you want to mock. **Mockable** does not depend on the `XCTest` framework so it can be added to any target.
## Add **Mockable** using Xcode
1. Open your Xcode project.
2. Navigate to the **File** menu and select **Add Package Dependencies...**
3. Enter the following URL: **`https://github.com/Kolos65/Mockable`**
4. Specify the version you want to use and click **Add Package**.
6. Click **Add Package**
> If you have multiple targets or multiple test targets:
> Navigate to each target's **General** settings and add Mockable under the **Frameworks and Libraries** settings.
### Using a Package.swift manifest:
If you have SPM modules or you want to test an SPM package, add **Mockable** as a package dependency in the manifest file.
In you target definitions add the **Mockable** product to your main and test targets.
```swift
let package = Package(
...
dependencies: [
.package(url: "https://github.com/Kolos65/Mockable", from: "0.0.1"),
],
targets: [
.target(
...
dependencies: [
.product(name: "Mockable", package: "Mockable")
]
),
.testTarget(
...
dependencies: [
.product(name: "Mockable", package: "Mockable")
]
)
]
)
```
### Using XcodeGen:
Add Mockable to the `packages` definition and the `targets` definition.
```yaml
packages:
Mockable:
url: https://github.com/Kolos65/Mockable
from: "0.0.1"
targets:
MyApp:
...
dependencies:
- package: Mockable
product: Mockable
MyAppUnitTests:
...
dependencies:
- package: Mockable
product: Mockable
```
================================================
FILE: Sources/Mockable/Documentation/Documentation.docc/Mockable.md
================================================
# ``Mockable``
A macro driven testing framework that provides automatic mock implementations for your protocols. It offers an intuitive declarative syntax that simplifies the process of mocking services in unit tests.
## Overview
**Mockable** utilizes the new Swift macro system to generate code that eliminates the need for external dependencies like Sourcery.
It has a declarative API that enables you to rapidly specify return values and verify invocations in a readable format.
Associated types, generic functions, where clauses and constrained generic arguments are all supported.
The generated mock implementations can be excluded from release builds using a built in compile condition.
## Topics
### Guides
- <doc:Installation>
- <doc:Configuration>
- <doc:Usage>
================================================
FILE: Sources/Mockable/Documentation/Documentation.docc/Usage.md
================================================
# Usage
Larn how to use **Mockable** to write readable and concise unit tests.
## Overview
Given a protocol annotated with the `@Mockable` macro:
```swift
import Mockable
@Mockable
protocol ProductService {
var url: URL? { get set }
func fetch(for id: UUID) async throws -> Product
func checkout(with product: Product) throws
}
```
A mock implementation named `MockProductService` will be generated, that can be used in unit tests like this:
```swift
import Mockable
lazy var productService = MockProductService()
lazy var cartService = CartServiceImpl(productService: productService)
func testCartService() async throws {
let mockURL = URL(string: "apple.com")
let mockError: ProductError = .notFound
let mockProduct = Product(name: "iPhone 15 Pro")
given(productService)
.fetch(for: .any).willReturn(mockProduct)
.checkout(with: .any).willThrow(mockError)
try await cartService.checkout(with: mockProduct, using: mockURL)
verify(productService)
.fetch(for: .value(mockProduct.id)).called(.atLeastOnce)
.checkout(with: .value(mockProduct)).called(.once)
.url(newValue: .value(mockURL)).setCalled(.once)
}
```
## Syntax
**Mockable** has a declarative syntax that utilizes builders to construct `given`, `when`, and `verify` clauses.
When constructing any of these clauses, you always follow the same syntax:
`clause type`(`service`).`function builder`.`behavior builder`
In the following example where we use the previously introduced product service:
```swift
let id = UUID()
let error: ProductError = .notFound
given(productService).fetch(for: .value(id)).willThrow(error)
```
We specify the following:
* **`given`**: we want to register **return values**
* **`(productService)`**: we specify what mockable service we want to register return values for
* **`.fetch(for: .value(id))`**: we want to mock the `fetch(for:)` method and constrain our behavior on calls with matching `id` parameters
* **`.willThrow(error)`**: if `fetch(for:)` is called with the specified parameter value, we want an **error** to be thrown
## Parameters
Function builders have **all parameters** from the original requirement but **encapsulate them** within the [`Parameter<Value>`](https://kolos65.github.io/Mockable/documentation/mockable/parameter) type.
When constructing mockable clauses, you have to **specify parameter conditions** for every parameter of a function. There are three available options:
* **`.any`**: Matches every call to the specified function, disregarding the actual parameter values.
* **`.value(Value)`**: Matches to calls with an identical value in the specified parameter.
* **`.matching((Value) -> Bool)`**: Uses the provided closure to filter functions calls.
> Computed properties have no parameters, but mutable properties get a `(newValue:)` parameter in function builders that can be used
to constraint functionality on property assignment with a match condition. These `newValue` conditions will only effect the `performOnGet`, `performOnSet`, `getCalled` and `setCalled`
clauses but will have no effect on return clauses.
Here are examples of using different parameter conditions:
```swift
// throw an error when `fetch(for:)` is called with `id`
given(productService).fetch(for: .value(id)).willThrow(error)
// print "Ouch!" if product service is called with a product named "iPhone 15 Pro"
when(productService)
.checkout(with: .matching { $0.name == "iPhone 15 Pro" })
.perform { print("Ouch!") }
// assert if the fetch(for:) was called exactly once regardless of what id parameter it was called with
verify(productService).fetch(for: .any).called(.once)
```
## Given
Return values can be specified using a `given(_ service:)` clause. There are three return builders available:
* [`willReturn(_ value:)`](https://kolos65.github.io/Mockable/documentation/mockable/functionreturnbuilder/willreturn(_:)): Will store the given return value and use it to mock subsequent calls.
* [`willThrow(_ error:)`](https://kolos65.github.io/Mockable/documentation/mockable/throwingfunctionreturnbuilder/willthrow(_:)): Will store the given error and throw it in subsequent calls. Only available for throwing functions and properties.
* [`willProduce(_ producer:)`](https://kolos65.github.io/Mockable/documentation/mockable/throwingfunctionreturnbuilder/willproduce(_:)): Will use the provided closure for mocking. The closure has the same signature as the mocked function, so for example a function that takes an integer returns a string and can throw will accept a closure of type `(Int) throws -> String`.
* [`willHandle(_ result:)`](https://kolos65.github.io/Mockable/documentation/mockable/throwingfunctionreturnbuilder/willhandle(_:)): Registers a result type that automatically emits a value when successful or throws a failure.
The provided return values are used up in FIFO order and the last one is always kept for any further calls. Here are examples of using return clauses:
```swift
// Throw an error for the first call and then return 'product' for every other call
given(productService)
.fetch(for: .any).willThrow(error)
.fetch(for: .any).willReturn(product)
// Throw an error if the id parameter ends with a 0, return a product otherwise
given(productService)
.fetch(for: .any).willProduce { id in
if id.uuidString.last == "0" {
throw error
} else {
return product
}
}
```
## When
Side effects can be added using `when(_ service:)` clauses. There are three kind of side effects:
* [`perform(_ action)`](https://kolos65.github.io/Mockable/documentation/mockable/functionactionbuilder/perform(_:)): Will register an operation to perform on invocations of the mocked function.
* [`performOnGet(_ action:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyactionbuilder/performonget(_:)): Available for mutable properties only, will perform the provided operation on property access.
* [`performOnSet(_ action:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyactionbuilder/performonset(_:)): Available for mutable properties only, will perform the provided operation on property assignment.
Some examples of using side effects are:
```swift
// log calls to fetch(for:)
when(productService).fetch(for: .any).perform {
print("fetch(for:) was called")
}
// log when url is accessed
when(productService).url().performOnGet {
print("url accessed")
}
// log when url is set to nil
when(productService).url(newValue: .value(nil)).performOnSet {
print("url set to nil")
}
```
## Verify
You can verify invocations of your mock service using the `verify(_ service:)` clause.
> **Mockable** supports both *XCTest* and *Swift Testing* by using Pointfree's [swift-issue-reporting](https://github.com/pointfreeco/swift-issue-reporting) to dynamically report test failures with the appropriate test framework.
There are three kind of verifications:
* [`called(_:)`](https://kolos65.github.io/Mockable/documentation/mockable/functionverifybuilder/called(_:file:line:)): Asserts invocation count based on the given value.
* [`getCalled(_:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyverifybuilder/getcalled(_:file:line:)): Available for mutable properties only, asserts property access count.
* [`setCalled(_:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyverifybuilder/setcalled(_:file:line:)): Available for mutable properties only, asserts property assignment count.
Here are some example assertions:
```swift
verify(productService)
// assert fetch(for:) was called between 1 and 5 times
.fetch(for: .any).called(.from(1, to: 5))
// assert checkout(with:) was called between exactly 10 times
.checkout(with: .any).called(10)
// assert url property was accessed at least 2 times
.url().get(.moreOrEqual(to: 2))
// assert url property was never set to nil
.url(newValue: .value(nil)).setCalled(.never)
```
If you are testing asynchronous code and cannot write sync assertions you can use the async counterparts of the above verifications:
* [`calledEventually(_:before:)`](https://kolos65.github.io/Mockable/documentation/mockable/functionverifybuilder/calledeventually(_:before:file:line:)): Wait until timeout or invocation count satisfied.
* [`getCalledEventually(_:before:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyverifybuilder/getcalledeventually(_:before:file:line:)): Wait until timeout or property access count satisfied.
* [`setSalledEventually(_:before:)`](https://kolos65.github.io/Mockable/documentation/mockable/propertyverifybuilder/setcalledeventually(_:before:file:line:)): Wait until timeout or property assignment count satisfied.
Here are some examples of async verifications:
```swift
await verify(productService)
// assert fetch(for:) was called between 1 and 5 times before default timeout (1 second)
.fetch(for: .any).calledEventually(.from(1, to: 5))
// assert checkout(with:) was called between exactly 10 times before 3 seconds
.checkout(with: .any).calledEventually(10, before: .seconds(3))
// assert url property was accessed at least 2 times before default timeout (1 second)
.url().getCalledEventually(.moreOrEqual(to: 2))
// assert url property was set to nil once
.url(newValue: .value(nil)).setCalledEventually(.once)
```
## Relaxed Mode
By default, you must specify a return value for all requirements; otherwise, a fatal error will be thrown. The reason for this is to aid in the discovery (and thus the verification) of every called function when writing unit tests.
However, it is common to prefer avoiding this strict default behavior in favor of a more relaxed setting, where,
for example, void or optional return values do not need explicit `given` registration.
Use the Use [`MockerPolicy`](https://kolos65.github.io/Mockable/documentation/mockable/mockerpolicy) (which is an [option set](https://developer.apple.com/documentation/swift/optionset)) to implicitly mock:
* only one kind of return value: `.relaxedMocked`
* construct a custom set of policies: `[.relaxedVoid, .relaxedOptional]`
* or opt for a fully relaxed mode: `.relaxed`.
You have two options to override the default strict behavior of the library:
* At **mock implementation level** you can override the mocker policy for each individual mock implementation in the initializer:
```swift
let relaxedMock = MockService(policy: [.relaxedOptional, .relaxedVoid])
```
* At **project level** you can set a custom default policy to use in every scenario by changing the default property of **MockerPolicy**:
```swift
MockerPolicy.default = .relaxedVoid
```
The `.relaxedMocked` policy in combination with the [`Mocked`](https://kolos65.github.io/Mockable/documentation/mockable/mocked) protocol can be used to set an implicit return value for custom (or even built in) types:
```swift
struct Car {
var name: String
var seats: Int
}
extension Car: Mocked {
static var mock: Car {
Car(name: "Mock Car", seats: 4)
}
// Defaults to [mock] but we can
// provide a custom array of cars:
static var mocks: [Car] {
[
Car(name: "Mock Car 1", seats: 4),
Car(name: "Mock Car 2", seats: 4)
]
}
}
@Mockable
protocol CarService {
func getCar() -> Car
func getCars() -> [Car]
}
func testCarService() {
func test() {
let mock = MockCarService(policy: .relaxedMocked)
// Implictly mocked without a given registration:
let car = mock.getCar()
let cars = mock.getCars()
}
}
```
> ⚠️ Relaxed mode will not work with generic return values as the type system is unable to locate the appropriate generic overload.
## Working with non-equatable Types
**Mockable** uses a [`Matcher`](https://kolos65.github.io/Mockable/documentation/mockable/matcher) internally to compare parameters.
By default the matcher is able to compare any custom type that conforms to `Equatable` (except when used in a generic function).
In special cases, when you
* have non-equatable parameter types
* need testing specific equality logic for a custom type
* have generic functions that are used with custom concrete types
you can register your custom types with the `Matcher.register()` functions.
Here is how to do it:
```swift
// register an equatable type to the matcher because we use it in a generic function
Matcher.register(SomeEquatableType.self)
// register a non-equatable type to the matcher
Matcher.register(Product.self, match: { $0.name == $1.name })
// register a meta-type to the matcher
Matcher.register(HomeViewController.Type.self)
// remove all previously registered custom types
Matcher.reset()
```
If you see this error during tests:
```
No comparator found for type XYZ. All non-equatable types must be
registered using Matcher.register(_).
```
remember to add the noted type to your `Matcher` using the `register()` function.
================================================
FILE: Sources/Mockable/Helpers/Async+Timeout.swift
================================================
//
// Async+Timeout.swift
// Mockable
//
// Created by Kolos Foltanyi on 2024. 04. 04..
//
import Foundation
public struct TimeoutError: Error {}
/// Runs an async task with a timeout.
///
/// - Parameters:
/// - maxDuration: The duration in seconds `work` is allowed to run before timing out.
/// - work: The async operation to perform.
/// - Returns: Returns the result of `work` if it completed in time.
/// - Throws: Throws ``TimedOutError`` if the timeout expires before `work` completes.
/// If `work` throws an error before the timeout expires, that error is propagated to the caller.
@discardableResult
func withTimeout<Value: Sendable>(
after maxDuration: TimeInterval,
_ operation: @Sendable @escaping () async throws -> Value
) async throws -> Value {
try await withThrowingTaskGroup(of: Value.self) { group in
group.addTask(operation: operation)
group.addTask {
try await Task.sleep(nanoseconds: UInt64(maxDuration * 1_000_000_000))
throw TimeoutError()
}
let result = try await group.next()! // swiftlint:disable:this force_unwrapping
group.cancelAll()
return result
}
}
================================================
FILE: Sources/Mockable/Helpers/AsyncSubject.swift
================================================
//
// AsyncSubject.swift
// Mockable
//
// Created by Kolos Foltanyi on 2024. 12. 16..
//
actor AsyncSubject<Element: Sendable>: AsyncSequence {
typealias Failure = Never
// MARK: Types
struct Subscription: Equatable {
let id: UInt64
let continuation: AsyncStream<Element>.Continuation
let stream: AsyncStream<Element>
static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id }
}
// MARK: Private properties
private(set) var value: Element
private var ids: UInt64 = 0
private var subscriptions = [Subscription]()
// MARK: Init
init(_ initialValue: Element) {
self.value = initialValue
}
deinit {
for subscription in subscriptions {
subscription.continuation.finish()
}
}
func finish() {
for subscription in subscriptions {
subscription.continuation.finish()
}
subscriptions.removeAll()
}
nonisolated func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(parent: self)
}
func generateId() -> UInt64 {
defer { ids &+= 1 }
return ids
}
fileprivate func subscribe() -> (Subscription, Element) {
let (stream, continuation) = AsyncStream<Element>.makeStream()
let subscription = Subscription(id: generateId(), continuation: continuation, stream: stream)
subscriptions.append(subscription)
return (subscription, value)
}
fileprivate func remove(_ subscription: Subscription) {
subscriptions.removeAll { $0 == subscription }
subscription.continuation.finish()
}
func send(_ value: Element) {
self.value = value
for subscription in subscriptions {
subscription.continuation.yield(value)
}
}
func update(with block: (inout Element) -> Void) {
block(&value)
send(value)
}
class AsyncIterator: AsyncIteratorProtocol {
private weak var parent: AsyncSubject?
private var subscription: Subscription?
private var iterator: AsyncStream<Element>.AsyncIterator?
fileprivate init(parent: AsyncSubject) {
self.parent = parent
}
deinit {
cancelSubscription()
}
func next() async -> Element? {
if iterator != nil {
return await iterator?.next()
} else if let parent {
let (subscription, value) = await parent.subscribe()
self.subscription = subscription
iterator = subscription.stream.makeAsyncIterator()
return value
} else {
return nil
}
}
private func cancelSubscription() {
guard let parent, let subscription else { return }
Task { await parent.remove(subscription) }
}
}
}
================================================
FILE: Sources/Mockable/Helpers/LockedValue.swift
================================================
//
// LockedValue.swift
// Mockable
//
// Created by Kolos Foltanyi on 2024. 12. 16..
//
import Foundation
/// A generic wrapper for isolating a mutable value with a lock.
///
/// If you trust the sendability of the underlying value, consider using ``UncheckedSendable``,
/// instead.
@dynamicMemberLookup
final class LockedValue<Value>: @unchecked Sendable {
private var _value: Value
private let lock = NSRecursiveLock()
private var didSet: ((Value) -> Void)?
/// Initializes lock-isolated state around a value.
///
/// - Parameter value: A value to isolate with a lock.
init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {
self._value = try value()
}
subscript<Subject>(dynamicMember keyPath: KeyPath<Value, Subject>) -> Subject {
self.lock.criticalRegion {
self._value[keyPath: keyPath]
}
}
/// Perform an operation with isolated access to the underlying value.
///
/// Useful for modifying a value in a single transaction.
///
/// ```swift
/// // Isolate an integer for concurrent read/write access:
/// var count = LockedValue(0)
///
/// func increment() {
/// // Safely increment it:
/// self.count.withValue { $0 += 1 }
/// }
/// ```
///
/// - Parameter operation: An operation to be performed on the the underlying value with a lock.
/// - Returns: The result of the operation.
func withValue<T>(
_ operation: (inout Value) throws -> T
) rethrows -> T {
try self.lock.criticalRegion {
var value = self._value
defer {
self._value = value
self.didSet?(self._value)
}
return try operation(&value)
}
}
/// Overwrite the isolated value with a new value.
///
/// ```swift
/// // Isolate an integer for concurrent read/write access:
/// var count = LockedValue(0)
///
/// func reset() {
/// // Reset it:
/// self.count.setValue(0)
/// }
/// ```
///
/// > Tip: Use ``withValue(_:)`` instead of ``setValue(_:)`` if the value being set is derived
/// > from the current value. That is, do this:
/// >
/// > ```swift
/// > self.count.withValue { $0 += 1 }
/// > ```
/// >
/// > ...and not this:
/// >
/// > ```swift
/// > self.count.setValue(self.count + 1)
/// > ```
/// >
/// > ``withValue(_:)`` isolates the entire transaction and avoids data races between reading and
/// > writing the value.
///
/// - Parameter newValue: The value to replace the current isolated value with.
func setValue(_ newValue: @autoclosure () throws -> Value) rethrows {
try self.lock.criticalRegion {
self._value = try newValue()
self.didSet?(self._value)
}
}
}
extension LockedValue where Value: Sendable {
var value: Value {
self.lock.criticalRegion {
self._value
}
}
/// Initializes lock-isolated state around a value.
///
/// - Parameter value: A value to isolate with a lock.
/// - Parameter didSet: A callback to invoke when the value changes.
convenience init(
_ value: @autoclosure @Sendable () throws -> Value,
didSet: (@Sendable (Value) -> Void)? = nil
) rethrows {
try self.init(value())
self.didSet = didSet
}
}
extension NSRecursiveLock {
@inlinable @discardableResult
func criticalRegion<R>(work: () throws -> R) rethrows -> R {
self.lock()
defer { self.unlock() }
return try work()
}
}
================================================
FILE: Sources/Mockable/Macro/MockableMacro.swift
================================================
//
// MockableMacro.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 20..
//
/// A peer macro that generates a mock implementation for the protocol it is attached to.
///
/// The generated implementation is named with a "Mock" prefix followed by the protocol name.
/// By default, the generated code is enclosed in an `#if MOCKING` condition, ensuring it is only accessible
/// in modules where the `MOCKING` compile-time condition is set.
///
/// Example usage:
///
/// ```swift
/// @Mockable
/// protocol UserService {
/// func get(id: UUID) -> User
/// func remove(id: UUID) throws
/// }
///
/// var mockUserService: MockUserService
///
/// func test() {
/// let error: UserError = .invalidId
/// let mockUser = User(id: UUID())
///
/// given(mockUserService)
/// .get(id: .any).willReturn(mockUser)
/// .remove(id: .any).willThrow(error)
///
/// try await loginService.login()
///
/// verify(mockUserService)
/// .get(id: .value(mockUser.id)).called(count: .once)
/// .remove(id: .any).called(count: .never)
/// }
/// ```
@attached(peer, names: prefixed(Mock))
public macro Mockable() = #externalMacro(module: "MockableMacro", type: "MockableMacro")
================================================
FILE: Sources/Mockable/Matcher/Matcher.swift
================================================
//
// Matcher.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 14..
//
import Foundation
/// A utility for defining matchers used in mock assertions.
public class Matcher {
// MARK: Public Types
/// A closure type representing a comparator function for elements of type `T`.
///
/// - Parameters:
/// - lhs: The left-hand side element for comparison.
/// - rhs: The right-hand side element for comparison.
/// - Returns: `true` if the elements match according to the comparison criteria; otherwise, `false`.
public typealias Comparator<T> = (T, T) -> Bool
/// A closure type representing a comparator function for elements of a sequence of type `T`.
///
/// - Parameters:
/// - lhs: The left-hand side element for comparison.
/// - rhs: The right-hand side element for comparison.
/// - Returns: `true` if the elements match according to the comparison criteria; otherwise, `false`.
public typealias ElementComparator<T: Sequence> = (T.Element, T.Element) -> Bool
// MARK: Private Types
private typealias MatcherType = (mirror: Mirror, comparator: Any)
// MARK: Private Properties
private let matchers = LockedValue<[MatcherType]>([])
#if swift(>=6)
nonisolated(unsafe) private static var `default` = Matcher()
#else
private static var `default` = Matcher()
#endif
// MARK: Init
private init() {
registerDefaultTypes()
registerCustomTypes()
}
// MARK: - Reset
/// Reset the default state of the matcher by removing all registered types.
public static func reset() {
`default` = Matcher()
}
// MARK: - Register
/// Registers comparator for given type **T**.
///
/// Comparator is a closure of `(T,T) -> Bool`.
///
/// When several comparators for same type are registered to common
/// **Matcher** instance - it will resolve the most receont one.
///
/// - Parameters:
/// - valueType: compared type
/// - match: comparator closure
public static func register<T>(_ valueType: T.Type, match: @escaping Comparator<T>) {
Self.default.register(valueType, match: match)
}
/// Registers comparator for type, like comparing Int.self to Int.self. These types of comparators always returns true. Register like: `Matcher.default.register(CustomType.Type.self)`
///
/// - Parameter valueType: Type.Type.self
public static func register<T>(_ valueType: T.Type.Type) {
Self.default.register(valueType)
}
/// Register default comparator for Equatable types. Required for generic mocks to work.
///
/// - Parameter valueType: Equatable type
public static func register<T>(_ valueType: T.Type) where T: Equatable {
Self.default.register(valueType)
}
// MARK: - Comparator
/// Returns comparator closure for given type (if any).
///
/// Comparator is a closure of `(T,T) -> Bool`.
///
/// When several comparators for same type are registered to common
/// **Matcher** instance - it will resolve the most receont one.
///
/// - Parameter valueType: compared type
/// - Returns: comparator closure
public static func comparator<T>(for valueType: T.Type) -> Comparator<T>? {
Self.default.comparator(for: valueType)
}
/// Default Equatable comparator, compares if elements are equal.
///
/// - Parameter valueType: Equatable type
/// - Returns: comparator closure
public static func comparator<T>(for valueType: T.Type) -> Comparator<T>? where T: Equatable {
Self.default.comparator(for: valueType)
}
/// Default Equatable Sequence comparator, compares count, and then for every element equal element.
///
/// - Parameter valueType: Equatable Sequence type
/// - Returns: comparator closure
public static func comparator<T>(for valueType: T.Type) -> Comparator<T>? where T: Equatable, T: Sequence {
Self.default.comparator(for: valueType)
}
/// Default Sequence comparator, compares count, and then depending on sequence type:
/// - for Arrays, elements will be compared element by element (verifying order as well)
/// - other Sequences would be treated as unordered, so every element has to have matching element
///
/// - Parameter valueType: Sequence type
/// - Returns: comparator closure
public static func comparator<T>(for valueType: T.Type) -> Comparator<T>? where T: Sequence {
Self.default.comparator(for: valueType)
}
}
// MARK: - Register
extension Matcher {
private func register<T>(_ valueType: T.Type, match: @escaping Comparator<T>) {
let mirror = Mirror(reflecting: valueType)
matchers.withValue { $0.append((mirror, match as Any)) }
}
private func register<T>(_ valueType: T.Type.Type) {
register(valueType, match: { _, _ in true })
}
private func register<T>(_ valueType: T.Type) where T: Equatable {
let mirror = Mirror(reflecting: valueType)
let comparator = comparator(for: T.self)
matchers.withValue { $0.append((mirror, comparator as Any)) }
}
}
// MARK: - Comparator
extension Matcher {
private func comparator<T>(for valueType: T.Type) -> Comparator<T>? {
let mirror = Mirror(reflecting: valueType)
return comparator(by: mirror) as? (T, T) -> Bool
}
private func comparator<T>(for valueType: T.Type) -> Comparator<T>? where T: Equatable {
{ $0 == $1 }
}
private func comparator<T>(for valueType: T.Type) -> Comparator<T>? where T: Equatable, T: Sequence {
{ $0 == $1 }
}
private func comparator<T>(for valueType: T.Type) -> Comparator<T>? where T: Sequence {
let mirror = Mirror(reflecting: valueType)
if let directComparator = comparator(by: mirror) as? Comparator<T> {
return directComparator
}
guard let elementComparator = comparator(for: T.Element.self) else {
return nil
}
return sequenceComparator(for: valueType, elementComparator: elementComparator)
}
}
// MARK: - Helpers
extension Matcher {
private func comparator(by mirror: Mirror) -> Any? {
let snapshot = matchers.withValue { $0 }
return snapshot.reversed().first { matcher -> Bool in
matcher.mirror.subjectType == mirror.subjectType
}?.comparator
}
private func sequenceComparator<T: Sequence>(
for valueType: T.Type,
elementComparator: @escaping ElementComparator<T>
) -> Comparator<T>? {
{ (left: T, right: T) -> Bool in
let left = Array(left)
let right = Array(right)
guard left.count == right.count else { return false }
if valueType is [T.Element].Type {
return self.orderedCompare(left: left, right: right, comparator: elementComparator)
} else {
return self.unorderedCompare(left: left, right: right, comparator: elementComparator)
}
}
}
private func orderedCompare<T>(left: [T], right: [T], comparator: Comparator<T>) -> Bool {
left.enumerated().allSatisfy { index, element in
comparator(element, right[index])
}
}
private func unorderedCompare<T>(left: [T], right: [T], comparator: Comparator<T>) -> Bool {
var buffer = right
for element in left {
let index = buffer.firstIndex { comparator(element, $0) }
guard let index else { return false }
buffer.remove(at: index)
}
return buffer.isEmpty
}
}
// MARK: - Defaults
extension Matcher {
private func registerCustomTypes() {
register(GenericValue.self) { left, right -> Bool in
left.comparator(left.value, right.value)
}
}
private func registerDefaultTypes() {
registerBasicTypes()
registerArrays()
registerMetaTypes()
}
private func registerBasicTypes() {
register(Bool.self)
register(String.self)
register(Float.self)
register(Double.self)
register(Character.self)
register(Int.self)
register(Int8.self)
register(Int16.self)
register(Int32.self)
register(Int64.self)
register(UInt.self)
register(UInt8.self)
register(UInt16.self)
register(UInt32.self)
register(UInt64.self)
register(Data.self)
register(UUID.self)
register(Bool?.self)
register(String?.self)
register(Float?.self)
register(Double?.self)
register(Character?.self)
register(Int?.self)
register(Int8?.self)
register(Int16?.self)
register(Int32?.self)
register(Int64?.self)
register(UInt?.self)
register(UInt8?.self)
register(UInt16?.self)
register(UInt32?.self)
register(UInt64?.self)
register(Data?.self)
register(UUID?.self)
}
private func registerArrays() {
register([Bool].self)
register([String].self)
register([Float].self)
register([Double].self)
register([Character].self)
register([Int].self)
register([Int8].self)
register([Int16].self)
register([Int32].self)
register([Int64].self)
register([UInt].self)
register([UInt8].self)
register([UInt16].self)
register([UInt32].self)
register([UInt64].self)
register([Data].self)
register([UUID].self)
register([Bool?].self)
register([String?].self)
register([Float?].self)
register([Double?].self)
register([Character?].self)
register([Int?].self)
register([Int8?].self)
register([Int16?].self)
register([Int32?].self)
register([Int64?].self)
register([UInt?].self)
register([UInt8?].self)
register([UInt16?].self)
register([UInt32?].self)
register([UInt64?].self)
register([Data?].self)
register([UUID?].self)
}
private func registerMetaTypes() {
register(Any.Type.self) { _, _ in true }
register(Bool.Type.self)
register(String.Type.self)
register(Float.Type.self)
register(Double.Type.self)
register(Character.Type.self)
register(Int.Type.self)
register(Int8.Type.self)
register(Int16.Type.self)
register(Int32.Type.self)
register(Int64.Type.self)
register(UInt.Type.self)
register(UInt8.Type.self)
register(UInt16.Type.self)
register(UInt32.Type.self)
register(UInt64.Type.self)
register(Data.Type.self)
register(Any?.Type.self) { _, _ in true }
register(Bool?.Type.self)
register(String?.Type.self)
register(Float?.Type.self)
register(Double?.Type.self)
register(Character?.Type.self)
register(Int?.Type.self)
register(Int8?.Type.self)
register(Int16?.Type.self)
register(Int32?.Type.self)
register(Int64?.Type.self)
register(UInt?.Type.self)
register(UInt8?.Type.self)
register(UInt16?.Type.self)
register(UInt32?.Type.self)
register(UInt64?.Type.self)
register(Data?.Type.self)
}
}
================================================
FILE: Sources/Mockable/Mocker/CaseIdentifiable.swift
================================================
//
// CaseIdentifiable.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 22..
//
/// A protocol for enumerations that can be identified by a unique identifier.
///
/// Enumerations conforming to `CaseIdentifiable` must provide a computed property `id`
/// that returns a unique identifier, typically derived from the type name.
public protocol CaseIdentifiable: Equatable, Hashable {
/// A computed property that returns a unique identifier for the case.
var id: String { get }
/// A computed property that returns a human readably name for member enum cases.
var name: String { get }
/// A computed property that returns a human readably description of member parameters.
var parameters: String { get }
}
extension CaseIdentifiable {
/// A default implementation of the `id` property, deriving the identifier from the case name.
///
/// The default implementation removes any additional information such as parameters
/// by splitting the type name at the opening parenthesis.
public var id: String {
String(String(describing: self).split(separator: "(")[0])
}
/// A default implementation of the `name` property, deriving a human readable name for member enum cases.
public var name: String {
guard let lastToken = id.split(separator: "_").last else { return id }
return String(lastToken)
}
/// A default implementation of the `parameters` property, deriving a human readable name for member parameters.
public var parameters: String {
let description = String(describing: self)
guard let index = description.firstIndex(of: "(") else { return "no parameters" }
return String(description[index...])
}
}
extension CaseIdentifiable {
/// Compares two `CaseIdentifiable` instances for equality based on their unique identifiers.
///
/// - Parameters:
/// - lhs: The left-hand side `CaseIdentifiable`.
/// - rhs: The right-hand side `CaseIdentifiable`.
/// - Returns: `true` if the instances have the same identifier; otherwise, `false`.
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
/// Hashes a `CaseIdentifiable` instance using its unique identifier.
///
/// - Parameter hasher: The hasher to use for combining the hash value.
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
================================================
FILE: Sources/Mockable/Mocker/Matchable.swift
================================================
//
// Matchable.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 14..
//
/// A protocol for types that can be used as matchers in mock assertions.
public protocol Matchable {
/// Determines if the receiver matches another instance of the same type according to custom criteria.
///
/// - Parameter other: The instance to compare against.
/// - Returns: `true` if the receiver matches the specified instance; otherwise, `false`.
func match(_ other: Self) -> Bool
}
================================================
FILE: Sources/Mockable/Mocker/MemberAction.swift
================================================
//
// MemberAction.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 14..
//
/// A class representing an action to be performed on a member.
///
/// `MemberAction` associates a member of type `Member` with a closure (`action`)
/// that can be executed when needed.
public class MemberAction<Member> {
/// The member associated with the action.
public let member: Member
/// The closure representing the action to be performed.
public let action: () -> Void
/// Initializes a new instance of `MemberAction`.
///
/// - Parameters:
/// - member: The member to associate with the action.
/// - action: The closure representing the action to be performed.
public init(member: Member, action: @escaping () -> Void) {
self.member = member
self.action = action
}
}
================================================
FILE: Sources/Mockable/Mocker/MemberReturn.swift
================================================
//
// MemberReturn.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 14..
//
/// A class representing the return value associated with a member.
///
/// `MemberReturn` associates a member of type `Member` with a `ReturnValue` object,
/// encapsulating the information about the expected return value or behavior.
public class MemberReturn<Member> {
/// The member associated with the return value.
public let member: Member
/// The `ReturnValue` object encapsulating information about the expected return value or behavior.
public let returnValue: ReturnValue
/// Initializes a new instance of `MemberReturn`.
///
/// - Parameters:
/// - member: The member to associate with the return value.
/// - returnValue: The `ReturnValue` object representing the expected return value or behavior.
public init(member: Member, returnValue: ReturnValue) {
self.member = member
self.returnValue = returnValue
}
}
================================================
FILE: Sources/Mockable/Mocker/Mocked.swift
================================================
//
// Mocked.swift
// Mocked
//
// Created by Kolos Foltanyi on 2024. 04. 03..
//
/// A protocol that represents auto-mocked types.
///
/// `Mocked` in combination with a `relaxedMocked` option of `MockerPolicy `can be used
/// to set an implicit return value for custom types:
///
/// ```swift
/// struct Car {
/// var name: String
/// var seats: Int
/// }
///
/// extension Car: Mocked {
/// static var mock: Car {
/// Car(name: "Mock Car", seats: 4)
/// }
///
/// // Defaults to [mock] but we can
/// // provide a custom array of cars:
/// static var mocks: [Car] {
/// [
/// Car(name: "Mock Car 1", seats: 4),
/// Car(name: "Mock Car 2", seats: 4)
/// ]
/// }
/// }
///
/// @Mockable
/// protocol CarService {
/// func getCar() -> Car
/// func getCars() -> [Car]
/// }
///
/// func testCarService() {
/// func test() {
/// let mock = MockCarService(policy: .relaxedMocked)
/// // Implictly mocked without a given registration:
/// let car = mock.getCar()
/// let cars = mock.getCars()
/// }
/// }
/// ```
public protocol Mocked {
/// A default mock return value to use when `.relaxedMocked` policy is set.
static var mock: Self { get }
/// An array of mock values to use as return values when `.relaxedMocked` policy is set.
/// Defaults to `[Self.mock]`.
static var mocks: [Self] { get }
}
extension Mocked {
public static var mocks: [Self] { [mock] }
}
extension Array: Mocked where Element: Mocked {
public static var mock: Self {
Element.mocks
}
}
extension Optional: Mocked where Wrapped: Mocked {
public static var mock: Self { Wrapped.mock }
}
================================================
FILE: Sources/Mockable/Mocker/Mocker.swift
================================================
//
// Mocker.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 14..
//
import Foundation
import IssueReporting
/// A class responsible for mocking and verifying interactions with a mockable service.
///
/// The `Mocker` class keeps track of invocations, expected return values, and actions associated with
/// specific members of a mockable service.
public class Mocker<Service: MockableService>: @unchecked Sendable {
// MARK: Public Properties
/// The associated type representing a member of the mockable service.
public typealias Member = Service.Member
/// The associated type representing the return value of a member.
public typealias Return = Service.Return
/// The associated type representing an action to be performed on a member.
public typealias Action = Service.Action
/// Custom relaxation policy to use when missing return values.
public var policy: MockerPolicy?
// MARK: Private Properties
/// Dictionary to store expected return values for each member.
private var returns = LockedValue<[Member: [Return]]>([:])
/// Dictionary to store actions to be performed on each member.
private var actions = LockedValue<[Member: [Action]]>([:])
/// Array to store invocations of members.
private lazy var invocations = LockedValue<[Member]>([]) { [invocationsSubject] newValue in
Task {
#if swift(>=6.0) && !swift(>=6.1)
invocationsSubject.send(newValue)
#else
await invocationsSubject.send(newValue)
#endif
}
}
/// Subject to track invocations.
private var invocationsSubject = AsyncSubject<[Member]>([])
/// Resolved relaxation policy to use when missing return values.
private var currentPolicy: MockerPolicy {
policy ?? .default
}
// MARK: Init
/// Initializes a new instance of `Mocker`.
public init(policy: MockerPolicy? = nil) {
self.policy = policy
}
// MARK: Public Methods
/// Adds an invocation for a member to the list of invocations.
///
/// - Parameter member: The member for which the invocation is added.
public func addInvocation(for member: Member) {
invocations.withValue { invocations in
invocations.append(member)
}
}
/// Specifies an expected return value for a member.
///
/// - Parameters:
/// - member: The member for which the return value is specified.
/// - returnValue: The expected return value.
public func addReturnValue(_ returnValue: ReturnValue, for member: Member) {
let given = Return(member: member, returnValue: returnValue)
returns.withValue { returns in
returns[member] = (returns[member] ?? []) + [given]
}
}
/// Specifies an action to be performed on a member.
///
/// - Parameters:
/// - member: The member for which the action is specified.
/// - action: The action to be performed.
public func addAction(_ action: @escaping () -> Void, for member: Member) {
let action = Action(member: member, action: action)
actions.withValue { actions in
actions[action.member] = (actions[action.member] ?? []) + [action]
}
}
/// Verifies the number of times a member has been called.
///
/// - Parameters:
/// - member: The member to verify.
/// - count: The expected number of invocations.
public func verify(member: Member,
count: Count,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column) {
let matches = invocations.value.filter(member.match)
let message = """
Expected \(count) invocation(s) of \(member.name), but was \(matches.count).",
"""
guard count.satisfies(matches.count) else {
reportIssue(message, fileID: fileID, filePath: filePath, line: line, column: column)
return
}
}
/// Verifies the number of times a member should be called.
///
/// - Parameters:
/// - member: The member to verify.
/// - count: The expected number of invocations.
/// - timeout: The maximum time it will wait for assertion to be true
public func verify(member: Member,
count: Count,
timeout: TimeoutDuration,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column) async {
do {
try await withTimeout(after: timeout.duration) { [invocationsSubject] in
for await invocations in invocationsSubject {
let matches = invocations.filter(member.match)
if count.satisfies(matches.count) {
break
} else {
continue
}
}
}
} catch {
let matches = invocations.value.filter(member.match)
let message = """
Expected \(count) invocation(s) of \(member.name) before \(timeout.duration) s, but was \(matches.count).
"""
guard count.satisfies(matches.count) else {
reportIssue(message, fileID: fileID, filePath: filePath, line: line, column: column)
return
}
}
}
/// Resets the state of the mocker.
///
/// - Parameter scopes: The set of scopes to reset (given, effect, verify).
public func reset(scopes: Set<MockerScope>) {
MockerScope.allCases.forEach { scope in
guard scopes.contains(scope) else { return }
switch scope {
case .given:
returns.setValue([:])
case .when:
actions.setValue([:])
case .verify:
invocations.setValue([])
}
}
}
/// Performs actions associated with a member.
///
/// - Parameter member: The member for which actions should be performed.
public func performActions(for member: Member) {
actions.withValue { actions in
guard let actions = actions[member] else { return }
let matches = actions.filter { member.match($0.member) }
matches.forEach { $0.action() }
}
}
/// Mocks a member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mock<V>(_ member: Member, producerResolver: (Any) throws -> V) -> V {
// swiftlint:disable:next force_try
return try! mock(member, producerResolver, .none)
}
/// Mocks a throwing member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mockThrowing<V>(_ member: Member, producerResolver: (Any) throws -> V) throws -> V {
return try mock(member, producerResolver, .none)
}
#if swift(>=6)
/// Mocks a throwing member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mockThrowing<V, E>(_ member: Member,
error: E.Type,
producerResolver: (Any) throws -> V) throws(E) -> V {
do {
return try mock(member, producerResolver, .none)
} catch {
throw error as! E // swiftlint:disable:this force_cast
}
}
#endif
}
// MARK: - Helpers
extension Mocker {
private func mock<V>(
_ member: Member,
_ producerResolver: (Any) throws -> V,
_ fallback: MockerFallback<V>
) throws -> V {
addInvocation(for: member)
performActions(for: member)
// swiftlint:disable:next closure_body_length
return try returns.withValue { returns in
let matchCount = returns[member]?
.filter { member.match($0.member) }
.count ?? 0
guard var candidates = returns[member], matchCount != 0 else {
if case .value(let value) = fallback {
return value
} else {
let message = notMockedMessage(member, value: V.self)
reportIssue(message)
fatalError(message)
}
}
for index in candidates.indices {
let match = candidates[index]
guard member.match(match.member) else { continue }
let removeMatch: (inout [Member: [Return]]) -> Void = { returns in
guard matchCount > 1 else { return }
candidates.remove(at: index)
returns[member] = candidates
}
switch match.returnValue {
case .return(let value):
guard let value = value as? V else { continue }
removeMatch(&returns)
return value
case .throw(let error):
removeMatch(&returns)
throw error
case .produce(let producer):
do {
let value = try producerResolver(producer)
removeMatch(&returns)
return value
} catch ProducerCastError.typeMismatch {
continue
} catch {
removeMatch(&returns)
throw error
}
}
}
let message = genericNotMockedMessage(member, value: V.self)
reportIssue(message)
fatalError(message)
}
}
}
// MARK: - Error Messages
extension Mocker {
private func notMockedMessage<V>(_ member: Member, value: V.Type) -> String {
"""
No return value found for member "\(member.name)" of "\(Service.self)" \
with parameter conditions: \(member.parameters) \
At least one return value of type "\(V.self)" must be provided \
matching the given parameters. Use a "given" clause to provide return values.
"""
}
private func genericNotMockedMessage<V>(_ member: Member, value: V.Type) -> String {
"""
No generic return value of type \(V.self) found for member "\(member.name)" of "\(Service.self)" \
with parameter conditions: \(member.parameters) \
At least one return value of type "\(V.self)" must be provided \
matching the given parameters. Use a "given" clause to provide return values.
"""
}
}
// MARK: - Void
extension Mocker {
/// Mocks a member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
public func mock(_ member: Member, producerResolver: (Any) throws -> Void) {
let relaxed = currentPolicy.contains(.relaxedNonThrowingVoid)
// swiftlint:disable:next force_try
return try! mock(member, producerResolver, relaxed ? .value(()) : .none)
}
/// Mocks a throwing member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
public func mockThrowing(_ member: Member, producerResolver: (Any) throws -> Void) throws {
let relaxed = currentPolicy.contains(.relaxedThrowingVoid)
return try mock(member, producerResolver, relaxed ? .value(()) : .none)
}
#if swift(>=6)
/// Mocks a throwing member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
public func mockThrowing<E>(_ member: Member,
error: E.Type,
producerResolver: (Any) throws -> Void) throws(E) {
do {
let relaxed = currentPolicy.contains(.relaxedThrowingVoid)
return try mock(member, producerResolver, relaxed ? .value(()) : .none)
} catch {
throw error as! E // swiftlint:disable:this force_cast
}
}
#endif
}
// MARK: - Optional
extension Mocker {
/// Mocks a member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mock<V>(
_ member: Member,
producerResolver: (Any) throws -> V
) -> V where V: ExpressibleByNilLiteral {
let relaxed = currentPolicy.contains(.relaxedOptional)
// swiftlint:disable:next force_try
return try! mock(member, producerResolver, relaxed ? .value(nil) : .none)
}
/// Mocks a throwing member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mockThrowing<V>(
_ member: Member,
producerResolver: (Any) throws -> V
) throws -> V where V: ExpressibleByNilLiteral {
let relaxed = currentPolicy.contains(.relaxedOptional)
return try mock(member, producerResolver, relaxed ? .value(nil) : .none)
}
#if swift(>=6)
/// Mocks a throwing member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mockThrowing<V, E>(
_ member: Member,
error: E.Type,
producerResolver: (Any) throws -> V
) throws(E) -> V where V: ExpressibleByNilLiteral {
do {
let relaxed = currentPolicy.contains(.relaxedThrowingVoid)
return try mock(member, producerResolver, relaxed ? .value(nil) : .none)
} catch {
throw error as! E // swiftlint:disable:this force_cast
}
}
#endif
}
// MARK: - Mockable
extension Mocker {
/// Mocks a member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mock<V>(
_ member: Member,
producerResolver: (Any) throws -> V
) -> V where V: Mocked {
let relaxed = currentPolicy.contains(.relaxedMocked)
// swiftlint:disable:next force_try
return try! mock(member, producerResolver, relaxed ? .value(.mock) : .none)
}
/// Mocks a throwing member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mockThrowing<V>(
_ member: Member,
producerResolver: (Any) throws -> V
) throws -> V where V: Mocked {
let relaxed = currentPolicy.contains(.relaxedMocked)
return try mock(member, producerResolver, relaxed ? .value(.mock) : .none)
}
#if swift(>=6)
/// Mocks a throwing member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mockThrowing<V, E>(
_ member: Member,
error: E,
producerResolver: (Any) throws -> V
) throws(E) -> V where V: Mocked {
do {
let relaxed = currentPolicy.contains(.relaxedMocked)
return try mock(member, producerResolver, relaxed ? .value(.mock) : .none)
} catch {
throw error as! E // swiftlint:disable:this force_cast
}
}
#endif
}
// MARK: - Mockable + Optional
extension Mocker {
/// Mocks a member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mock<V>(
_ member: Member,
producerResolver: (Any) throws -> V
) -> V where V: Mocked, V: ExpressibleByNilLiteral {
// swiftlint:disable force_try
if currentPolicy.contains(.relaxedMocked) {
return try! mock(member, producerResolver, .value(.mock))
} else if currentPolicy.contains(.relaxedOptional) {
return try! mock(member, producerResolver, .value(nil))
} else {
return try! mock(member, producerResolver, .none)
}
// swiftlint:enable force_try
}
/// Mocks a throwing member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mockThrowing<V>(
_ member: Member,
producerResolver: (Any) throws -> V
) throws -> V where V: Mocked, V: ExpressibleByNilLiteral {
if currentPolicy.contains(.relaxedMocked) {
return try mock(member, producerResolver, .value(.mock))
} else if currentPolicy.contains(.relaxedOptional) {
return try mock(member, producerResolver, .value(nil))
} else {
return try mock(member, producerResolver, .none)
}
}
#if swift(>=6)
/// Mocks a throwing member, performing associated actions and providing the expected return value.
///
/// - Parameters:
/// - member: The member to mock.
/// - producerResolver: A closure resolving the produced value.
/// - Returns: The expected return value.
@discardableResult
public func mockThrowing<V, E>(
_ member: Member,
error: E.Type,
producerResolver: (Any) throws -> V
) throws(E) -> V where V: Mocked, V: ExpressibleByNilLiteral {
do {
if currentPolicy.contains(.relaxedMocked) {
return try mock(member, producerResolver, .value(.mock))
} else if currentPolicy.contains(.relaxedOptional) {
return try mock(member, producerResolver, .value(nil))
} else {
return try mock(member, producerResolver, .none)
}
} catch {
throw error as! E // swiftlint:disable:this force_cast
}
}
#endif
}
================================================
FILE: Sources/Mockable/Mocker/MockerFallback.swift
================================================
//
// MockerFallback.swift
// Mockable
//
// Created by Kolos Foltanyi on 2024. 03. 17..
//
/// Describes an optional default value to use when no stored
/// return value found during mocking.
enum MockerFallback<V> {
/// Specifies a default value to be used when no stored return value is found.
case value(V)
/// Specifies that no default value should be used when no stored return value is found.
/// This results in a fatal error. This is the default behavior.
case none
}
================================================
FILE: Sources/Mockable/Mocker/MockerPolicy.swift
================================================
//
// MockerPolicy.swift
// Mockable
//
// Created by Kolos Foltanyi on 2024. 04. 01..
//
/// A policy that controls how the library handles when no return value is found during mocking.
///
/// MockerPolicy can be used to customize mocking behavior and disable the requirement of
/// return value registration in case of certain types.
public struct MockerPolicy: OptionSet, Sendable {
/// Default policy to use when none was explicitly specified for a mock.
///
/// Change this property to set the default policy to use for all mocks. Defaults to `strict`.
#if swift(>=6)
nonisolated(unsafe) public static var `default`: Self = .strict
#else
public static var `default`: Self = .strict
#endif
/// All return values must be registered, a fatal error will occur otherwise.
public static let strict: Self = []
/// Every literal expressible requirement will return a default value.
public static let relaxed: Self = [
.relaxedOptional,
.relaxedThrowingVoid,
.relaxedNonThrowingVoid,
.relaxedMocked
]
/// Every void function will run normally without a registration
public static let relaxedVoid: Self = [
.relaxedNonThrowingVoid,
.relaxedThrowingVoid
]
/// Throwing Void functions will run without return value registration.
public static let relaxedThrowingVoid = Self(rawValue: 1 << 0)
/// Non-throwing Void functions will run without return value registration.
public static let relaxedNonThrowingVoid = Self(rawValue: 1 << 1)
/// Optional return values will default to nil.
public static let relaxedOptional = Self(rawValue: 1 << 2)
/// Types conforming to the `Mocked` protocol will default to their mock value.
public static let relaxedMocked = Self(rawValue: 1 << 3)
/// Option set raw value.
public let rawValue: Int
/// Creates a new option set from the given raw value.
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
================================================
FILE: Sources/Mockable/Mocker/MockerScope.swift
================================================
//
// MockerScope.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 21..
//
/// An enumeration representing different scopes of the Mocker state.
///
/// Scopes can be used to only reset specific states of a mock service.
public enum MockerScope: CaseIterable {
/// The scope for storing expected return values.
case given
/// The scope for storing actions to be performed on members.
case when
/// The scope for storing invocations to be verified.
case verify
}
extension Set where Element == MockerScope {
/// A convenience property representing a set containing all available scopes.
public static var all: Self { Set(MockerScope.allCases) }
}
================================================
FILE: Sources/Mockable/Models/Count.swift
================================================
//
// Count.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 13..
//
/// An enumeration representing different counting conditions for verifying invocations.
///
/// Use `Count` in `verify` clauses to write assertions:
/// ```swift
/// // Assert `fetch(for)` was called between 1 and 5 times:
/// verify(productService)
/// .fetch(for: .any)
/// .called(.from(1, to: 5))
/// ```
public enum Count: ExpressibleByIntegerLiteral, Sendable {
/// The associated type for the integer literal.
public typealias IntegerLiteralType = Int
/// Initializes a new instance of `Count` using an integer literal.
///
/// - Parameter value: The integer literal value.
public init(integerLiteral value: IntegerLiteralType) {
self = .exactly(value)
}
/// The member was called at least once.
case atLeastOnce
/// The member was called exactly once.
case once
/// The member was called a specific number of times.
case exactly(Int)
/// The member was called within a specific range of times.
case from(Int, to: Int)
/// The member was called less than a specific number of times.
case less(than: Int)
/// The member was called less than or equal to a specific number of times.
case lessOrEqual(to: Int)
/// The member was called more than a specific number of times.
case more(than: Int)
/// The member was called more than or equal to a specific number of times.
case moreOrEqual(to: Int)
/// The member was never called.
case never
/// Checks if the given count satisfies the specified condition.
///
/// - Parameter count: The actual count to be compared.
/// - Returns: `true` if the condition is satisfied; otherwise, `false`.
func satisfies(_ count: Int) -> Bool {
switch self {
case .atLeastOnce: return count >= 1
case .once: return count == 1
case .exactly(let times): return count == times
case .from(let from, let to): return count >= from && count <= to
case .less(let than): return count < than
case .lessOrEqual(let to): return count <= to
case .more(let than): return count > than
case .moreOrEqual(let to): return count >= to
case .never: return count == 0
}
}
}
extension Count: CustomStringConvertible {
public var description: String {
switch self {
case .atLeastOnce:
return "at least 1"
case .once:
return "exactly one"
case .exactly(let value):
return "exactly \(value)"
case .from(let lowerBound, let upperBound):
return "from \(lowerBound) to \(upperBound)"
case .less(let value):
return "less than \(value)"
case .lessOrEqual(let value):
return "less than or equal to \(value)"
case .more(let value):
return "more than \(value)"
case .moreOrEqual(let value):
return "more than or equal to \(value)"
case .never:
return "none"
}
}
}
================================================
FILE: Sources/Mockable/Models/GenericValue.swift
================================================
//
// GenericAttribute.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 17..
//
/// A type erased wrapper for generic parameters.
///
/// `GenericValue` encapsulates an arbitrary generic value along with a custom comparator closure.
/// The comparator is used to determine equality between two instances of `GenericValue`.
public struct GenericValue {
/// The encapsulated value of type `Any`.
public let value: Any
/// The comparator closure used to determine equality between two instances of `GenericValue`.
public let comparator: (Any, Any) -> Bool
/// Initializes a new instance of `GenericValue`.
///
/// - Parameters:
/// - value: The value to be encapsulated.
/// - comparator: The closure used to determine equality between two instances of `GenericValue`.
public init(value: Any, comparator: @escaping (Any, Any) -> Bool) {
self.value = value
self.comparator = comparator
}
}
================================================
FILE: Sources/Mockable/Models/Parameter+Match.swift
================================================
//
// Parameter+Match.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 25..
//
import IssueReporting
extension Parameter {
private func match(_ parameter: Parameter<Value>, using comparator: Matcher.Comparator<Value>?) -> Bool {
switch (self, parameter) {
case (.any, _): return true
case (_, .any): return true
case (.value(let value), .matching(let matcher)): return matcher(value)
case (.matching(let matcher), .value(let value)): return matcher(value)
case (.value(let value1), .value(let value2)):
guard let comparator else {
reportIssue(noComparatorMessage)
fatalError(noComparatorMessage)
}
return comparator(value1, value2)
default: return false
}
}
}
// MARK: - Default
extension Parameter {
/// Matches the current parameter with another parameter.
///
/// - Parameter parameter: The parameter to match against.
/// - Returns: `true` if the parameters match; otherwise, `false`.
public func match(_ parameter: Parameter<Value>) -> Bool {
match(parameter, using: Matcher.comparator(for: Value.self))
}
}
// MARK: - Equatable
extension Parameter where Value: Equatable {
/// Matches the current parameter with another parameter.
///
/// - Parameter parameter: The parameter to match against.
/// - Returns: `true` if the parameters match; otherwise, `false`.
public func match(_ parameter: Parameter<Value>) -> Bool {
match(parameter, using: Matcher.comparator(for: Value.self))
}
}
// MARK: - Sequence
extension Parameter where Value: Sequence {
/// Matches the current parameter with another parameter.
///
/// - Parameter parameter: The parameter to match against.
/// - Returns: `true` if the parameters match; otherwise, `false`.
public func match(_ parameter: Parameter<Value>) -> Bool {
match(parameter, using: Matcher.comparator(for: Value.self))
}
}
// MARK: - Equatable Sequence
extension Parameter where Value: Equatable, Value: Sequence {
/// Matches the current parameter with another parameter.
///
/// - Parameter parameter: The parameter to match against.
/// - Returns: `true` if the parameters match; otherwise, `false`.
public func match(_ parameter: Parameter<Value>) -> Bool {
match(parameter, using: Matcher.comparator(for: Value.self))
}
}
================================================
FILE: Sources/Mockable/Models/Parameter.swift
================================================
//
// Parameter.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 13..
//
import IssueReporting
/// An enumeration representing different types of parameters used in mocking.
public enum Parameter<Value>: @unchecked Sendable {
/// Matches any value.
case any
/// Matches a specific value.
case value(Value)
/// Matches a value using a custom matching closure.
case matching((Value) -> Bool)
}
extension Parameter {
/// Creates a type erased parameter from a value of type `T`.
///
/// - Parameter value: The value to be encapsulated in a generic parameter.
/// - Returns: A a type erased parameter containing the provided value.
public static func generic<T>(_ value: T) -> Parameter<GenericValue> {
Parameter<T>.value(value).eraseToGenericValue()
}
/// Type erases a parameter of type `Parameter<T>` to a `Parameter<GenericValue>`
///
/// - Returns: A type erased parameter with the same matching behavior.
public func eraseToGenericValue() -> Parameter<GenericValue> {
switch self {
case .any:
return .any
case .value(let value):
let value = GenericValue(value: value) { left, right in
guard let left = left as? Value,
let right = right as? Value else { return false }
guard let comparator = Matcher.comparator(for: Value.self) else {
reportIssue(noComparatorMessage)
fatalError(noComparatorMessage)
}
return comparator(left, right)
}
return .value(value)
case .matching(let matcher):
return .matching { value in
guard let value = value.value as? Value else { return false }
return matcher(value)
}
}
}
}
extension Parameter {
var noComparatorMessage: String {
"""
No comparator found for type "\(Value.self)". \
All non-equatable types must be registered using Matcher.register(_).
"""
}
}
================================================
FILE: Sources/Mockable/Models/ReturnValue.swift
================================================
//
// ReturnValue.swift
// Mockable
//
// Created by Kolos Foltanyi on 2023. 11. 13..
//
/// An enumeration representing different types of return values for mocked functions.
///
/// - `return(Any)`: A concrete value to be returned.
/// - `throw(Error)`: An error to be thrown.
/// - `produce(Any)`: A value producer to be invoked.
public enum ReturnValue {
/// A case representing a concrete value to be returned.
case `return`(Any)
/// A case representing an error to be thrown.
case `throw`(any Error)
/// A case representing a value producer to be invoked.
case produce(Any)
}
/// An error thrown when type erased producers cannot be casted to their original closure type.
public enum ProducerCastError: Error {
case typeMismatch
}
/// Casts the given producer to the specified type.
///
/// This function is used to cast the producer to a specific type when using the `.produce` case
/// in the `ReturnValue` enum.
///
/// - Parameter producer: The producer to be cast.
/// - Returns: The casted producer of type `P`.
public func cast<P>(_ producer: Any) throws -> P {
guard let producer = producer as? P else {
throw ProducerCastError.typeMismatch
}
return producer
}
================================================
FILE: Sources/Mockable/Models/TimeoutDuration.swift
================================================
//
// TimeoutDuration.swift
// Mockable
//
//
// Created by Nayanda Haberty on 3/4/24.
//
import Foundation
/// An enumeration representing a duration of time.
public enum TimeoutDuration {
/// A TimeoutDuration representing a given number of seconds.
case seconds(Double)
/// A TimeoutDuration representing a given number of miliseconds.
case miliseconds(UInt)
/// Converts the duration to TimeInterval.
public var duration: TimeInterval {
switch self {
case .seconds(let value): value
case .miliseconds(let value): TimeInterval(value) / 1000
}
}
}
// MARK: - ExpressibleByFloatLiteral
extension TimeoutDuration: ExpressibleByFloatLiteral {
public init(floatLiteral value: Double) {
self = .seconds(value)
}
}
// MARK: - ExpressibleByIntegerLiteral
extension TimeoutDuration: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self = .seconds(Double(value))
}
}
================================================
FILE: Sources/Mockable/Utils/Utils.swift
================================================
//
// Utils.swift
// Mockable
//
// Created by Kolos Foltanyi on 2024. 03. 17..
//
/// Creates a proxy for building return values for members of the given service.
///
/// Example usage of `given(_ service:)`:
/// ```swift
/// // Throw an error for the first call and then return 'product' for every other call
/// given(productService)
/// .fetch(for: .any).willThrow(error)
/// .fetch(for: .any).willReturn(product)
///
/// // Throw an error if the id parameter ends with a 0, return a product otherwise
/// given(productService)
/// .fetch(for: .any).willProduce { id in
/// if id.uuidString.last == "0" {
/// throw error
/// } else {
/// return product
/// }
/// }
/// ```
///
/// - Parameter service: The mockable service for which return values are specified.
/// - Returns: The service's return value builder.
public func given<T: MockableService>(_ service: T) -> T.ReturnBuilder { service._given }
/// Creates a proxy for building actions for members of the given service.
///
/// Example usage of `when(_ service:)`:
/// ```swift
/// // log calls to fetch(for:)
/// when(productService).fetch(for: .any).perform {
/// print("fetch(for:) was called")
/// }
///
/// // log when url is accessed
/// when(productService).url().performOnGet {
/// print("url accessed")
/// }
///
/// // log when url is set to nil
/// when(productService).url(newValue: .value(nil)).performOnSet {
/// print("url set to nil")
/// }
/// ```
///
/// - Parameter service: The mockable service for which actions are specified.
/// - Returns: The service's action builder.
public func when<T: MockableService>(_ service: T) -> T.ActionBuilder { service._when }
/// Creates a proxy for verifying invocations of members of the given service.
///
/// Example usage of `verify(_ service:)`:
/// ```swift
/// verify(productService)
/// // assert fetch(for:) was called between 1 and 5 times
/// .fetch(for: .any).called(.from(1, to: 5))
/// // assert checkout(with:) was called between exactly 10 times
/// .checkout(with: .any).called(10)
/// // assert url property was accessed at least 2 times
/// .url().getCalled(.moreOrEqual(to: 2))
/// // assert url property was never set to nil
/// .url(newValue: .value(nil)).setCalled(.never)
/// ```
/// - Parameter service: The mockable service for which invocations are verified.
/// - Returns: The service's verification builder.
public func verify<T: MockableService>(_ service: T) -> T.VerifyBuilder { service._verify }
================================================
FILE: Sources/MockableMacro/Extensions/AttributeListSyntax+Extensions.swift
================================================
//
// File.swift
// Mockable
//
// Created by Kolos Foltányi on 2025. 11. 16..
//
import SwiftSyntax
extension AttributeListSyntax {
func filter(allowedNames: [String]) -> AttributeListSyntax {
filter { element in
switch element {
case .attribute(let attributeSyntax):
return attribute(attributeSyntax, matches: allowedNames)
case .ifConfigDecl(let ifConfigDeclSyntax):
return ifConfigDeclSyntax.clauses
.compactMap { $0.elements }
.allSatisfy { item in
guard case .attributes(let list) = item else { return false }
return list.allSatisfy {
guard case .attribute(let attr) = $0 else { return false }
return attribute(attr, matches: allowedNames)
}
}
}
}
}
private func attribute(_ attribute: AttributeSyntax, matches allowedNames: [String]) -> Bool {
allowedNames.contains(attribute.attributeName.trimmedDescription)
}
}
================================================
FILE: Sources/MockableMacro/Extensions/AttributeSyntax+Extensions.swift
================================================
//
// AttributeSyntax+Extensions.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 25..
//
import SwiftSyntax
extension AttributeSyntax {
static var escaping: Self {
AttributeSyntax(
atSign: .atSignToken(),
attributeName: IdentifierTypeSyntax(
name: .keyword(.escaping)
)
)
}
}
================================================
FILE: Sources/MockableMacro/Extensions/DeclModifierListSyntax+Extensions.swift
================================================
//
// DeclModifierListSyntax+Extensions.swift
// Mockable
//
// Created by Kolos Foltanyi on 27/09/2024.
//
import SwiftSyntax
extension DeclModifierListSyntax {
func filtered(keywords: Set<Keyword>) -> DeclModifierListSyntax {
filter { modifier in
guard case .keyword(let keyword) = modifier.name.tokenKind else { return false }
return keywords.contains(keyword)
}
}
func appending(_ other: DeclModifierListSyntax) -> DeclModifierListSyntax {
self + Array(other)
}
}
================================================
FILE: Sources/MockableMacro/Extensions/FunctionDeclSyntax+Extensions.swift
================================================
//
// FunctionDeclSyntax+Extensions.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 23..
//
import SwiftSyntax
extension FunctionDeclSyntax {
var isVoid: Bool {
signature.returnClause == nil
}
var isThrowing: Bool {
#if canImport(SwiftSyntax600)
signature.effectSpecifiers?.throwsClause?.throwsSpecifier.tokenKind == .keyword(.throws)
#else
signature.effectSpecifiers?.throwsSpecifier?.tokenKind == .keyword(.throws)
#endif
}
var closureType: FunctionTypeSyntax {
let params = signature.parameterClause.parameters
.map { $0.resolvedType(for: .parameter) }
#if canImport(SwiftSyntax600)
let effectSpecifiers = TypeEffectSpecifiersSyntax(
throwsClause: isThrowing ? .init(throwsSpecifier: .keyword(.throws)) : nil
)
#else
let effectSpecifiers = TypeEffectSpecifiersSyntax(
throwsSpecifier: isThrowing ? .keyword(.throws) : nil
)
#endif
return FunctionTypeSyntax(
parameters: TupleTypeElementListSyntax {
for param in params {
TupleTypeElementSyntax(type: param)
}
},
effectSpecifiers: effectSpecifiers,
returnClause: signature.returnClause ?? .init(type: IdentifierTypeSyntax(name: NS.Void))
)
}
func containsGenericType(in functionParameter: FunctionParameterSyntax) -> Bool {
let genericParameters = genericParameterClause?.parameters ?? []
guard !genericParameters.isEmpty else { return false }
let generics = genericParameters.map(\.name.trimmedDescription)
return nil != TokenFinder.find(in: functionParameter.type) {
guard case .identifier(let name) = $0.tokenKind else { return false }
return generics.contains(name)
}
}
}
#if canImport(SwiftSyntax600)
extension FunctionDeclSyntax {
var errorType: TypeSyntax? {
signature.effectSpecifiers?.throwsClause?.type
}
}
#endif
================================================
FILE: Sources/MockableMacro/Extensions/FunctionParameterSyntax+Extensions.swift
================================================
//
// FunctionParameterSyntax+Extensions.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2023. 11. 20..
//
import SwiftSyntax
import SwiftSyntaxMacros
enum FunctionParameterTypeResolveRole {
/// Resolves a type that can be used for property type bindings, removes attributes.
case binding
/// Resolves a type that can be used as a function or closure parameter, keeps attributes.
case parameter
}
extension FunctionParameterSyntax {
func resolvedType(for role: FunctionParameterTypeResolveRole = .binding) -> TypeSyntax {
guard ellipsis == nil else {
return TypeSyntax(ArrayTypeSyntax(element: type))
}
guard role != .parameter else { return type }
if let attributeType = type.as(AttributedTypeSyntax.self) {
return TypeSyntax(attributeType.baseType)
} else {
return TypeSyntax(type)
}
}
var isInout: Bool {
#if canImport(SwiftSyntax600)
type.as(AttributedTypeSyntax.self)?.specifiers.contains { specifier in
guard case .simpleTypeSpecifier(let simpleSpecifier) = specifier else { return false }
return simpleSpecifier.specifier.tokenKind == .keyword(.inout)
} ?? false
#else
type.as(AttributedTypeSyntax.self)?.specifier?.tokenKind == .keyword(.inout)
#endif
}
}
================================================
FILE: Sources/MockableMacro/Extensions/GenericArgumentSyntax+Extensions.swift
================================================
//
// GenericArgumentSyntax+Extensions.swift
// Mockable
//
// Created by Scott Hoyt on 10/06/2025.
//
import SwiftSyntax
#if canImport(SwiftSyntax601)
// The purpose of this initializer is to silence deprecation warnings by the using the new API when
// built against SwiftSyntax >= 601.0.0
extension GenericArgumentSyntax {
init(argument: any TypeSyntaxProtocol) {
self.init(argument: Argument(argument))
}
}
#endif
================================================
FILE: Sources/MockableMacro/Extensions/ProtocolDeclSyntax+Extensions.swift
================================================
//
// ProtocolDeclSyntax+Extensions.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 23..
//
import SwiftSyntax
extension ProtocolDeclSyntax {
var mockName: String {
NS.Mock.trimmedDescription + name.trimmedDescription
}
var mockType: IdentifierTypeSyntax {
IdentifierTypeSyntax(name: .identifier(mockName))
}
}
================================================
FILE: Sources/MockableMacro/Extensions/String+Extensions.swift
================================================
//
// String+Extensions.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2023. 11. 19..
//
extension String {
var capitalizedFirstLetter: String {
let firstLetter = prefix(1).capitalized
let remainingLetters = dropFirst()
return firstLetter + remainingLetters
}
}
================================================
FILE: Sources/MockableMacro/Extensions/TokenSyntax+Extensions.swift
================================================
//
// TokenSyntax+Extensions.swift
// MockableMacro
//
// Created by Kolos Foltányi on 2025. 05. 06..
//
import SwiftSyntax
extension TokenSyntax {
public var declNameOrVarCallName: Self {
let text = trimmed.description
if text.hasPrefix("`") && text.hasSuffix("`") {
return self
}
if Keyword.all.contains(text) {
return "`\(raw: text)`"
} else {
return self
}
}
}
extension Keyword {
fileprivate static let all: [String] = [
"__consuming",
"__owned",
"__setter_access",
"__shared",
"_alignment",
"_backDeploy",
"_borrow",
"_borrowing",
"_BridgeObject",
"_cdecl",
"_Class",
"_compilerInitialized",
"_const",
"_consuming",
"_documentation",
"_dynamicReplacement",
"_effects",
"_expose",
"_forward",
"_implements",
"_linear",
"_local",
"_modify",
"_move",
"_mutating",
"_NativeClass",
"_NativeRefCountedObject",
"_noMetadata",
"_nonSendable",
"_objcImplementation",
"_objcRuntimeName",
"_opaqueReturnTypeOf",
"_optimize",
"_originallyDefinedIn",
"_PackageDescription",
"_private",
"_projectedValueProperty",
"_read",
"_RefCountedObject",
"_semantics",
"_specialize",
"_spi",
"_spi_available",
"_swift_native_objc_runtime_base",
"_Trivial",
"_TrivialAtMost",
"_TrivialStride",
"_typeEraser",
"_unavailableFromAsync",
"_underlyingVersion",
"_UnknownLayout",
"_version",
"accesses",
"actor",
"addressWithNativeOwner",
"addressWithOwner",
"any",
"Any",
"as",
"assignment",
"associatedtype",
"associativity",
"async",
"attached",
"autoclosure",
"availability",
"available",
"await",
"backDeployed",
"before",
"block",
"borrowing",
"break",
"canImport",
"case",
"catch",
"class",
"compiler",
"consume",
"copy",
"consuming",
"continue",
"convenience",
"convention",
"cType",
"default",
"defer",
"deinit",
"dependsOn",
"deprecated",
"derivative",
"didSet",
"differentiable",
"distributed",
"do",
"dynamic",
"each",
"else",
"enum",
"escaping",
"exclusivity",
"exported",
"extension",
"fallthrough",
"false",
"file",
"fileprivate",
"final",
"for",
"discard",
"forward",
"func",
"freestanding",
"get",
"guard",
"higherThan",
"if",
"import",
"in",
"indirect",
"infix",
"init",
"initializes",
"inline",
"inout",
"internal",
"introduced",
"is",
"isolated",
"kind",
"lazy",
"left",
"let",
"line",
"linear",
"lowerThan",
"macro",
"message",
"metadata",
"module",
"mutableAddressWithNativeOwner",
"mutableAddressWithOwner",
"mutating",
"nil",
"noasync",
"noDerivative",
"noescape",
"none",
"nonisolated",
"nonmutating",
"objc",
"obsoleted",
"of",
"open",
"operator",
"optional",
"override",
"package",
"postfix",
"precedencegroup",
"preconcurrency",
"prefix",
"private",
"Protocol",
"protocol",
"public",
"reasync",
"renamed",
"repeat",
"required",
"_resultDependsOn",
"_resultDependsOnSelf",
"rethrows",
"retroactive",
"return",
"reverse",
"right",
"safe",
"scoped",
"self",
"sending",
"Self",
"Sendable",
"set",
"some",
"sourceFile",
"spi",
"spiModule",
"static",
"struct",
"subscript",
"super",
"swift",
"switch",
"target",
"then",
"throw",
"throws",
"transpose",
"true",
"try",
"Type",
"typealias",
"unavailable",
"unchecked",
"unowned",
"unsafe",
"unsafeAddress",
"unsafeMutableAddress",
"var",
"visibility",
"weak",
"where",
"while",
"willSet",
"witness_method",
"wrt",
"yield"
]
}
================================================
FILE: Sources/MockableMacro/Extensions/VariableDeclSyntax+Extensions.swift
================================================
//
// VariableDeclSyntax+Extensions.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 10..
//
import SwiftSyntax
extension VariableDeclSyntax {
var name: TokenSyntax {
get throws {
.identifier(try binding.pattern.trimmedDescription)
}
}
var isComputed: Bool { setAccessor == nil }
var isThrowing: Bool {
get throws {
#if canImport(SwiftSyntax600)
try getAccessor.effectSpecifiers?.throwsClause?.throwsSpecifier != nil
#else
try getAccessor.effectSpecifiers?.throwsSpecifier != nil
#endif
}
}
var resolvedType: TypeSyntax {
get throws {
let type = try type
if let type = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
return type.wrappedType
}
return type
}
}
var type: TypeSyntax {
get throws {
guard let typeAnnotation = try binding.typeAnnotation else {
throw MockableMacroError.invalidVariableRequirement
}
return typeAnnotation.type.trimmed
}
}
var getAccessor: AccessorDeclSyntax {
get throws {
let getAccessor = try accessors.first { $0.accessorSpecifier.tokenKind == .keyword(.get) }
guard let getAccessor else { throw MockableMacroError.invalidVariableRequirement }
return getAccessor
}
}
var setAccessor: AccessorDeclSyntax? {
try? accessors.first { $0.accessorSpecifier.tokenKind == .keyword(.set) }
}
var closureType: FunctionTypeSyntax {
get throws {
#if canImport(SwiftSyntax600)
let effectSpecifiers = TypeEffectSpecifiersSyntax(
throwsClause: try isThrowing ? .init(throwsSpecifier: .keyword(.throws)) : nil
)
#else
let effectSpecifiers = TypeEffectSpecifiersSyntax(
throwsSpecifier: try isThrowing ? .keyword(.throws) : nil
)
#endif
return FunctionTypeSyntax(
parameters: TupleTypeElementListSyntax(),
effectSpecifiers: effectSpecifiers,
returnClause: .init(type: try resolvedType)
)
}
}
var binding: PatternBindingSyntax {
get throws {
guard let binding = bindings.first else {
throw MockableMacroError.invalidVariableRequirement
}
return binding
}
}
}
// MARK: - Helpers
extension VariableDeclSyntax {
private var accessors: AccessorDeclListSyntax {
get throws {
guard let accessorBlock = try binding.accessorBlock,
case .accessors(let accessorList) = accessorBlock.accessors else {
throw MockableMacroError.invalidVariableRequirement
}
return accessorList
}
}
}
#if canImport(SwiftSyntax600)
extension VariableDeclSyntax {
var errorType: TypeSyntax? {
get throws { try getAccessor.effectSpecifiers?.throwsClause?.type }
}
}
#endif
================================================
FILE: Sources/MockableMacro/Factory/Buildable/Buildable.swift
================================================
//
// Buildable.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 23..
//
import SwiftSyntax
/// An enum representing the different builder structs to generate.
enum BuilderKind: CaseIterable {
case `return`
case action
case verify
/// The builder struct declaration's name.
var name: TokenSyntax {
switch self {
case .return: NS.ReturnBuilder
case .action: NS.ActionBuilder
case .verify: NS.VerifyBuilder
}
}
/// The builder struct declaration's type.
var type: IdentifierTypeSyntax {
IdentifierTypeSyntax(name: name)
}
}
/// A protocol to associate builder functions with individual requirements.
///
/// Used to generate members of builder struct declarations in builder factory.
protocol Buildable {
/// Returns the specified builder implementation.
///
/// - Parameter kind: The kind of builder function to generate.
/// - Parameter modifiers: Declaration modifiers to add to the builder.
/// - Parameter mockType: The enclosing mock service's type.
/// - Returns: A function or variable declaration that mirrors a protocol requirement
/// with its syntax. The parameters of the generated declaration are wrapped in the `Parameter`
/// wrapper, and it returns the corresponding builder object.
func builder(
of kind: BuilderKind,
with modifiers: DeclModifierListSyntax,
using mockType: IdentifierTypeSyntax
) throws -> DeclSyntax
}
================================================
FILE: Sources/MockableMacro/Factory/Buildable/Function+Buildable.swift
================================================
//
// Function+Buildable.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 23..
//
import SwiftSyntax
// MARK: - FunctionRequirement + Buildable
extension FunctionRequirement: Buildable {
func builder(
of kind: BuilderKind,
with modifiers: DeclModifierListSyntax,
using mockType: IdentifierTypeSyntax
) throws -> DeclSyntax {
let decl = FunctionDeclSyntax(
attributes: syntax.attributes
.filter(allowedNames: [NS.available.text])
.trimmed.with(\.trailingTrivia, .newline),
modifiers: DeclModifierListSyntax {
modifiers
DeclModifierSyntax(name: .keyword(.nonisolated))
},
name: syntax.name.trimmed,
genericParameterClause: genericParameterClause(for: kind),
signature: signature(for: kind, using: mockType),
genericWhereClause: genericWhereClause(for: kind),
body: try body(for: kind)
)
return DeclSyntax(decl)
}
}
// MARK: - Helpers
extension FunctionRequirement {
private func genericParameterClause(for kind: BuilderKind) -> GenericParameterClauseSyntax? {
switch kind {
case .return: syntax.genericParameterClause
case .action: filteredGenericParameterClause
case .verify: filteredGenericParameterClause
}
}
private func genericWhereClause(for kind: BuilderKind) -> GenericWhereClauseSyntax? {
switch kind {
case .return: syntax.genericWhereClause
case .action: filteredGenericWhereClause
case .verify: filteredGenericWhereClause
}
}
private func signature(
for kind: BuilderKind,
using mockType: IdentifierTypeSyntax
) -> FunctionSignatureSyntax {
FunctionSignatureSyntax(
parameterClause: parameterClause,
returnClause: returnClause(for: kind, using: mockType)
)
}
private var parameterClause: FunctionParameterClauseSyntax {
let parameters = FunctionParameterListSyntax {
for parameter in syntax.signature.parameterClause.parameters {
FunctionParameterSyntax(
firstName: parameter.firstName,
secondName: parameter.secondName,
type: IdentifierTypeSyntax(name: NS.Parameter(parameter.resolvedType().description))
)
}
}
return FunctionParameterClauseSyntax(parameters: parameters)
}
private func returnClause(
for kind: BuilderKind,
using mockType: IdentifierTypeSyntax
) -> ReturnClauseSyntax {
let name = syntax.isThrowing ? NS.ThrowingFunction(kind) : NS.Function(kind)
let arguments = GenericArgumentListSyntax {
GenericArgumentSyntax(argument: mockType)
GenericArgumentSyntax(argument: kind.type)
if let returnType = functionReturnType(for: kind) {
returnType
}
if let errorType = errorType(for: kind) {
errorType
}
if let produceType = functionProduceType(for: kind) {
produceType
}
}
return ReturnClauseSyntax(
type: MemberTypeSyntax(
baseType: IdentifierTypeSyntax(name: NS.Mockable),
name: name,
genericArgumentClause: .init(arguments: arguments)
)
)
}
private func errorType(for kind: BuilderKind) -> GenericArgumentSyntax? {
guard syntax.isThrowing && kind == .return else { return nil }
#if canImport(SwiftSyntax600)
guard let errorType = syntax.errorType else {
return GenericArgumentSyntax(argument: defaultErrorType)
}
return GenericArgumentSyntax(argument: errorType.trimmed)
#else
return GenericArgumentSyntax(argument: defaultErrorType)
#endif
}
private var defaultErrorType: some TypeSyntaxProtocol {
SomeOrAnyTypeSyntax(
someOrAnySpecifier: .keyword(.any),
constraint: MemberTypeSyntax(
baseType: IdentifierTypeSyntax(name: NS.Swift),
name: NS.Error
)
)
}
private func functionReturnType(for kind: BuilderKind) -> GenericArgumentSyntax? {
guard kind == .return else { return nil }
guard let returnClause = syntax.signature.returnClause else {
let voidType = IdentifierTypeSyntax(name: NS.Void)
return GenericArgumentSyntax(argument: voidType)
}
return GenericArgumentSyntax(argument: returnClause.type)
}
private func functionProduceType(for kind: BuilderKind) -> GenericArgumentSyntax? {
guard kind == .return else { return nil }
return GenericArgumentSyntax(argument: syntax.closureType)
}
private func body(for kind: BuilderKind) throws -> CodeBlockSyntax {
let arguments = try LabeledExprListSyntax {
LabeledExprSyntax(
expression: DeclReferenceExprSyntax(baseName: NS.mocker)
)
LabeledExprSyntax(
label: NS.kind,
colon: .colonToken(),
expression: try caseSpecifier(wrapParams: false)
)
}
let statements = CodeBlockItemListSyntax {
FunctionCallExprSyntax(
calledExpression: MemberAccessExprSyntax(name: NS.initializer),
leftParen: .leftParenToken(),
arguments: arguments,
rightParen: .rightParenToken()
)
}
return .init(statements: statements)
}
/// Returns the function's generic parameter clause with return only generics filtered out.
/// If a generic parameter is only used in the return clause of a function, it will not
/// be part of the returned generic parameter clause.
private var filteredGenericParameterClause: GenericParameterClauseSyntax? {
guard let generics = syntax.genericParameterClause else { return nil }
var parameters = generics.parameters.filter { generic in
hasParameter(containing: generic.name.trimmedDescription)
}
if let lastIndex = parameters.indices.last {
parameters[lastIndex] = parameters[lastIndex].with(\.trailingComma, nil)
}
return parameters.isEmpty ? nil : .init(parameters: parameters)
}
/// Returns the function's generic where clause with return only generics filtered out.
/// If a generic parameter is only used in the return clause of a function, it will not
/// be part of the returned generic where clause.
private var filteredGenericWhereClause: GenericWhereClauseSyntax? {
guard let generics = syntax.genericWhereClause else { return nil }
var requirements = generics.requirements.filter { requirement in
switch requirement.requirement {
case .conformanceRequirement(let conformance):
return hasParameter(containing: conformance.leftType.trimmedDescription)
case .sameTypeRequirement(let sameType):
return hasParameter(containing: sameType.leftType.trimmedDescription)
default:
return false
}
}
if let lastIndex = requirements.indices.last {
var last = requirements[lastIndex]
last.trailingComma = nil
requirements[lastIndex] = last
}
let whereClause = GenericWhereClauseSyntax(requirements: requirements)
.with(\.trailingTrivia, .space)
return requirements.isEmpty ? nil : whereClause
}
private func hasParameter(containing identifier: String) -> Bool {
nil != TokenFinder.find(in: syntax.signature.parameterClause) {
$0.tokenKind == .identifier(identifier)
}
}
}
================================================
FILE: Sources/MockableMacro/Factory/Buildable/Variable+Buildable.swift
================================================
//
// Variable+Buildable.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 23..
//
import SwiftSyntax
// MARK: - VariableRequirement + Buildable
extension VariableRequirement: Buildable {
func builder(
of kind: BuilderKind,
with modifiers: DeclModifierListSyntax,
using mockType: IdentifierTypeSyntax
) throws -> DeclSyntax {
if syntax.isComputed || kind == .return {
return try variableDeclaration(of: kind, with: modifiers, using: mockType)
} else {
return try functionDeclaration(of: kind, with: modifiers, using: mockType)
}
}
}
// MARK: - Helpers
extension VariableRequirement {
func variableDeclaration(
of kind: BuilderKind,
with modifiers: DeclModifierListSyntax,
using mockType: IdentifierTypeSyntax
) throws -> DeclSyntax {
let variableDecl = VariableDeclSyntax(
attributes: syntax.attributes
.filter(allowedNames: [NS.available.text])
.trimmed.with(\.trailingTrivia, .newline),
modifiers: DeclModifierListSyntax {
modifiers
DeclModifierSyntax(name: .keyword(.nonisolated))
},
bindingSpecifier: .keyword(.var),
bindings: try PatternBindingListSyntax {
PatternBindingSyntax(
pattern: try syntax.binding.pattern,
typeAnnotation: TypeAnnotationSyntax(
type: try returnType(for: kind, using: mockType)
),
accessorBlock: AccessorBlockSyntax(
accessors: .getter(try body(for: kind))
)
)
}
)
return DeclSyntax(variableDecl)
}
private func functionDeclaration(
of kind: BuilderKind,
with modifiers: DeclModifierListSyntax,
using mockType: IdentifierTypeSyntax
) throws -> DeclSyntax {
let functionDecl = FunctionDeclSyntax(
attributes: syntax.attributes.trimmed.with(\.trailingTrivia, .newline),
modifiers: modifiers,
name: try syntax.name,
signature: try signature(for: kind, using: mockType),
body: .init(statements: try body(for: kind))
)
return DeclSyntax(functionDecl)
}
private func signature(
for kind: BuilderKind,
using mockType: IdentifierTypeSyntax
) throws -> FunctionSignatureSyntax {
let parameters = try FunctionParameterListSyntax {
FunctionParameterSyntax(
firstName: NS.newValue,
type: IdentifierTypeSyntax(
name: NS.Parameter(try syntax.resolvedType.trimmedDescription)
),
defaultValue: InitializerClauseSyntax(
value: MemberAccessExprSyntax(name: NS.any)
)
)
}
return FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax(parameters: parameters),
returnClause: ReturnClauseSyntax(
type: try returnType(for: kind, using: mockType)
)
)
}
private func returnType(
for kind: BuilderKind,
using mockType: IdentifierTypeSyntax
) throws -> MemberTypeSyntax {
let name = if syntax.isComputed {
try syntax.isThrowing ? NS.ThrowingFunction(kind) : NS.Function(kind)
} else {
try syntax.isThrowing ? NS.ThrowingProperty(kind) : NS.Property(kind)
}
let arguments = try GenericArgumentListSyntax {
GenericArgumentSyntax(argument: mockType)
GenericArgumentSyntax(argument: kind.type)
if let returnType = try variableReturnType(for: kind) {
returnType
}
if let errorType = try errorType(for: kind) {
errorType
}
if let produceType = try variableProduceType(for: kind) {
produceType
}
}
return MemberTypeSyntax(
baseType: IdentifierTypeSyntax(name: NS.Mockable),
name: name,
genericArgumentClause: .init(arguments: arguments)
)
}
private func errorType(for kind: BuilderKind) throws -> GenericArgumentSyntax? {
guard try syntax.isThrowing && syntax.isComputed && kind == .return else { return nil }
#if canImport(SwiftSyntax600)
guard let errorType = try syntax.errorType else {
return GenericArgumentSyntax(argument: defaultErrorType)
}
return GenericArgumentSyntax(argument: errorType.trimmed)
#else
return GenericArgumentSyntax(argument: defaultErrorType)
#endif
}
private var defaultErrorType: some TypeSyntaxProtocol {
SomeOrAnyTypeSyntax(
someOrAnySpecifier: .keyword(.any),
constraint: IdentifierTypeSyntax(name: NS.Error)
)
}
private func variableReturnType(for kind: BuilderKind) throws -> GenericArgumentSyntax? {
guard kind == .return else { return nil }
return GenericArgumentSyntax(argument: try syntax.resolvedType)
}
private func variableProduceType(for kind: BuilderKind) throws -> GenericArgumentSyntax? {
guard kind == .return, syntax.isComputed else { return nil }
return GenericArgumentSyntax(argument: try syntax.closureType)
}
private func body(for kind: BuilderKind) throws -> CodeBlockItemListSyntax {
let arguments = try LabeledExprListSyntax {
LabeledExprSyntax(
expression: DeclReferenceExprSyntax(baseName: NS.mocker)
)
LabeledExprSyntax(
label: NS.kind,
colon: .colonToken(),
expression: try caseSpecifier(wrapParams: false)
)
if kind != .return, let setterSpecifier = try setterCaseSpecifier(wrapParams: false) {
LabeledExprSyntax(
label: NS.setKind,
colon: .colonToken(),
expression: setterSpecifier
)
}
}
return CodeBlockItemListSyntax {
FunctionCallExprSyntax(
calledExpression: MemberAccessExprSyntax(name: NS._init),
leftParen: .leftParenToken(),
arguments: arguments,
rightParen: .rightParenToken()
)
}
}
}
================================================
FILE: Sources/MockableMacro/Factory/BuilderFactory.swift
================================================
//
// BuilderFactory.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 23..
//
import SwiftSyntax
/// Factory to generate builder struct declarations.
///
/// Creates a member block item list that includes `ReturnBuilder`,
/// `ActionBuilder` and `VerifyBuilder` struct declarations.
enum BuilderFactory: Factory {
static func build(from requirements: Requirements) throws -> MemberBlockItemListSyntax {
try MemberBlockItemListSyntax {
for builder in BuilderKind.allCases {
try builderDeclaration(for: builder, requirements)
}
}
}
}
// MARK: - Helpers
extension BuilderFactory {
private static func builderDeclaration(
for kind: BuilderKind,
_ requirements: Requirements
) throws -> some DeclSyntaxProtocol {
StructDeclSyntax(
modifiers: requirements.modifiers,
name: kind.name,
inheritanceClause: InheritanceClauseSyntax(
inheritedTypes: [
InheritedTypeSyntax(
type: MemberTypeSyntax(
baseType: IdentifierTypeSyntax(name: NS.Mockable),
name: NS.Builder
)
)
]
),
memberBlock: MemberBlockSyntax(members: try members(kind, requirements))
)
}
private static func members(_ kind: BuilderKind, _ requirements: Requirements) throws -> MemberBlockItemListSyntax {
try MemberBlockItemListSyntax {
mockerDeclaration(requirements)
initializerDeclaration(kind, requirements)
for variable in requirements.variables {
MemberBlockItemSyntax(
decl: try variable.builder(
of: kind,
with: requirements.modifiers,
using: requirements.syntax.mockType
)
)
}
for function in requirements.functions {
MemberBlockItemSyntax(
decl: try function.builder(
of: kind,
with: requirements.modifiers,
using: requirements.syntax.mockType
)
)
}
}
}
private static func mockerDeclaration(_ requirements: Requirements) -> VariableDeclSyntax {
VariableDeclSyntax(
modifiers: [DeclModifierSyntax(name: .keyword(.private))],
bindingSpecifier: .keyword(.let),
bindingsBuilder: {
PatternBindingSyntax(
pattern: IdentifierPatternSyntax(identifier: NS.mocker),
typeAnnotation: TypeAnnotationSyntax(type: IdentifierTypeSyntax(name: NS.Mocker))
)
}
)
}
private static func initializerDeclaration(
_ kind: BuilderKind,
_ requirements: Requirements
) -> InitializerDeclSyntax {
InitializerDeclSyntax(
modifiers: DeclModifierListSyntax {
requirements.modifiers
DeclModifierSyntax(name: .keyword(.nonisolated))
},
signature: initializerSignature(kind, requirements)
) {
InfixOperatorExprSyntax(
leftOperand: MemberAccessExprSyntax(
base: DeclReferenceExprSyntax(baseName: .keyword(.self)),
name: NS.mocker
),
operator: AssignmentExprSyntax(),
rightOperand: DeclReferenceExprSyntax(baseName: NS.mocker)
)
}
}
private static func initializerSignature(
_ kind: BuilderKind,
_ requirements: Requirements
) -> FunctionSignatureSyntax {
FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax {
FunctionParameterSyntax(
firstName: NS.mocker,
type: IdentifierTypeSyntax(name: NS.Mocker)
)
}
)
}
}
================================================
FILE: Sources/MockableMacro/Factory/Caseable/Caseable.swift
================================================
//
// Caseable.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2023. 12. 07..
//
import SwiftSyntax
/// Describes requirements that are representable by a `Member` enum case.
///
/// Requirements conforming to `Caseable` provide their case declarations and a
/// utility that helps creating member access expressions for a given case.
protocol Caseable {
/// Returns the member enum represenatation of a syntax.
///
/// For example a member like:
/// ```
/// func foo(param: String) -> Int
/// ```
/// would return the following case declaration:
/// ```
/// case foo(param: Parameter<String>)
/// ```
var caseDeclarations: [EnumCaseDeclSyntax] { get throws }
/// Returns the initializer block that creates the enum case representation.
///
/// For example a member like:
/// ```
/// func foo(param: String) -> Int
/// ```
/// would return the following initializer declaration if wrapParams is true:
/// ```
/// .m1_foo(param: .value(param))
/// ```
/// and:
/// ```
/// .m1_foo(param: param)
/// ```
/// if wrapParams is false.
func caseSpecifier(wrapParams: Bool) throws -> ExprSyntax
/// Returns the initializer block that creates the enum case representation.
///
/// For example a member like:
/// ```
/// var prop: Int { get set }
/// ```
/// would return the following initializer declaration if wrapParams is true:
/// ```
/// .m1_set_name(newValue: .value(newValue))
/// ```
/// and:
/// ```
/// .m1_set_name(newValue: newValue)
/// ```
/// if wrapParams is false.
func setterCaseSpecifier(wrapParams: Bool) throws -> ExprSyntax?
}
================================================
FILE: Sources/MockableMacro/Factory/Caseable/Function+Caseable.swift
================================================
//
// Function+Caseable.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2023. 12. 07..
//
import SwiftSyntax
import Foundation
// MARK: - FunctionRequirement + Caseable
extension FunctionRequirement: Caseable {
var caseDeclarations: [EnumCaseDeclSyntax] {
get throws {
let enumCase = EnumCaseElementSyntax(
name: caseExpression.declName.baseName,
parameterClause: enumParameterClause
)
let elements: EnumCaseElementListSyntax = .init(arrayLiteral: enumCase)
return [EnumCaseDeclSyntax(elements: elements)]
}
}
func caseSpecifier(wrapParams: Bool) throws -> ExprSyntax {
guard let parameters = parameters(wrap: wrapParams) else {
return ExprSyntax(caseExpression)
}
let functionCallExpr = FunctionCallExprSyntax(
calledExpression: caseExpression,
leftParen: .leftParenToken(),
arguments: parameters,
rightParen: .rightParenToken()
)
return ExprSyntax(functionCallExpr)
}
func setterCaseSpecifier(wrapParams: Bool) -> ExprSyntax? { nil }
}
// MARK: - Helpers
extension FunctionRequirement {
private var caseExpression: MemberAccessExprSyntax {
let indexPrefix = String(index + 1)
let specialEnclosingCharacters: CharacterSet = ["`"]
let caseName = syntax.name.trimmedDescription.trimmingCharacters(in: specialEnclosingCharacters)
return MemberAccessExprSyntax(
name: .identifier("m\(indexPrefix)_\(caseName)")
)
}
private func parameters(wrap: Bool) -> LabeledExprListSyntax? {
guard let parameterClause = enumParameterClause else { return nil }
let functionParameters = syntax.signature.parameterClause.parameters
let zippedParameters = zip(parameterClause.parameters, functionParameters)
let enumeratedParameters = zippedParameters.enumerated()
let lastIndex = enumeratedParameters.map(\.offset).last
return LabeledExprListSyntax {
for (index, element) in enumeratedParameters {
let (enumParameter, functionParameter) = element
let hasComma = index != lastIndex && lastIndex != 0
let hasColon = enumParameter.firstName != nil
LabeledExprSyntax(
label: enumParameter.firstName,
colon: hasColon ? .colonToken() : nil,
expression: parameterExpression(
for: functionParameter,
wrapParams: wrap
),
trailingComma: hasComma ? .commaToken() : nil
)
}
}
}
private var enumParameterClause: EnumCaseParameterClauseSyntax? {
guard !syntax.signature.parameterClause.parameters.isEmpty else { return nil }
let enumParameters: EnumCaseParameterListSyntax = .init {
for parameter in syntax.signature.parameterClause.parameters {
let firstName = parameter.firstName.tokenKind == .wildcard
? nil : parameter.firstName.trimmed
EnumCaseParameterSyntax(
firstName: firstName,
colon: firstName == nil ? nil : .colonToken(),
type: wrappedType(for: parameter)
)
}
}
return .init(parameters: enumParameters)
}
private func wrappedType(for parameter: FunctionParameterSyntax) -> IdentifierTypeSyntax {
let type = parameter.resolvedType().description
let isGeneric = syntax.containsGenericType(in: parameter)
let identifier = isGeneric ? NS.GenericValue : type
return IdentifierTypeSyntax(name: NS.Parameter(identifier))
}
private func parameterExpression(for functionParameter: FunctionParameterSyntax, wrapParams: Bool) -> ExprSyntax {
if wrapParams {
wrappedParameterExpression(for: functionParameter)
} else {
parameterExpression(for: functionParameter)
}
}
private func wrappedParameterExpression(for functionParameter: FunctionParameterSyntax) -> ExprSyntax {
let isGeneric = syntax.containsGenericType(in: functionParameter)
let functionParamName = (functionParameter.secondName ?? functionParameter.firstName).declNameOrVarCallName
let functionCallExpr = FunctionCallExprSyntax(
calledExpression: MemberAccessExprSyntax(name: isGeneric ? NS.generic : NS.value),
leftParen: .leftParenToken(),
arguments: LabeledExprListSyntax {
LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: functionParamName))
},
rightParen: .rightParenToken()
)
return ExprSyntax(functionCallExpr)
}
private func parameterExpression(for functionParameter: FunctionParameterSyntax) -> ExprSyntax {
let isGeneric = syntax.containsGenericType(in: functionParameter)
let functionParamName = (functionParameter.secondName ?? functionParameter.firstName).declNameOrVarCallName
if isGeneric {
return ExprSyntax(
FunctionCallExprSyntax(
calledExpression: MemberAccessExprSyntax(
base: DeclReferenceExprSyntax(baseName: functionParamName),
name: NS.eraseToGenericValue
),
leftParen: .leftParenToken(),
arguments: [],
rightParen: .rightParenToken()
)
)
} else {
return ExprSyntax(
DeclReferenceExprSyntax(baseName: functionParamName)
)
}
}
}
================================================
FILE: Sources/MockableMacro/Factory/Caseable/Variable+Caseable.swift
================================================
//
// Variable+Caseable.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 10..
//
import SwiftSyntax
// MARK: - VariableRequirement + Caseable
extension VariableRequirement: Caseable {
var caseDeclarations: [EnumCaseDeclSyntax] {
get throws {
[try getterEnumCaseDeclaration, try setterEnumCaseDeclaration].compactMap { $0 }
}
}
func caseSpecifier(wrapParams: Bool) throws -> ExprSyntax {
ExprSyntax(MemberAccessExprSyntax(name: try getterEnumName))
}
func setterCaseSpecifier(wrapParams: Bool) throws -> ExprSyntax? {
guard let setterName = try setterEnumName else { return nil }
let functionCallExpr = FunctionCallExprSyntax(
calledExpression: MemberAccessExprSyntax(name: setterName),
leftParen: .leftParenToken(),
arguments: LabeledExprListSyntax {
LabeledExprSyntax(
label: NS.newValue,
colon: .colonToken(),
expression: wrapParams ? wrappedSetterParameters : setterParameters
)
},
rightParen: .rightParenToken()
)
return ExprSyntax(functionCallExpr)
}
}
// MARK: - Helpers
extension VariableRequirement {
private func enumName(prefix: String = "") throws -> TokenSyntax {
.identifier("m\(String(index + 1))_\(prefix)\(try syntax.name)")
}
private var getterEnumName: TokenSyntax {
get throws {
syntax.isComputed ? try enumName() : try enumName(prefix: NS.get_)
}
}
private var setterEnumName: TokenSyntax? {
get throws {
syntax.isComputed ? nil : try enumName(prefix: NS.set_)
}
}
private var setterParameters: ExprSyntax {
ExprSyntax(DeclReferenceExprSyntax(baseName: NS.newValue))
}
private var wrappedSetterParameters: ExprSyntax {
let functionCallExpr = FunctionCallExprSyntax(
calledExpression: MemberAccessExprSyntax(name: NS.value),
leftParen: .leftParenToken(),
arguments: LabeledExprListSyntax {
LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: NS.newValue))
},
rightParen: .rightParenToken()
)
return ExprSyntax(functionCallExpr)
}
private var getterEnumCaseDeclaration: EnumCaseDeclSyntax {
get throws {
let getterCase = EnumCaseElementSyntax(name: try getterEnumName)
let elements: EnumCaseElementListSyntax = .init(arrayLiteral: getterCase)
return EnumCaseDeclSyntax(elements: elements)
}
}
private var setterEnumCaseDeclaration: EnumCaseDeclSyntax? {
get throws {
guard let setterName = try setterEnumName else { return nil }
let setterCase = EnumCaseElementSyntax(
name: setterName,
parameterClause: try setterEnumParameterClause
)
let elements: EnumCaseElementListSyntax = .init(arrayLiteral: setterCase)
return EnumCaseDeclSyntax(elements: elements)
}
}
private var wrappedType: IdentifierTypeSyntax {
get throws {
let baseType = try syntax.resolvedType.trimmedDescription
return IdentifierTypeSyntax(name: NS.Parameter(baseType))
}
}
private var setterEnumParameterClause: EnumCaseParameterClauseSyntax? {
get throws {
guard !syntax.isComputed else { return nil }
let parameter = EnumCaseParameterSyntax(
firstName: NS.newValue,
colon: .colonToken(),
type: try wrappedType
)
return EnumCaseParameterClauseSyntax(
parameters: EnumCaseParameterListSyntax([parameter])
)
}
}
}
================================================
FILE: Sources/MockableMacro/Factory/ConformanceFactory.swift
================================================
//
// ConformanceFactory.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 23..
//
import SwiftSyntax
/// Factory to generate mock conformances of requirements.
///
/// Returns a member block item list that includes a mock implementation for every requirement.
enum ConformanceFactory: Factory {
static func build(from requirements: Requirements) throws -> MemberBlockItemListSyntax {
try MemberBlockItemListSyntax {
try inits(requirements)
try functions(requirements)
try variables(requirements)
}
}
}
// MARK: - Helpers
extension ConformanceFactory {
private static func variables(_ requirements: Requirements) throws -> MemberBlockItemListSyntax {
try MemberBlockItemListSyntax {
for variable in requirements.variables {
MemberBlockItemSyntax(
decl: try variable.implement(with: requirements.modifiers)
)
}
}
}
private static func functions(_ requirements: Requirements) throws -> MemberBlockItemListSyntax {
try MemberBlockItemListSyntax {
for function in requirements.functions {
MemberBlockItemSyntax(
decl: try function.implement(with: requirements.modifiers)
)
}
}
}
private static func inits(_ requirements: Requirements) throws -> MemberBlockItemListSyntax {
try MemberBlockItemListSyntax {
for initializer in requirements.initializers {
MemberBlockItemSyntax(
decl: try initializer.implement(with: requirements.modifiers)
)
}
}
}
}
================================================
FILE: Sources/MockableMacro/Factory/EnumFactory.swift
================================================
//
// EnumFactory.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 12..
//
import SwiftSyntax
/// Factory to generate the `Member` enum.
///
/// Generates an enum that represents the given requirements as an enum case.
/// The enum declaration also contains the implementation of the `match(_:)` function.
enum EnumFactory: Factory {
static func build(from requirements: Requirements) throws -> IfConfigDeclSyntax {
SwiftVersionHelper.condition(
minimumVersion: .swift_6_1,
gte: .decls(
try MemberBlockItemListSyntax {
try enumDeclaration(requirements, nonisolated: true)
}
), else: .decls(
try MemberBlockItemListSyntax {
try enumDeclaration(requirements, nonisolated: false)
}
)
)
}
}
// MARK: - Helpers
extension EnumFactory {
private static func enumDeclaration(
_ requirements: Requirements,
nonisolated: Bool
) throws -> EnumDeclSyntax {
EnumDeclSyntax(
modifiers: DeclModifierListSyntax {
requirements.modifiers
if nonisolated {
DeclModifierSyntax(name: .keyword(.nonisolated))
}
},
name: NS.Member,
inheritanceClause: inheritanceClause,
memberBlock: MemberBlockSyntax(members: try members(requirements))
)
}
private static var inheritanceClause: InheritanceClauseSyntax {
InheritanceClauseSyntax {
InheritedTypeSyntax(
type: MemberTypeSyntax(
baseType: IdentifierTypeSyntax(name: NS.Mockable),
name: NS.Matchable
)
)
InheritedTypeSyntax(
type: MemberTypeSyntax(
baseType: IdentifierTypeSyntax(name: NS.Mockable),
name: NS.CaseIdentifiable
)
)
InheritedTypeSyntax(
type: MemberTypeSyntax(
baseType: IdentifierTypeSyntax(name: NS.Swift),
name: NS.Sendable
)
)
}
}
private static func members(_ requirements: Requirements) throws -> MemberBlockItemListSyntax {
try MemberBlockItemListSyntax {
for enumCase in try enumCaseDeclarations(requirements) {
enumCase
}
try matcherFunction(requirements)
}
}
private static func enumCaseDeclarations(_ requirements: Requirements) throws -> [EnumCaseDeclSyntax] {
var cases = [EnumCaseDeclSyntax]()
for variable in requirements.variables {
cases += try variable.caseDeclarations
}
for function in requirements.functions {
cases += try function.caseDeclarations
}
return cases
}
private static func matcherFunction(_ requirements: Requirements) throws -> MemberBlockItemSyntax {
let param = FunctionParameterSyntax(
firstName: .wildcardToken(),
secondName: NS.other,
type: IdentifierTypeSyntax(name: NS.Member)
)
let returnType = IdentifierTypeSyntax(name: NS.Bool)
let signature = FunctionSignatureSyntax(
parameterClause: .init(parameters: [param]),
returnClause: .init(type: returnType)
)
let statement = try CodeBlockItemListSyntax {
try matcherSwitch(requirements)
}
let decl = FunctionDeclSyntax(
modifiers: DeclModifierListSyntax {
requirements.modifiers
DeclModifierSyntax(name: .keyword(.nonisolated))
},
name: NS.match,
signature: signature,
body: .init(statements: statement)
)
return .init(decl: decl)
}
private static func matcherSwitch(_ requirements: Requirements) throws -> ExprSyntax {
let subject = TupleExprSyntax {
LabeledExprListSyntax {
LabeledExprSyntax(
expression: DeclReferenceExprSyntax(baseName: .keyword(.self))
)
LabeledExprSyntax(
expression: DeclReferenceExprSyntax(baseName: NS.other)
)
}
}
let cases =
gitextract_fbsz8xau/
├── .github/
│ └── workflows/
│ ├── doc.yml
│ └── pr.yml
├── .gitignore
├── .swiftlint.yml
├── LICENSE
├── Package.swift
├── README.md
├── Scripts/
│ ├── doc.sh
│ ├── open.sh
│ ├── test.sh
│ └── utils.sh
├── Sources/
│ ├── Mockable/
│ │ ├── Builder/
│ │ │ ├── Builder.swift
│ │ │ ├── FunctionBuilders/
│ │ │ │ ├── FunctionActionBuilder.swift
│ │ │ │ ├── FunctionReturnBuilder.swift
│ │ │ │ ├── FunctionVerifyBuilder.swift
│ │ │ │ ├── ThrowingFunctionActionBuilder.swift
│ │ │ │ ├── ThrowingFunctionReturnBuilder.swift
│ │ │ │ └── ThrowingFunctionVerifyBuilder.swift
│ │ │ ├── MockableService.swift
│ │ │ └── PropertyBuilders/
│ │ │ ├── PropertyActionBuilder.swift
│ │ │ ├── PropertyReturnBuilder.swift
│ │ │ └── PropertyVerifyBuilder.swift
│ │ ├── Documentation/
│ │ │ └── Documentation.docc/
│ │ │ ├── Configuration.md
│ │ │ ├── Installation.md
│ │ │ ├── Mockable.md
│ │ │ └── Usage.md
│ │ ├── Helpers/
│ │ │ ├── Async+Timeout.swift
│ │ │ ├── AsyncSubject.swift
│ │ │ └── LockedValue.swift
│ │ ├── Macro/
│ │ │ └── MockableMacro.swift
│ │ ├── Matcher/
│ │ │ └── Matcher.swift
│ │ ├── Mocker/
│ │ │ ├── CaseIdentifiable.swift
│ │ │ ├── Matchable.swift
│ │ │ ├── MemberAction.swift
│ │ │ ├── MemberReturn.swift
│ │ │ ├── Mocked.swift
│ │ │ ├── Mocker.swift
│ │ │ ├── MockerFallback.swift
│ │ │ ├── MockerPolicy.swift
│ │ │ └── MockerScope.swift
│ │ ├── Models/
│ │ │ ├── Count.swift
│ │ │ ├── GenericValue.swift
│ │ │ ├── Parameter+Match.swift
│ │ │ ├── Parameter.swift
│ │ │ ├── ReturnValue.swift
│ │ │ └── TimeoutDuration.swift
│ │ └── Utils/
│ │ └── Utils.swift
│ └── MockableMacro/
│ ├── Extensions/
│ │ ├── AttributeListSyntax+Extensions.swift
│ │ ├── AttributeSyntax+Extensions.swift
│ │ ├── DeclModifierListSyntax+Extensions.swift
│ │ ├── FunctionDeclSyntax+Extensions.swift
│ │ ├── FunctionParameterSyntax+Extensions.swift
│ │ ├── GenericArgumentSyntax+Extensions.swift
│ │ ├── ProtocolDeclSyntax+Extensions.swift
│ │ ├── String+Extensions.swift
│ │ ├── TokenSyntax+Extensions.swift
│ │ └── VariableDeclSyntax+Extensions.swift
│ ├── Factory/
│ │ ├── Buildable/
│ │ │ ├── Buildable.swift
│ │ │ ├── Function+Buildable.swift
│ │ │ └── Variable+Buildable.swift
│ │ ├── BuilderFactory.swift
│ │ ├── Caseable/
│ │ │ ├── Caseable.swift
│ │ │ ├── Function+Caseable.swift
│ │ │ └── Variable+Caseable.swift
│ │ ├── ConformanceFactory.swift
│ │ ├── EnumFactory.swift
│ │ ├── Factory.swift
│ │ ├── MemberFactory.swift
│ │ ├── MockFactory.swift
│ │ └── Mockable/
│ │ ├── Function+Mockable.swift
│ │ ├── Initializer+Mockable.swift
│ │ ├── Mockable.swift
│ │ └── Variable+Mockable.swift
│ ├── MockableMacro.swift
│ ├── MockableMacroError.swift
│ ├── MockableMacroWarning.swift
│ ├── Plugin.swift
│ ├── Requirements/
│ │ ├── FunctionRequirement.swift
│ │ ├── InitializerRequirement.swift
│ │ ├── Requirements.swift
│ │ └── VariableRequirement.swift
│ └── Utils/
│ ├── Availability.swift
│ ├── Messages.swift
│ ├── Namespace.swift
│ ├── SwiftVersionHelper.swift
│ └── TokenFinder.swift
└── Tests/
├── MockableMacroTests/
│ ├── AccessModifierTests.swift
│ ├── ActorConformanceTests.swift
│ ├── AssociatedTypeTests.swift
│ ├── AttributesTests.swift
│ ├── DocCommentsTests.swift
│ ├── ExoticParameterTests.swift
│ ├── FunctionEffectTests.swift
│ ├── GenericFunctionTests.swift
│ ├── InheritedTypeMappingTests.swift
│ ├── InitRequirementTests.swift
│ ├── NameCollisionTests.swift
│ ├── PropertyRequirementTests.swift
│ ├── TypedThrowsTests_Swift6.swift
│ └── Utils/
│ └── MockableMacroTestCase.swift
└── MockableTests/
├── ActionTests.swift
├── BuildTests.swift
├── ErrorShadowingCompileTests.swift
├── GivenTests.swift
├── GivenTests_Swift6.swift
├── Helpers/
│ └── Task+Sleep.swift
├── PolicyTests.swift
├── Protocols/
│ ├── Models/
│ │ ├── Product.swift
│ │ ├── User.swift
│ │ └── UserError.swift
│ ├── TestService.swift
│ └── TestService_Swift6.swift
└── VerifyTests.swift
Condensed preview — 113 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (493K chars).
[
{
"path": ".github/workflows/doc.yml",
"chars": 673,
"preview": "name: update-documentation\n\non:\n push:\n branches:\n - main\n\njobs:\n update_docs:\n name: Update Documentation\n"
},
{
"path": ".github/workflows/pr.yml",
"chars": 1601,
"preview": "name: pull-request-validation\n\non:\n pull_request:\n branches:\n - \"*\"\n\njobs:\n commit-lint:\n name: Lint Commit"
},
{
"path": ".gitignore",
"chars": 97,
"preview": ".DS_Store\n/.build\n/Packages\nPackage.resolved\nxcuserdata/\nDerivedData/\n.swiftpm\n.netrc\n.env\n.build"
},
{
"path": ".swiftlint.yml",
"chars": 4122,
"preview": "disabled_rules:\n - dynamic_inline\n - private_unit_test\n - type_body_length\n - valid_ibinspectable\n - function_defau"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2023 Kolos Foltanyi\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "Package.swift",
"chars": 2987,
"preview": "// swift-tools-version: 5.9\n\nimport PackageDescription\nimport CompilerPluginSupport\n\nlet test = Context.environment[\"MOC"
},
{
"path": "README.md",
"chars": 20185,
"preview": "\n<p>\n<img width=\"300\" src=\"https://github.com/Kolos65/Mockable/assets/26504214/4e19e4fc-8453-4320-a061-e672dcc95023\" alt"
},
{
"path": "Scripts/doc.sh",
"chars": 202,
"preview": "#!/bin/bash\n\nswift package \\\n --allow-writing-to-directory ./docs \\\n generate-documentation \\\n --target Mockable \\\n --ou"
},
{
"path": "Scripts/open.sh",
"chars": 84,
"preview": "#!/bin/bash\n\nexport MOCKABLE_LINT=true\nexport MOCKABLE_TEST=true\nopen Package.swift\n"
},
{
"path": "Scripts/test.sh",
"chars": 693,
"preview": "#!/bin/bash\n\nset -eo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(dirname \"$SCRIPT_D"
},
{
"path": "Scripts/utils.sh",
"chars": 350,
"preview": "#!/bin/bash\n\n# Function to determine the container runtime\nget_container_runtime() {\n if command -v podman &> /dev/nu"
},
{
"path": "Sources/Mockable/Builder/Builder.swift",
"chars": 545,
"preview": "//\n// Builder.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 21..\n//\n\n/// Used to specify members of "
},
{
"path": "Sources/Mockable/Builder/FunctionBuilders/FunctionActionBuilder.swift",
"chars": 1486,
"preview": "//\n// FunctionActionBuilder.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\n/// A builder for"
},
{
"path": "Sources/Mockable/Builder/FunctionBuilders/FunctionReturnBuilder.swift",
"chars": 2349,
"preview": "//\n// FunctionReturnBuilder.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 14..\n//\n\n/// A builder for"
},
{
"path": "Sources/Mockable/Builder/FunctionBuilders/FunctionVerifyBuilder.swift",
"chars": 2864,
"preview": "//\n// FunctionVerifyBuilder.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\n/// A builder for"
},
{
"path": "Sources/Mockable/Builder/FunctionBuilders/ThrowingFunctionActionBuilder.swift",
"chars": 547,
"preview": "//\n// ThrowingFunctionActionBuilder.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\n/// A bui"
},
{
"path": "Sources/Mockable/Builder/FunctionBuilders/ThrowingFunctionReturnBuilder.swift",
"chars": 3495,
"preview": "//\n// ThrowingFunctionReturnBuilder.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\n/// A bui"
},
{
"path": "Sources/Mockable/Builder/FunctionBuilders/ThrowingFunctionVerifyBuilder.swift",
"chars": 533,
"preview": "//\n// ThrowingFunctionVerifyBuilder.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\n/// A bui"
},
{
"path": "Sources/Mockable/Builder/MockableService.swift",
"chars": 1263,
"preview": "//\n// MockableService.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 13..\n//\n\n/// A protocol defining"
},
{
"path": "Sources/Mockable/Builder/PropertyBuilders/PropertyActionBuilder.swift",
"chars": 2279,
"preview": "//\n// PropertyActionBuilder.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\n/// A builder for"
},
{
"path": "Sources/Mockable/Builder/PropertyBuilders/PropertyReturnBuilder.swift",
"chars": 2118,
"preview": "//\n// PropertyReturnBuilder.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\n/// A builder for"
},
{
"path": "Sources/Mockable/Builder/PropertyBuilders/PropertyVerifyBuilder.swift",
"chars": 5189,
"preview": "//\n// PropertyVerifyBuilder.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\n/// A builder for"
},
{
"path": "Sources/Mockable/Documentation/Documentation.docc/Configuration.md",
"chars": 2849,
"preview": "# Configuration\n\nLearn how to configure build settings so generated mock implementations are excluded from release build"
},
{
"path": "Sources/Mockable/Documentation/Documentation.docc/Installation.md",
"chars": 1914,
"preview": "# Installation\n\nLearn how to install **Mockable** and integrate into your targets.\n\n## Overview\n\n**Mockable** can be ins"
},
{
"path": "Sources/Mockable/Documentation/Documentation.docc/Mockable.md",
"chars": 780,
"preview": "# ``Mockable``\n\nA macro driven testing framework that provides automatic mock implementations for your protocols. It off"
},
{
"path": "Sources/Mockable/Documentation/Documentation.docc/Usage.md",
"chars": 13057,
"preview": "# Usage\n\nLarn how to use **Mockable** to write readable and concise unit tests.\n\n## Overview\n\nGiven a protocol annotated"
},
{
"path": "Sources/Mockable/Helpers/Async+Timeout.swift",
"chars": 1184,
"preview": "//\n// Async+Timeout.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2024. 04. 04..\n//\n\nimport Foundation\n\npublic"
},
{
"path": "Sources/Mockable/Helpers/AsyncSubject.swift",
"chars": 2915,
"preview": "//\n// AsyncSubject.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2024. 12. 16..\n//\n\nactor AsyncSubject<Element"
},
{
"path": "Sources/Mockable/Helpers/LockedValue.swift",
"chars": 3666,
"preview": "//\n// LockedValue.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2024. 12. 16..\n//\nimport Foundation\n\n/// A gen"
},
{
"path": "Sources/Mockable/Macro/MockableMacro.swift",
"chars": 1232,
"preview": "//\n// MockableMacro.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 20..\n//\n\n/// A peer macro that gen"
},
{
"path": "Sources/Mockable/Matcher/Matcher.swift",
"chars": 11459,
"preview": "//\n// Matcher.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 14..\n//\n\nimport Foundation\n\n/// A utilit"
},
{
"path": "Sources/Mockable/Mocker/CaseIdentifiable.swift",
"chars": 2441,
"preview": "//\n// CaseIdentifiable.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\n/// A protocol for enu"
},
{
"path": "Sources/Mockable/Mocker/Matchable.swift",
"chars": 502,
"preview": "//\n// Matchable.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 14..\n//\n\n/// A protocol for types that"
},
{
"path": "Sources/Mockable/Mocker/MemberAction.swift",
"chars": 840,
"preview": "//\n// MemberAction.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 14..\n//\n\n/// A class representing a"
},
{
"path": "Sources/Mockable/Mocker/MemberReturn.swift",
"chars": 984,
"preview": "//\n// MemberReturn.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 14..\n//\n\n/// A class representing t"
},
{
"path": "Sources/Mockable/Mocker/Mocked.swift",
"chars": 1736,
"preview": "//\n// Mocked.swift\n// Mocked\n//\n// Created by Kolos Foltanyi on 2024. 04. 03..\n//\n\n/// A protocol that represents aut"
},
{
"path": "Sources/Mockable/Mocker/Mocker.swift",
"chars": 20266,
"preview": "//\n// Mocker.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 14..\n//\n\nimport Foundation\nimport IssueRe"
},
{
"path": "Sources/Mockable/Mocker/MockerFallback.swift",
"chars": 502,
"preview": "//\n// MockerFallback.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2024. 03. 17..\n//\n\n/// Describes an optiona"
},
{
"path": "Sources/Mockable/Mocker/MockerPolicy.swift",
"chars": 2021,
"preview": "//\n// MockerPolicy.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2024. 04. 01..\n//\n\n/// A policy that controls"
},
{
"path": "Sources/Mockable/Mocker/MockerScope.swift",
"chars": 697,
"preview": "//\n// MockerScope.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 21..\n//\n\n/// An enumeration represen"
},
{
"path": "Sources/Mockable/Models/Count.swift",
"chars": 3091,
"preview": "//\n// Count.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 13..\n//\n\n/// An enumeration representing d"
},
{
"path": "Sources/Mockable/Models/GenericValue.swift",
"chars": 970,
"preview": "//\n// GenericAttribute.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 17..\n//\n\n/// A type erased wrap"
},
{
"path": "Sources/Mockable/Models/Parameter+Match.swift",
"chars": 2466,
"preview": "//\n// Parameter+Match.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 25..\n//\n\nimport IssueReporting\n\n"
},
{
"path": "Sources/Mockable/Models/Parameter.swift",
"chars": 2101,
"preview": "//\n// Parameter.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 13..\n//\n\nimport IssueReporting\n\n/// An"
},
{
"path": "Sources/Mockable/Models/ReturnValue.swift",
"chars": 1231,
"preview": "//\n// ReturnValue.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2023. 11. 13..\n//\n\n/// An enumeration represen"
},
{
"path": "Sources/Mockable/Models/TimeoutDuration.swift",
"chars": 987,
"preview": "//\n// TimeoutDuration.swift\n// Mockable\n//\n//\n// Created by Nayanda Haberty on 3/4/24.\n//\n\nimport Foundation\n\n/// An "
},
{
"path": "Sources/Mockable/Utils/Utils.swift",
"chars": 2561,
"preview": "//\n// Utils.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2024. 03. 17..\n//\n\n/// Creates a proxy for building "
},
{
"path": "Sources/MockableMacro/Extensions/AttributeListSyntax+Extensions.swift",
"chars": 1135,
"preview": "//\n// File.swift\n// Mockable\n//\n// Created by Kolos Foltányi on 2025. 11. 16..\n//\n\nimport SwiftSyntax\n\nextension Attr"
},
{
"path": "Sources/MockableMacro/Extensions/AttributeSyntax+Extensions.swift",
"chars": 377,
"preview": "//\n// AttributeSyntax+Extensions.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 25..\n//\n\nimport "
},
{
"path": "Sources/MockableMacro/Extensions/DeclModifierListSyntax+Extensions.swift",
"chars": 538,
"preview": "//\n// DeclModifierListSyntax+Extensions.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 27/09/2024.\n//\n\nimport S"
},
{
"path": "Sources/MockableMacro/Extensions/FunctionDeclSyntax+Extensions.swift",
"chars": 2082,
"preview": "//\n// FunctionDeclSyntax+Extensions.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimpo"
},
{
"path": "Sources/MockableMacro/Extensions/FunctionParameterSyntax+Extensions.swift",
"chars": 1371,
"preview": "//\n// FunctionParameterSyntax+Extensions.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2023. 11. 20..\n//\n"
},
{
"path": "Sources/MockableMacro/Extensions/GenericArgumentSyntax+Extensions.swift",
"chars": 439,
"preview": "//\n// GenericArgumentSyntax+Extensions.swift\n// Mockable\n//\n// Created by Scott Hoyt on 10/06/2025.\n//\n\nimport SwiftS"
},
{
"path": "Sources/MockableMacro/Extensions/ProtocolDeclSyntax+Extensions.swift",
"chars": 369,
"preview": "//\n// ProtocolDeclSyntax+Extensions.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimpo"
},
{
"path": "Sources/MockableMacro/Extensions/String+Extensions.swift",
"chars": 309,
"preview": "//\n// String+Extensions.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2023. 11. 19..\n//\n\nextension String"
},
{
"path": "Sources/MockableMacro/Extensions/TokenSyntax+Extensions.swift",
"chars": 5019,
"preview": "//\n// TokenSyntax+Extensions.swift\n// MockableMacro\n//\n// Created by Kolos Foltányi on 2025. 05. 06..\n//\n\nimport Swif"
},
{
"path": "Sources/MockableMacro/Extensions/VariableDeclSyntax+Extensions.swift",
"chars": 3153,
"preview": "//\n// VariableDeclSyntax+Extensions.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 10..\n//\n\nimpo"
},
{
"path": "Sources/MockableMacro/Factory/Buildable/Buildable.swift",
"chars": 1511,
"preview": "//\n// Buildable.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimport SwiftSyntax\n\n/// "
},
{
"path": "Sources/MockableMacro/Factory/Buildable/Function+Buildable.swift",
"chars": 7969,
"preview": "//\n// Function+Buildable.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimport SwiftSyn"
},
{
"path": "Sources/MockableMacro/Factory/Buildable/Variable+Buildable.swift",
"chars": 6563,
"preview": "//\n// Variable+Buildable.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimport SwiftSyn"
},
{
"path": "Sources/MockableMacro/Factory/BuilderFactory.swift",
"chars": 4148,
"preview": "//\n// BuilderFactory.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimport SwiftSyntax\n"
},
{
"path": "Sources/MockableMacro/Factory/Caseable/Caseable.swift",
"chars": 1731,
"preview": "//\n// Caseable.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2023. 12. 07..\n//\n\nimport SwiftSyntax\n\n/// D"
},
{
"path": "Sources/MockableMacro/Factory/Caseable/Function+Caseable.swift",
"chars": 5820,
"preview": "//\n// Function+Caseable.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2023. 12. 07..\n//\n\nimport SwiftSynt"
},
{
"path": "Sources/MockableMacro/Factory/Caseable/Variable+Caseable.swift",
"chars": 3876,
"preview": "//\n// Variable+Caseable.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 10..\n//\n\nimport SwiftSynt"
},
{
"path": "Sources/MockableMacro/Factory/ConformanceFactory.swift",
"chars": 1730,
"preview": "//\n// ConformanceFactory.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimport SwiftSyn"
},
{
"path": "Sources/MockableMacro/Factory/EnumFactory.swift",
"chars": 10460,
"preview": "//\n// EnumFactory.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 12..\n//\n\nimport SwiftSyntax\n\n//"
},
{
"path": "Sources/MockableMacro/Factory/Factory.swift",
"chars": 983,
"preview": "//\n// Factory.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 28..\n//\n\nimport SwiftSyntax\n\n/// De"
},
{
"path": "Sources/MockableMacro/Factory/MemberFactory.swift",
"chars": 9595,
"preview": "//\n// MemberFactory.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimport SwiftSyntax\n\n"
},
{
"path": "Sources/MockableMacro/Factory/MockFactory.swift",
"chars": 5980,
"preview": "//\n// MockFactory.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 28..\n//\n\n#if canImport(SwiftSyn"
},
{
"path": "Sources/MockableMacro/Factory/Mockable/Function+Mockable.swift",
"chars": 6934,
"preview": "//\n// Function+Mockable.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimport SwiftSynt"
},
{
"path": "Sources/MockableMacro/Factory/Mockable/Initializer+Mockable.swift",
"chars": 1204,
"preview": "//\n// Initializer+Mockable.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimport SwiftS"
},
{
"path": "Sources/MockableMacro/Factory/Mockable/Mockable.swift",
"chars": 594,
"preview": "//\n// Mockable.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimport SwiftSyntax\n\n/// D"
},
{
"path": "Sources/MockableMacro/Factory/Mockable/Variable+Mockable.swift",
"chars": 8104,
"preview": "//\n// Variable+Mockable.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 23..\n//\n\nimport SwiftSynt"
},
{
"path": "Sources/MockableMacro/MockableMacro.swift",
"chars": 1322,
"preview": "//\n// MockableMacro.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2023. 11. 14..\n//\n\nimport SwiftSyntax\ni"
},
{
"path": "Sources/MockableMacro/MockableMacroError.swift",
"chars": 1423,
"preview": "//\n// MockableMacroError.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2023. 11. 24..\n//\n\npublic enum Moc"
},
{
"path": "Sources/MockableMacro/MockableMacroWarning.swift",
"chars": 1183,
"preview": "//\n// MockableMacroWarning.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 2024. 12. 01..\n//\n\nimport SwiftSyntax"
},
{
"path": "Sources/MockableMacro/Plugin.swift",
"chars": 278,
"preview": "//\n// MockableMacroPlugin.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2023. 11. 14..\n//\n\nimport SwiftCo"
},
{
"path": "Sources/MockableMacro/Requirements/FunctionRequirement.swift",
"chars": 211,
"preview": "//\n// FunctionRequirement.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2023. 12. 07..\n//\n\nimport SwiftSy"
},
{
"path": "Sources/MockableMacro/Requirements/InitializerRequirement.swift",
"chars": 220,
"preview": "//\n// InitializerRequirement.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 14..\n//\n\nimport Swif"
},
{
"path": "Sources/MockableMacro/Requirements/Requirements.swift",
"chars": 5491,
"preview": "//\n// Requirements.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 12..\n//\n\nimport SwiftSyntax\n\ns"
},
{
"path": "Sources/MockableMacro/Requirements/VariableRequirement.swift",
"chars": 211,
"preview": "//\n// VariableRequirement.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 10..\n//\n\nimport SwiftSy"
},
{
"path": "Sources/MockableMacro/Utils/Availability.swift",
"chars": 1771,
"preview": "//\n// Availability.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 30/07/2024.\n//\n\nimport SwiftSyntax\n\nenum"
},
{
"path": "Sources/MockableMacro/Utils/Messages.swift",
"chars": 302,
"preview": "//\n// Messages.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 04. 01..\n//\n\nenum Messages {\n stati"
},
{
"path": "Sources/MockableMacro/Utils/Namespace.swift",
"chars": 4110,
"preview": "//\n// Namespace.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2024. 03. 28..\n//\n\n#if canImport(SwiftSynta"
},
{
"path": "Sources/MockableMacro/Utils/SwiftVersionHelper.swift",
"chars": 1674,
"preview": "//\n// File.swift\n// Mockable\n//\n// Created by Kolos Foltányi on 2025. 11. 13..\n//\n\nimport SwiftSyntax\n\nenum SwiftVers"
},
{
"path": "Sources/MockableMacro/Utils/TokenFinder.swift",
"chars": 912,
"preview": "//\n// TokenFinder.swift\n// MockableMacro\n//\n// Created by Kolos Foltanyi on 2023. 11. 20..\n//\n\nimport SwiftSyntax\n\ncl"
},
{
"path": "Tests/MockableMacroTests/AccessModifierTests.swift",
"chars": 16163,
"preview": "//\n// AccessModifierTests.swift\n//\n//\n// Created by Nayanda Haberty on 29/5/24.\n//\n\nimport MacroTesting\nimport XCTest\n"
},
{
"path": "Tests/MockableMacroTests/ActorConformanceTests.swift",
"chars": 25319,
"preview": "//\n// ActorConformanceTests.swift\n// \n//\n// Created by Kolos Foltanyi on 06/07/2024.\n//\n\nimport MacroTesting\nimport X"
},
{
"path": "Tests/MockableMacroTests/AssociatedTypeTests.swift",
"chars": 13580,
"preview": "//\n// AssociatedTypeTests.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 21..\n//\n\nimport MacroTesting\nimport XC"
},
{
"path": "Tests/MockableMacroTests/AttributesTests.swift",
"chars": 9439,
"preview": "//\n// AttributesTest.swift\n//\n//\n// Created by Kolos Foltanyi on 2024. 03. 17..\n//\n\nimport MacroTesting\nimport XCTest\n"
},
{
"path": "Tests/MockableMacroTests/DocCommentsTests.swift",
"chars": 4423,
"preview": "//\n// DocCommentsTests.swift\n// \n//\n// Created by Sagar Dagdu on 20/12/24.\n//\n\nimport XCTest\nimport MacroTesting\n\nfin"
},
{
"path": "Tests/MockableMacroTests/ExoticParameterTests.swift",
"chars": 17981,
"preview": "//\n// ExoticParameterTests.swift\n// \n//\n// Created by Kolos Foltanyi on 2023. 11. 21..\n//\n\nimport MacroTesting\nimport"
},
{
"path": "Tests/MockableMacroTests/FunctionEffectTests.swift",
"chars": 18611,
"preview": "//\n// FunctionEffectTests.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 21..\n//\n\nimport MacroTesting\nimport XC"
},
{
"path": "Tests/MockableMacroTests/GenericFunctionTests.swift",
"chars": 32752,
"preview": "//\n// GenericFunctionTests.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 21..\n//\n\nimport MacroTesting\nimport X"
},
{
"path": "Tests/MockableMacroTests/InheritedTypeMappingTests.swift",
"chars": 4265,
"preview": "//\n// InheritedTypeMappingTests.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 27/09/2024.\n//\n\nimport MacroTest"
},
{
"path": "Tests/MockableMacroTests/InitRequirementTests.swift",
"chars": 6474,
"preview": "//\n// InitRequirementTests.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 23..\n//\n\nimport MacroTesting\nimport X"
},
{
"path": "Tests/MockableMacroTests/NameCollisionTests.swift",
"chars": 17113,
"preview": "//\n// NameCollisionTests.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 21..\n//\n\nimport MacroTesting\nimport XCT"
},
{
"path": "Tests/MockableMacroTests/PropertyRequirementTests.swift",
"chars": 21090,
"preview": "//\n// PropertyRequirementTests.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 21..\n//\n\nimport MacroTesting\nimpo"
},
{
"path": "Tests/MockableMacroTests/TypedThrowsTests_Swift6.swift",
"chars": 6048,
"preview": "//\n// TypedThrowsTests.swift\n// \n//\n// Created by Kolos Foltanyi on 08/07/2024.\n//\n\nimport MacroTesting\nimport XCTest"
},
{
"path": "Tests/MockableMacroTests/Utils/MockableMacroTestCase.swift",
"chars": 611,
"preview": "//\n// MockableMacroTestCase.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 21..\n//\n\nimport Foundation\nimport Sw"
},
{
"path": "Tests/MockableTests/ActionTests.swift",
"chars": 3427,
"preview": "//\n// ActionTests.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\nimport XCTest\nimport Mockable\n\nfinal "
},
{
"path": "Tests/MockableTests/BuildTests.swift",
"chars": 3052,
"preview": "//\n// TestProtocol.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 21..\n//\n\nimport Mockable\n\n@Mockable\nprotocol "
},
{
"path": "Tests/MockableTests/ErrorShadowingCompileTests.swift",
"chars": 332,
"preview": "import XCTest\nimport Mockable\n\nprivate typealias Error = Int\n\n@Mockable\nprivate protocol ShadowedErrorTypealiasService {"
},
{
"path": "Tests/MockableTests/GivenTests.swift",
"chars": 11281,
"preview": "//\n// GivenTests.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\nimport XCTest\nimport Mockable\n\nfinal c"
},
{
"path": "Tests/MockableTests/GivenTests_Swift6.swift",
"chars": 826,
"preview": "//\n// GivenTests_Swift6.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 28/09/2024.\n//\n\nimport XCTest\nimport Moc"
},
{
"path": "Tests/MockableTests/Helpers/Task+Sleep.swift",
"chars": 297,
"preview": "//\n// Task+Sleep.swift\n//\n//\n// Created by Kolos Foltanyi on 2024. 04. 07..\n//\n\nimport Foundation\n\nextension Task wher"
},
{
"path": "Tests/MockableTests/PolicyTests.swift",
"chars": 2714,
"preview": "//\n// PolicyTests.swift\n// \n//\n// Created by Kolos Foltanyi on 2024. 04. 02..\n//\n\nimport XCTest\nimport Mockable\n\npubl"
},
{
"path": "Tests/MockableTests/Protocols/Models/Product.swift",
"chars": 209,
"preview": "//\n// Product.swift\n// \n//\n// Created by Kolos Foltanyi on 2023. 11. 26..\n//\n\nimport Foundation\n\nstruct Product {\n "
},
{
"path": "Tests/MockableTests/Protocols/Models/User.swift",
"chars": 554,
"preview": "//\n// User.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 26..\n//\n\nimport Foundation\n\nstruct User: Equatable, H"
},
{
"path": "Tests/MockableTests/Protocols/Models/UserError.swift",
"chars": 127,
"preview": "//\n// UserError.swift\n// \n//\n// Created by Kolos Foltanyi on 2023. 11. 26..\n//\n\nenum UserError: Error {\n case notF"
},
{
"path": "Tests/MockableTests/Protocols/TestService.swift",
"chars": 912,
"preview": "//\n// TestService.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 24..\n//\n\nimport Mockable\nimport Foundation\n\n@M"
},
{
"path": "Tests/MockableTests/Protocols/TestService_Swift6.swift",
"chars": 282,
"preview": "//\n// TestService6.swift\n// Mockable\n//\n// Created by Kolos Foltanyi on 28/09/2024.\n//\n\nimport Mockable\n\n#if swift(>="
},
{
"path": "Tests/MockableTests/VerifyTests.swift",
"chars": 3721,
"preview": "//\n// VerifyTests.swift\n//\n//\n// Created by Kolos Foltanyi on 2023. 11. 22..\n//\n\nimport XCTest\nimport Foundation\n@test"
}
]
About this extraction
This page contains the full source code of the Kolos65/Mockable GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 113 files (458.9 KB), approximately 100.6k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.