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
71 changes: 61 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,70 @@
name: Build
name: CI

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

jobs:
build:
permissions:
contents: read

jobs:
build-macos:
runs-on: macos-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v4

- name: Cache Swift packages
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-${{ hashFiles('Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-

- name: Build
run: swift build -v

- name: Run tests
run: swift test -v

- name: Upload test results on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results-macos
path: .build/debug/*Tests*
retention-days: 5

build-linux:
runs-on: ubuntu-latest
timeout-minutes: 15
container: swift:6.0

steps:
- uses: actions/checkout@v2
- name: Build
run: swift build -v
- name: Run tests
run: swift test -v
- uses: actions/checkout@v4

- name: Cache Swift packages
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-${{ hashFiles('Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-

- name: Build
run: swift build -v

- name: Run tests
run: swift test -v

- name: Upload test results on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results-linux
path: .build/debug/*Tests*
retention-days: 5
21 changes: 14 additions & 7 deletions .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
name: Documentation

on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Runs on releases
release:
types: [published]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
Expand All @@ -26,14 +26,21 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Pages
uses: actions/configure-pages@v3
uses: actions/configure-pages@v5
- name: Cache Swift packages
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-${{ hashFiles('Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- name: Generate Docs
run: |
xcodebuild docbuild -scheme Endpoints -destination generic/platform=iOS OTHER_DOCC_FLAGS="--transform-for-static-hosting --hosting-base-path Endpoints --output-path docs"
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
uses: actions/upload-pages-artifact@v3
with:
path: ./docs

Expand All @@ -49,4 +56,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
uses: actions/deploy-pages@v4
10 changes: 10 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/Endpoints.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EndpointsMockingTests"
BuildableName = "EndpointsMockingTests"
BlueprintName = "EndpointsMockingTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
59 changes: 45 additions & 14 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,50 @@
// swift-tools-version:5.5
// swift-tools-version:6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

// Conditional targets based on platform
#if os(Linux)
let products: [Product] = [
.library(
name: "Endpoints",
targets: ["Endpoints"]),
]

let targets: [Target] = [
.target(
name: "Endpoints",
dependencies: []),
.testTarget(
name: "EndpointsTests",
dependencies: ["Endpoints"]),
]
#else
let products: [Product] = [
.library(
name: "Endpoints",
targets: ["Endpoints"]),
.library(
name: "EndpointsMocking",
targets: ["EndpointsMocking"]),
]

let targets: [Target] = [
.target(
name: "Endpoints",
dependencies: []),
.target(
name: "EndpointsMocking",
dependencies: ["Endpoints"]),
.testTarget(
name: "EndpointsTests",
dependencies: ["Endpoints"]),
.testTarget(
name: "EndpointsMockingTests",
dependencies: ["EndpointsMocking"]),
]
#endif

let package = Package(
name: "Endpoints",
platforms: [
Expand All @@ -11,17 +53,6 @@ let package = Package(
.macOS(.v10_15),
.watchOS(.v6)
],
products: [
.library(
name: "Endpoints",
targets: ["Endpoints"]),
],
targets: [
.target(
name: "Endpoints",
dependencies: []),
.testTarget(
name: "EndpointsTests",
dependencies: ["Endpoints"]),
]
products: products,
targets: targets
)
133 changes: 125 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,49 @@ Endpoints is a small library for creating statically and strongly-typed definiti

The purpose of Endpoints is to, in a type-safe way, define how to create a `URLRequest` from typed properties and, additionally, define how a response for the request should be handled. The library not only includes the ability to create these requests in a type-safe way, but also includes helpers to perform the requests using `URLSession`. Endpoints does not try to wrap the URL loading system to provide features on top of it like Alamofire. Instead, Endpoints focuses on defining endpoints and associated data to produce a request as a `URLRequest` object to be plugged into vanilla `URLSession`s. However, this library could be used in conjunction with Alamofire if desired.

## Features

- **Type-safe endpoint definitions** - Define endpoints with compile-time checking of paths, parameters, and headers
- **Server definition with multiple environments** - Support for local, development, staging, and production environments with easy switching
- **Built-in mocking support** - Comprehensive testing utilities through the `EndpointsMocking` module
- **Swift 6.0 compatible** - Built with modern Swift concurrency and Sendable support
- **Combine and async/await support** - Use either reactive or async patterns

## Getting Started

The basic process for defining an Endpoint starts with defining a value conforming to `Endpoint`. With the `Endpoint` protocol, you are encapsulating the definition of the endpoint, all the properties that are plugged into the definition and the types for parsing the response. Within the `Endpoint`, the `definition` static var serves as an immutable definition of the server's endpoint and how the variable pieces of the `Endpoint` should fit together when making the full request.

To get started, first create a type (struct or class) conforming to `Endpoint`. There are only two required elements to conform: defining the `Response` and creating the ``Definition``.
### Defining a Server

`Endpoints` and `Definitions` do not contain base URLs so that these requests can be used on different environments. Environments are defined as conforming to the `EnvironmentType` and implement a `baseURL` as well as an optional `requestProcessor` which has a final hook before `URLRequest` creation to modify the `URLRequest` to attach authentication or signatures.
First, define a server that conforms to `ServerDefinition`. This encapsulates your base URLs for different environments:

To find out more about the pieces of the `Endpoint`, check out [Defining a ResponseType](https://github.com/velos/Endpoints/wiki/DefiningResponseType) on the wiki.
```swift
import Endpoints
import Foundation

## Examples
struct ApiServer: ServerDefinition {
var baseUrls: [Environments: URL] {
return [
.local: URL(string: "https://local-api.example.com")!,
.staging: URL(string: "https://staging-api.example.com")!,
.production: URL(string: "https://api.example.com")!
]
}

static var defaultEnvironment: Environments { .production }
}
```

To get started, first create a type (struct or class) conforming to `Endpoint`. There are only two required elements to conform: defining the `Response` and creating the `Definition`.

The most basic example of defining an Endpoint is creating a simple GET request. This means defining a type that conforms to `Endpoint` such as:
`Endpoints` and `Definitions` now include server information, eliminating the need to pass environments at call time. Servers implement a `requestProcessor` which has a final hook before `URLRequest` creation to modify the `URLRequest` to attach authentication or signatures.

### Basic Endpoint Example

```Swift
struct MyEndpoint: Endpoint {
typealias Server = ApiServer

static let definition: Definition<MyEndpoint> = Definition(
method: .get,
path: "path/to/resource"
Expand All @@ -36,13 +63,13 @@ struct MyEndpoint: Endpoint {
}
```

This includes a `Response` associated type (can be typealiased to a more complex existing type) which defines how the response will come back from the request.
This includes a `Response` associated type (can be typealiased to a more complex existing type) which defines how the response will come back from the request. The server is specified via `typealias Server = ApiServer`.

Then usage can employ the `URLSession` extensions:

#### Usage
```Swift
URLSession.shared.endpointPublisher(in: .production, with: MyEndpoint())
URLSession.shared.endpointPublisher(with: MyEndpoint())
.sink { completion in
guard case .failure(let error) = completion else { return }
// handle error
Expand All @@ -52,4 +79,94 @@ URLSession.shared.endpointPublisher(in: .production, with: MyEndpoint())
.store(in: &cancellables)
```

To browse more complex examples, make sure to check out the [Examples](https://github.com/velos/Endpoints/wiki/Examples) wiki page.
Notice that the API no longer requires passing an environment - it's handled automatically by the server definition.

### Async/Await

```swift
do {
let response = try await URLSession.shared.response(with: MyEndpoint())
// handle response
} catch {
// handle error
}
```

## Testing with EndpointsMocking

Endpoints includes a comprehensive mocking system through the `EndpointsMocking` module:

```swift
import Testing
import Endpoints
import EndpointsMocking

@Test func testMyEndpoint() async throws {
try await withMock(MyEndpoint.self, action: .return(.init(resourceId: "123", resourceName: "Test"))) {
let response = try await URLSession.shared.response(with: MyEndpoint())
#expect(response.resourceId == "123")
}
}
```

The mocking system supports:
- Returning successful responses
- Returning error responses
- Throwing network errors
- Dynamic response generation
- Combine publisher mocking

To find out more about the pieces of the `Endpoint`, check out [Defining a ResponseType](https://github.com/velos/Endpoints/wiki/DefiningResponseType) on the wiki.

## Examples

To browse more complex examples, make sure to check out the [Examples](https://github.com/velos/Endpoints/wiki/Examples) wiki page or the documentation in Xcode.

## Requirements

- Swift 6.0+
- iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+

## Installation

### Swift Package Manager

Add the following to your `Package.swift`:

```swift
dependencies: [
.package(url: "https://github.com/velos/Endpoints.git", from: "2.0.0")
]
```

For testing, also add:

```swift
testTarget(
name: "YourTests",
dependencies: ["Endpoints", "EndpointsMocking"]
)
```

## Documentation

Full documentation is available in Xcode (Product > Build Documentation) and includes:
- API reference for all types
- Comprehensive examples
- Mocking guide
- Best practices

## Migration from 0.4.0

If you're upgrading from version 0.4.0 or earlier, the main changes are:

1. **ServerDefinition replaces EnvironmentType** - Define your environments in a `ServerDefinition` conforming type
2. **No more environment parameter** - Remove `in: .production` from all API calls
3. **Add Server typealias** - Add `typealias Server = YourServer` to your endpoints
4. **Swift 6.0 required** - Update your Swift toolchain

See the [Migration Guide](https://github.com/velos/Endpoints/wiki/Migration) for detailed instructions.

## License

Endpoints is released under the MIT license. See LICENSE for details.
4 changes: 4 additions & 0 deletions Sources/Endpoints/Definition+URLResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

import Foundation

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public extension Definition {

/// Converts data, response and error into a Result type by processing data and throwing errors based on response codes and response data.
Expand Down
Loading