Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/ISSUE_TEMPLATE/general-issue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
name: General issue
about: Report a reproducible bug or issue
title: '[Bug] [Feature Request] Short description of the issue'
labels: ''
assignees: ''
---

## 🐞 Expected Behavior
<!-- Describe what should happen under normal conditions. -->

## ❌ Actual Behavior
<!-- Describe what is currently happening instead of the expected behavior. -->

## 🔄 Steps to Reproduce
1. <!-- Step 1: Provide clear instructions to reproduce the issue. -->
2. <!-- Step 2: Add any additional steps as needed. -->
3. <!-- Continue listing steps until the issue is fully reproducible. -->

## 📄 Logs / Error Messages
<!-- Paste any relevant logs, error messages, or stack traces. -->

## 📸 Screenshots
<!-- Attach screenshots or GIFs that help visualize the issue. -->

## 💻 Code Snippets
```swift
// Include relevant code snippets that help reproduce the issue.
```

## 🛠 System Information
- **Package Version**: <!-- e.g., 0.1.0 -->
- **Operating System**: <!-- e.g., macOS 14, iOS 18 -->
- **Device / Hardware**: <!-- e.g., iPhone 14 Pro, M1 MacBook -->
8 changes: 8 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## What
<!-- What was changed -->

## Why
<!-- Why this change was needed -->

## Changes
<!-- List affected files/modules -->
14 changes: 14 additions & 0 deletions .github/workflows/test-and-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Package Test

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
coverage:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: Snapp-Mobile/[email protected]
5 changes: 5 additions & 0 deletions .spi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: 1
builder:
configs:
- documentation_targets: [Fetcher]
platform: ios
57 changes: 57 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"version": 1,
"lineLength": 120,
"indentation": {
"spaces": 4
},
"tabWidth": 4,
"maximumBlankLines": 1,
"respectsExistingLineBreaks": true,
"lineBreakBeforeEachArgument": true,
"lineBreakBeforeEachGenericRequirement": true,
"prioritizeKeepingFunctionOutputTogether": false,
"indentConditionalCompilationBlocks": true,
"lineBreakAroundMultilineExpressionChainComponents": false,
"indentSwitchCaseLabels": false,
"spacesAroundRangeFormationOperators": false,
"noAssignmentInExpressions": {
"allowedFunctions": ["XCTAssertNoThrow"]
},
"fileScopedDeclarationPrivacy": {
"accessLevel": "private"
},
"rules": {
"AllPublicDeclarationsHaveDocumentation": true,
"AlwaysUseLowerCamelCase": true,
"AmbiguousTrailingClosureOverload": true,
"BeginDocumentationCommentWithOneLineSummary": false,
"DoNotUseSemicolons": true,
"DontRepeatTypeInStaticProperties": true,
"FileScopedDeclarationPrivacy": true,
"FullyIndirectEnum": true,
"GroupNumericLiterals": true,
"IdentifiersMustBeASCII": true,
"NeverForceUnwrap": false,
"NeverUseForceTry": false,
"NeverUseImplicitlyUnwrappedOptionals": false,
"NoAccessLevelOnExtensionDeclaration": true,
"NoAssignmentInExpressions": true,
"NoEmptyTrailingClosureParentheses": true,
"NoLeadingUnderscores": false,
"NoParensAroundConditions": true,
"NoVoidReturnOnFunctionSignature": true,
"OneCasePerLine": true,
"OneVariableDeclarationPerLine": true,
"OnlyOneTrailingClosureArgument": true,
"OrderedImports": true,
"ReturnVoidInsteadOfEmptyTuple": true,
"UseEarlyExits": false,
"UseLetInEveryBoundCaseVariable": false,
"UseShorthandTypeNames": true,
"UseSingleLinePropertyGetter": true,
"UseSynthesizedInitializer": false,
"UseTripleSlashForDocumentationComments": true,
"UseWhereClausesInForLoops": false,
"ValidateDocumentationComments": false
}
}
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Change Log
All notable changes to this project will be documented in this file.

<!--
## [Unreleased] - yyyy-mm-dd

### Added
- [ISSUE-XXXX](http://tickets.projectname.com/browse/PROJECTNAME-XXXX)
Ticket title goes here.
- [ISSUE-YYYY](http://tickets.projectname.com/browse/PROJECTNAME-YYYY)
Ticket title goes here.

### Changed
- Describe changes here.

### Fixed
- Describe fixes here.

-->

## [0.1.0] - 2026-01-12

Initial public release of Fetcher.

### Added
- MIT License for the project
- Continuous Integration workflow
- Swift Package Index configuration (.spi.yml)
- Shield badges to README
- Description, installation and usage instructions to README
- `Fetcher` class with `async/await` support
- `APIURL` and `FetcherEnvironment` protocols
- `Token` and `APIError` models
- `URLSession` backports for older iOS versions
- Error handling for API responses and token management
- Concurrency utilities for multiple requests

## [0.0.2] - 2023-02-10
Improved Concurrency integration

## [0.0.1] - 2022-07-01
Initial implementation
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Snapp Mobile Germany GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
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.
12 changes: 9 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ let package = Package(
targets: ["Fetcher"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(
url: "https://github.com/Snapp-Mobile/SwiftFormatLintPlugin.git",
exact: "1.0.4"
)
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand All @@ -27,7 +29,11 @@ let package = Package(
dependencies: [],
resources: [
.process("Mocks")
]),
],
plugins: [
.plugin(name: "Lint", package: "SwiftFormatLintPlugin")
]
),
.testTarget(
name: "FetcherTests",
dependencies: ["Fetcher"]),
Expand Down
143 changes: 136 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,144 @@
<div align="center">
<img src="Sources/Fetcher/Fetcher.docc/Resources/Fetcher-logo.png" width="200" alt="Fetcher Logo">
</div>

# Fetcher

A lightweight REST API client written in Swift
A lightweight, modern, and robust REST API client written in Swift.

[![Swift Package Index](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSnapp-Mobile%2FFetcher%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Snapp-Mobile/Fetcher)
[![iOS 13.0+ | macOS 11.0+](https://img.shields.io/badge/iOS_13.0%2B_%7C_macOS_11.0%2B-007AFF?logo=apple&logoColor=white)](https://www.apple.com/ios/)
[![Latest Release](https://img.shields.io/github/v/release/Snapp-Mobile/Fetcher?color=8B5CF6&logo=github&logoColor=white)](https://github.com/Snapp-Mobile/Fetcher/releases)
[![Tests](https://github.com/Snapp-Mobile/Fetcher/actions/workflows/test-and-coverage.yml/badge.svg)](https://github.com/Snapp-Mobile/Fetcher/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-22C55E)](LICENSE)

## Overview

The framework exposes a tiny client and a few helper protocols to consume REST APIs.
`Fetcher` is a Swift library designed to simplify interactions with RESTful APIs. It provides a clean, protocol-oriented, and testable solution for making network requests, handling responses, and managing common API workflows like authentication and error handling.

The core of the framework is the `Fetcher` struct, which acts as the main client for performing requests. It operates on objects conforming to the `APIURL` protocol, which provides a structured and type-safe way to define your API endpoints, avoiding scattered URL strings across your codebase.

Key features include:
- **Asynchronous API**: Modern `async/await` syntax for clean and concurrent network calls.
- **Protocol-Driven Endpoints**: Define your API endpoints semantically using the `APIURL` protocol.
- **Robust Error Handling**: A comprehensive `APIError` enum to represent various network and server errors.
- **Token Management**: Built-in logic to handle token-based authentication, including automatic retries and token refresh.
- **Environment-Based Configuration**: Use the `FetcherEnvironment` protocol to inject dependencies like `URLSession` and custom logic, making your networking layer highly testable.
- **Concurrency Utilities**: Helpers for running multiple requests in parallel.

## Installation

Add `Fetcher` as a dependency to your `Package.swift` file:

```swift
.package(url: "https://github.com/Snapp-Mobile/Fetcher.git", from: "0.1.0")
```

## Setup

To use Fetcher, you need to provide an implementation of `FetcherEnvironment`.

```swift
import Fetcher
import Foundation
import os.log

// A basic implementation of FetcherEnvironment for demonstration.
// In a real application, you would manage tokens, network reachability,
// and logging more robustly.
class MyProductionEnvironment: FetcherEnvironment {
let urlSession: URLSession = .shared
var isNetworkingAvailable: Bool = true // You might use Network.framework here

var apiErrorsLogger: FetcherLogger? = nil // Implement a custom logger if needed

// Placeholder implementations for token management
func updateToken(to newToken: Fetcher.Token?, logger: FetcherLogger?) async throws -> Fetcher.Token? {
// ... update stored token ...
return newToken
}

func refreshToken(_ token: Fetcher.Token?, using fetcher: Fetcher) async throws -> Fetcher.Token? {
// ... logic to refresh token, e.g., make an API call to your auth server ...
return token // Return a new, valid token
}

func getToken(logger: FetcherLogger?) async throws -> Fetcher.Token? {
// ... retrieve current token from storage ...
return nil
}

func logout() async throws {
// ... clear stored token and user session ...
}
}

let myEnvironment = MyProductionEnvironment()
let fetcher = Fetcher(environment: myEnvironment)
```

## Usage

Define your API endpoints by conforming to the `APIURL` protocol:

```swift
import Fetcher
import Foundation

// Example API endpoint definition
enum MyAPIEndpoint: APIURL {
case getGreeting

var path: String {
switch self {
case .getGreeting:
return "/greeting"
}
}

var requestMethod: String {
switch self {
case .getGreeting:
return "GET"
}
}

var url: URL? {
return URL(string: "https://api.example.com")?.appendingPathComponent(path)
}

func bodyParams(token: Fetcher.Token?) -> [String: Any]? {
return nil
}

func request(token: Fetcher.Token?) -> URLRequest? {
guard let url = self.url else { return nil }
var request = URLRequest(url: url)
request.httpMethod = requestMethod
// Add authorization header if token is available
if let accessToken = token?.accessToken {
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
return request
}
}

// Define a Decodable struct for your API response
struct GreetingResponse: Decodable {
let message: String
}

The core of the framework is the `Fetcher` API client itself along with the `APIURL` protocol.
These two are connected in a way that the `Fetcher` only operates on implementations of `APIURL`.
The main reason for operating with `Fetcher/APIURL` is to avoid having hardcoded URL strings accross your entire
codebase. Instead, you can wrap them in enums that conform to the `APIURL` protocol.
// To fetch data from your API:
func fetchGreeting() async {
do {
let greeting: GreetingResponse = try await fetcher.fetch(MyAPIEndpoint.getGreeting)
print("Received greeting: \(greeting.message)")
} catch {
print("Failed to fetch greeting: \(error.localizedDescription)")
}
}

Of course `Fetcher` can also operate on plain `URLRequest` objects to fetch the response data, if needed.
// Call the function (e.g., from an async context)
// Task {
// await fetchGreeting()
// }
12 changes: 6 additions & 6 deletions Sources/Fetcher/Extensions/OSLog+Categories.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
//
// OSLog+Categories.swift
//
//
//
// Created by Ilian Konchev on 17.06.20.
// Copyright © 2020 Ilian Konchev. All rights reserved.
//
import Foundation
import os.log

public extension OSLog {
extension OSLog {
private static var subsystem = Bundle.main.bundleIdentifier!

/// Logs the view cycles like viewDidLoad.
static let viewCycle = OSLog(subsystem: subsystem, category: "ViewCycle")
public static let viewCycle = OSLog(subsystem: subsystem, category: "ViewCycle")
/// Logs the API data-related info
static let apiData = OSLog(subsystem: subsystem, category: "APIData")
public static let apiData = OSLog(subsystem: subsystem, category: "APIData")
/// Logs generic app info
static let app = OSLog(subsystem: subsystem, category: "Application")
public static let app = OSLog(subsystem: subsystem, category: "Application")
/// Logs out the info that goes to file loggers
static let fileLogger = OSLog(subsystem: subsystem, category: "FileLogger")
public static let fileLogger = OSLog(subsystem: subsystem, category: "FileLogger")
}
Loading