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
21 changes: 10 additions & 11 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
---
name: Bug report
about: Something isn't working as expected
about: Report a reproducible bug
labels: bug
---

**Nexio version:**

**Platform / Swift version:**

**Describe the bug**
A clear description of what the bug is.

**To Reproduce**
Minimal code to reproduce the issue.

**To reproduce**
```swift
// your code here
// Minimal code that reproduces the issue
```

**Expected behaviour**
What you expected to happen.

**Actual behaviour**
What actually happened, including any error messages or stack traces.
What actually happened. Include the full error message or stack trace if applicable.

**Environment**
- Nexio version:
- Swift version:
- Xcode version:
- Platform (iOS/macOS/watchOS/tvOS) and OS version:
**Additional context**
Anything else that might help (network conditions, specific server behaviour, etc.)
16 changes: 7 additions & 9 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
---
name: Feature request
about: Suggest an improvement or new capability
about: Propose a new feature or API change
labels: enhancement
---

**Problem**
Describe the problem this feature would solve.

**Proposed solution**
How you'd like the feature to work, ideally with a code example.
**Problem / motivation**
What problem does this solve? What use case does it unlock?

**Proposed API**
```swift
// example API
// How would you use this feature?
```

**Alternatives considered**
Any alternative approaches you've considered.
Any workarounds or alternative designs you've considered.

**Additional context**
Any other context or screenshots.
Links, prior art, related issues.
17 changes: 17 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## What changed

<!-- A short description of the change and why it was made -->

## Type of change

- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation / tests only

## Checklist

- [ ] `swift build -c release` passes with zero warnings
- [ ] `swift test` passes
- [ ] New public API has a doc comment
- [ ] `CHANGELOG.md` updated under `[Unreleased]`
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Changelog

All notable changes to Nexio will be documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Nexio uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [Unreleased]

## [1.0.0] - 2026-06-07

### Added
- `NexioClient` actor — type-safe GET, POST, PUT, PATCH, DELETE with automatic JSON encoding/decoding
- `NexioConfig` — base URL, timeout, default headers, retry policy, log level
- `Endpoint` protocol — type-safe request descriptors with per-request auth override
- `AuthStrategy` — bearer token, API key, custom headers, or `.none` to skip auth
- `AuthInterceptor` — dynamic auth provider (async closure, for OAuth token refresh)
- `Interceptor` protocol — adapt requests and retry failures with full pipeline control
- `RetryInterceptor` + `RetryPolicy` — none, linear, or exponential backoff; standard preset
- `NexioConfig.retry` auto-installs `RetryInterceptor` on `configure(_:)`
- `NexioConfig.protocolClasses` — inject `URLProtocol` subclasses for testing
- `ImageLoader` actor — `URLCache`-backed remote image loading with prefetch and cache clear
- `NexioImage` SwiftUI view — drop-in `AsyncImage` replacement with caching, placeholder, failure view, transition
- `NexioError` — typed errors: `invalidURL`, `noInternet`, `timeout`, `unauthorized`, `notFound`, `serverError`, `decodingFailed`, `unknown`
- Top-level `nexioGet` / `nexioPost` convenience functions over `NexioClient.shared`
- Swift 6 strict concurrency compliance — zero data-race warnings
- MIT license, license headers on all source files
26 changes: 26 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Code of Conduct

## Our Pledge

We are committed to making participation in this project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.

## Our Standards

**Positive behaviour:**
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community

**Unacceptable behaviour:**
- Harassment, insults, or derogatory comments
- Publishing others' private information without permission
- Any conduct which could reasonably be considered inappropriate in a professional setting

## Enforcement

Instances of unacceptable behaviour may be reported by opening a GitHub issue or contacting the maintainer directly. All reports will be reviewed and investigated promptly.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
50 changes: 50 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Contributing to Nexio

Thank you for your interest in contributing. This document explains how to get started.

## Code of Conduct

Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).

## Getting Started

1. Fork the repository and clone your fork
2. Open `Package.swift` in Xcode or your preferred editor
3. Run the test suite to confirm everything passes before making changes

```bash
swift test
```

## Making Changes

- **Bugs** — open an issue first to confirm the behaviour is unintended
- **Features** — open an issue to discuss before spending time on an implementation
- **Docs / tests** — PRs welcome without prior issue

### Branch naming

```
fix/short-description
feat/short-description
docs/short-description
```

### Code style

- Swift 6 strict concurrency must stay clean — no `@unchecked Sendable` without a justifying comment
- No new external dependencies
- Public API changes require a doc comment update and a new test
- Run `swift build -c release` and `swift test` before opening a PR

## Pull Request Checklist

- [ ] `swift build -c release` passes with zero errors and zero warnings
- [ ] `swift test` passes (all platforms you can test)
- [ ] New public API has a doc comment
- [ ] `CHANGELOG.md` updated under `[Unreleased]`
- [ ] PR description explains *what* changed and *why*

## Reporting Issues

Use GitHub Issues. For security vulnerabilities, see [SECURITY.md](SECURITY.md).
21 changes: 21 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Security Policy

## Supported Versions

| Version | Supported |
|---------|-----------|
| 1.x | ✅ |

## Reporting a Vulnerability

**Do not open a public GitHub issue for security vulnerabilities.**

Please report security issues by emailing the maintainer directly (see GitHub profile) or by using [GitHub's private vulnerability reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability).

Include:
- A description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (optional)

You will receive a response within 72 hours. Once confirmed, a fix will be released and you will be credited in the changelog (unless you prefer otherwise).
5 changes: 4 additions & 1 deletion Sources/Nexio/Auth/AuthInterceptor.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

import Foundation

/// An ``Interceptor`` that injects authentication headers from a dynamic provider.
Expand Down Expand Up @@ -47,4 +50,4 @@ public struct AuthInterceptor: Interceptor {
) async -> Bool {
false // Auth interceptor does not handle retries
}
}
}
5 changes: 4 additions & 1 deletion Sources/Nexio/Auth/AuthStrategy.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

/// Authentication strategy applied to outgoing requests.
///
/// Pass to ``NexioClient/setAuth(_:)`` for global auth, or set
Expand All @@ -23,4 +26,4 @@ public enum AuthStrategy: Sendable {

/// Skips authentication entirely for this request.
case none
}
}
5 changes: 4 additions & 1 deletion Sources/Nexio/Core/Endpoint.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

import Foundation

/// HTTP methods supported by ``NexioClient``.
Expand Down Expand Up @@ -58,4 +61,4 @@ public extension Endpoint {
var queryItems: [URLQueryItem] { [] }
var body: (any Encodable)? { nil }
var auth: AuthStrategy? { nil }
}
}
5 changes: 4 additions & 1 deletion Sources/Nexio/Core/NexioClient.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

import Foundation

// MARK: - NexioClient
Expand Down Expand Up @@ -425,4 +428,4 @@ public func nexioPost<T: Decodable & Sendable>(
body: some Encodable & Sendable
) async throws -> T {
try await NexioClient.shared.post(url, body: body)
}
}
10 changes: 9 additions & 1 deletion Sources/Nexio/Core/NexioConfig.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

import Foundation

/// Configuration for ``NexioClient``.
Expand Down Expand Up @@ -51,7 +54,12 @@ public struct NexioConfig: Sendable {
/// Custom `URLProtocol` classes layered onto the session ahead of the
/// system defaults — primarily for intercepting traffic in tests (e.g.
/// stubbing responses with a `URLProtocol` subclass). `nil` by default.
public var protocolClasses: [AnyClass]?
///
/// - Note: `AnyClass` is not `Sendable`; this property is intentionally
/// exempt because protocol classes are always `NSObject` subclasses
/// whose class objects are global singletons — safe to share across
/// concurrency boundaries as long as callers pass only class literals.
public nonisolated(unsafe) var protocolClasses: [AnyClass]?

// MARK: - Init

Expand Down
5 changes: 4 additions & 1 deletion Sources/Nexio/Core/NexioRequest.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

import Foundation

/// A handle to an in-flight Nexio request.
Expand Down Expand Up @@ -34,4 +37,4 @@ private final class _TaskBox: @unchecked Sendable {
private let wrapped: URLSessionTask
init(_ task: URLSessionTask) { wrapped = task }
func cancel() { wrapped.cancel() }
}
}
5 changes: 4 additions & 1 deletion Sources/Nexio/Errors/NexioError.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

import Foundation

/// Typed errors surfaced by all Nexio operations.
Expand Down Expand Up @@ -52,4 +55,4 @@ public enum NexioError: Error, LocalizedError, Sendable {
return "Unknown error: \(err.localizedDescription)"
}
}
}
}
5 changes: 4 additions & 1 deletion Sources/Nexio/Image/ImageLoader.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

import Foundation

#if canImport(UIKit)
Expand Down Expand Up @@ -110,4 +113,4 @@ public actor ImageLoader {

private enum ImageDecodingError: Error {
case invalidImageData
}
}
5 changes: 4 additions & 1 deletion Sources/Nexio/Image/NexioImage.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

#if canImport(SwiftUI)
import SwiftUI

Expand Down Expand Up @@ -156,4 +159,4 @@ private extension Image {
}
}

#endif // canImport(SwiftUI)
#endif // canImport(SwiftUI)
5 changes: 4 additions & 1 deletion Sources/Nexio/Interceptor/Interceptor.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

import Foundation

/// A hook that can inspect and mutate requests before they are sent,
Expand Down Expand Up @@ -41,4 +44,4 @@ public protocol Interceptor: Sendable {
/// - attempt: Zero-based retry counter (0 = first failure).
/// - Returns: `true` to retry, `false` to propagate the error.
func retry(_ request: URLRequest, dueTo error: NexioError, attempt: Int) async -> Bool
}
}
5 changes: 4 additions & 1 deletion Sources/Nexio/Interceptor/RetryInterceptor.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

import Foundation

// MARK: - RetryPolicy
Expand Down Expand Up @@ -115,4 +118,4 @@ public struct RetryInterceptor: Interceptor {
return false
}
}
}
}
5 changes: 4 additions & 1 deletion Sources/Nexio/Internal/HTTPURLResponseExtensions.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) 2026 ANSCoder
// Licensed under the MIT License. See LICENSE in the project root for details.

import Foundation

extension HTTPURLResponse {
Expand All @@ -21,4 +24,4 @@ extension HTTPURLResponse {
return .serverError(statusCode: statusCode, data: data)
}
}
}
}
Loading
Loading