First off, thank you for considering contributing to NetworkLayer! It's people like you that make NetworkLayer such a great tool for network communication.
- Code of Conduct
- Getting Started
- How Can I Contribute?
- Development Workflow
- Coding Standards
- Community
This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to nv3212@gmail.com.
-
Fork the repository
# Click the "Fork" button on GitHub -
Clone your fork
git clone https://github.com/YOUR_USERNAME/network-layer.git cd network-layer -
Set up the development environment
# Bootstrap the project mise install -
Create a feature branch
git checkout -b feature/your-feature-name
-
Open the project in Xcode
open Package.swift
Before creating a bug report, please check the existing issues to avoid duplicates.
When creating a bug report, include:
- Clear title - Describe the issue concisely
- Reproduction steps - Detailed steps to reproduce the bug
- Expected behavior - What you expected to happen
- Actual behavior - What actually happened
- Environment - OS, Xcode version, Swift version
- Code samples - Minimal reproducible example
- Error messages - Complete error output if applicable
Example:
**Title:** RequestProcessor fails to refresh credentials on 401 response
**Steps to reproduce:**
1. Configure RequestProcessor with authentication interceptor
2. Make request with requiresAuthentication = true
3. Server returns 401
4. Observe that refresh is not triggered
**Expected:** Authentication interceptor should refresh credentials
**Actual:** Request fails without refresh attempt
**Environment:**
- iOS 16.0
- Xcode 14.3
- Swift 5.7
**Code:**
\`\`\`swift
let interceptor = MyAuthInterceptor()
let configuration = Configuration(
sessionConfiguration: .default,
interceptor: interceptor
)
let processor = NetworkLayerAssembly().assemble(configuration: configuration)
struct AuthRequest: IRequest {
var domainName: String { "https://api.example.com" }
var path: String { "secure/data" }
var httpMethod: HTTPMethod { .get }
var requiresAuthentication: Bool { true }
}
let response: Response<Data> = try await processor.send(AuthRequest())
\`\`\`We love feature suggestions! When proposing a new feature, include:
- Problem statement - What problem does this solve?
- Proposed solution - How should it work?
- Alternatives - What alternatives did you consider?
- Use cases - Real-world scenarios
- API design - Example code showing usage
- Breaking changes - Will this break existing code?
Example:
**Feature:** Add request/response interceptor chain
**Problem:** Currently only one authentication interceptor is supported. Complex apps need multiple interceptors for logging, analytics, error handling, etc.
**Solution:** Add interceptor chain that executes multiple interceptors in order.
**API:**
\`\`\`swift
let configuration = Configuration(
sessionConfiguration: .default,
interceptors: [
LoggingInterceptor(),
AuthenticationInterceptor(),
AnalyticsInterceptor()
]
)
\`\`\`
**Use case:** Mobile app needs to:
1. Log all requests/responses
2. Add authentication headers
3. Track API usage analytics
4. Handle rate limitingDocumentation improvements are always welcome:
- Code comments - Add/improve inline documentation
- DocC documentation - Enhance documentation articles
- README - Fix typos, add examples
- Guides - Write tutorials or how-to guides
- API documentation - Document public APIs
- Migration guides - Help users upgrade versions
- Check existing work - Look for related issues or PRs
- Discuss major changes - Open an issue for large features
- Follow coding standards - See Coding Standards
- Write tests - All code changes require tests
- Update documentation - Keep docs in sync with code
- Create a pull request - Use clear description
We use a simplified branching model:
main- Main development branch (all PRs target this)feature/*- New featuresfix/*- Bug fixesdocs/*- Documentation updatesrefactor/*- Code refactoringtest/*- Test improvements
Branch naming examples:
feature/interceptor-chain
fix/credential-refresh-timing
docs/update-authentication-guide
refactor/simplify-request-builder
test/add-retry-integration-testsWe use Conventional Commits for clear, structured commit history.
Format:
<type>(<scope>): <subject>
<body>
<footer>
Types:
feat- New featurefix- Bug fixdocs- Documentation changesstyle- Code style (formatting, no logic changes)refactor- Code refactoringtest- Adding or updating testschore- Maintenance tasksperf- Performance improvements
Scopes:
core- Core networking logicprocessor- RequestProcessorbuilder- RequestBuilderauth- Authenticationretry- Retry logicconfig- Configurationinterfaces- Protocol interfacesassembly- Dependency injection
Examples:
feat(auth): add interceptor chain support
Implement support for multiple interceptors that execute in sequence.
Each interceptor can modify requests and handle authentication challenges.
Closes #78
---
fix(processor): correct credential refresh detection
The refresh method was checking HTTP status codes incorrectly,
causing valid 401 responses to be ignored. Now properly delegates
to interceptor's isRequireRefresh method.
Fixes #92
---
docs(readme): add GraphQL client example
Add practical example showing how to use NetworkLayer with GraphQL APIs,
including query variables and error handling.
---
test(retry): add integration tests for retry policies
Add tests for:
- Retry with constant strategy
- Retry with exponential backoff
- Retry with custom shouldRetry evaluation
- Interaction between global and local retry evaluatorsCommit message rules:
- Use imperative mood ("add" not "added")
- Don't capitalize first letter
- No period at the end
- Keep subject line under 72 characters
- Separate subject from body with blank line
- Reference issues in footer
-
Update your branch
git checkout main git pull upstream main git checkout feature/your-feature git rebase main
-
Run tests and checks
# Run all tests swift test # Check test coverage swift test --enable-code-coverage # Run specific test suite swift test --filter NetworkLayerTests
-
Push to your fork
git push origin feature/your-feature
-
Create pull request
- Target the
mainbranch - Provide clear description
- Link related issues
- Include examples if applicable
- Request review from maintainers
- Target the
-
Review process
- Address review comments
- Keep PR up to date with main
- Squash commits if requested
- Wait for CI to pass
-
After merge
# Clean up local branch git checkout main git pull upstream main git branch -d feature/your-feature # Clean up remote branch git push origin --delete feature/your-feature
We follow the Swift API Design Guidelines and Ray Wenderlich Swift Style Guide.
Key points:
-
Naming
// ✅ Good func send<M: Decodable>(_ request: some IRequest) async throws -> Response<M> let requestProcessor: IRequestProcessor // ❌ Bad func doSend(_ req: Any) async throws -> Any let proc: Any
-
Protocols
// ✅ Good - Use "I" prefix for protocols protocol IRequestProcessor { func send<M: Decodable>(_ request: some IRequest) async throws -> Response<M> } protocol IAuthenticationInterceptor { func adapt(request: inout URLRequest, for session: URLSession) async throws } // ❌ Bad protocol RequestProcessor { } protocol Authenticator { }
-
Access Control
// ✅ Good - Explicit access control public actor RequestProcessor { private let configuration: Configuration private let session: URLSession private let requestBuilder: IRequestBuilder public func send<M: Decodable>( _ request: some IRequest ) async throws -> Response<M> { // Implementation } }
-
Documentation
/// An actor responsible for executing network requests in a thread-safe manner. /// /// `RequestProcessor` manages the entire lifecycle of a request, including construction, /// authentication adaptation, execution, credential refreshing, and retry logic. /// /// - Note: All operations are performed within the actor's isolation domain, /// ensuring thread-safe access to internal state. /// /// - Example: /// ```swift /// let processor = NetworkLayerAssembly().assemble() /// let response: Response<User> = try await processor.send(GetUserRequest()) /// print("Fetched user: \(response.value.name)") /// ``` public actor RequestProcessor: IRequestProcessor { // Implementation } -
Actor Usage
// ✅ Good - Use actors for thread-safe state management actor RequestProcessor { private let configuration: Configuration func performRequest() async throws -> Response<Data> { // Thread-safe access to configuration } } // ❌ Bad - Avoid @MainActor unless UI-related @MainActor class RequestProcessor { }
- No force unwrapping - Use optional binding or guards
- No force casting - Use conditional casting
- No magic numbers - Use named constants
- Single responsibility - One class, one purpose
- DRY principle - Don't repeat yourself
- SOLID principles - Follow SOLID design
- Actor isolation - Respect Swift concurrency boundaries
Example:
// ✅ Good
private enum NetworkConstants {
static let defaultTimeout: TimeInterval = 60
static let maxRetryAttempts = 3
}
guard let urlRequest = try requestBuilder.build(request, configure) else {
throw NetworkLayerError.badURL
}
// ❌ Bad
let timeout = 60.0 // Magic number
let request = try requestBuilder.build(request, configure)! // Force unwrapAll code changes must include tests:
- Unit tests - Test individual components
- Integration tests - Test component interactions
- Edge cases - Test boundary conditions
- Error handling - Test failure scenarios
- Concurrency tests - Test actor isolation and async behavior
- Mock tests - Use Mocker for network mocking
Coverage requirements:
- New code: minimum 80% coverage
- Modified code: maintain or improve existing coverage
- Critical paths: 100% coverage
Test structure:
import XCTest
@testable import NetworkLayer
import Mocker
final class RequestProcessorTests: XCTestCase {
var sut: RequestProcessor!
var configuration: Configuration!
var mockSession: URLSession!
override func setUp() {
super.setUp()
let mockConfiguration = URLSessionConfiguration.default
mockConfiguration.protocolClasses = [MockingURLProtocol.self]
configuration = Configuration(sessionConfiguration: mockConfiguration)
sut = NetworkLayerAssembly().assemble(configuration: configuration)
}
override func tearDown() {
sut = nil
configuration = nil
mockSession = nil
super.tearDown()
}
// MARK: - Success Tests
func testSend_WithValidRequest_ReturnsDecodedResponse() async throws {
// Given
let expectedUser = User(id: "123", name: "John Doe")
let requestURL = URL(string: "https://api.example.com/users/123")!
let mockedData = try JSONEncoder().encode(expectedUser)
let mock = Mock(url: requestURL, dataType: .json, statusCode: 200, data: [.get: mockedData])
mock.register()
struct GetUserRequest: IRequest {
var domainName: String { "https://api.example.com" }
var path: String { "users/123" }
var httpMethod: HTTPMethod { .get }
}
// When
let response: Response<User> = try await sut.send(GetUserRequest())
// Then
XCTAssertEqual(response.value.id, expectedUser.id)
XCTAssertEqual(response.value.name, expectedUser.name)
}
// MARK: - Failure Tests
func testSend_WithNetworkError_ThrowsError() async {
// Given
let requestURL = URL(string: "https://api.example.com/users/123")!
let mock = Mock(url: requestURL, dataType: .json, statusCode: 500, data: [.get: Data()])
mock.register()
struct GetUserRequest: IRequest {
var domainName: String { "https://api.example.com" }
var path: String { "users/123" }
var httpMethod: HTTPMethod { .get }
}
// Then
await XCTAssertThrowsError(
try await sut.send(GetUserRequest()) as Response<User>
)
}
// MARK: - Authentication Tests
func testSend_WithAuthentication_AdaptsRequest() async throws {
// Given
let token = "test-token"
let interceptor = MockAuthInterceptor(token: token)
let config = Configuration(
sessionConfiguration: .default,
interceptor: interceptor
)
sut = NetworkLayerAssembly().assemble(configuration: config)
struct AuthRequest: IRequest {
var domainName: String { "https://api.example.com" }
var path: String { "secure" }
var httpMethod: HTTPMethod { .get }
var requiresAuthentication: Bool { true }
}
// When/Then
// Verify interceptor was called with correct parameters
}
// MARK: - Retry Tests
func testSend_WithRetryPolicy_RetriesOnFailure() async throws {
// Given
let retryService = RetryPolicyService(
strategy: .constant(retry: 2, duration: .milliseconds(100))
)
sut = NetworkLayerAssembly().assemble(
configuration: configuration,
retryPolicyService: retryService
)
var attemptCount = 0
// When/Then
// Verify retry behavior
}
// MARK: - Concurrency Tests
func testSend_WithConcurrentRequests_HandlesAllRequests() async throws {
// Given
let requestCount = 10
// When
let responses = try await withThrowingTaskGroup(of: Response<User>.self) { group in
for i in 0..<requestCount {
group.addTask {
try await self.sut.send(GetUserRequest(id: "\(i)"))
}
}
var results: [Response<User>] = []
for try await response in group {
results.append(response)
}
return results
}
// Then
XCTAssertEqual(responses.count, requestCount)
}
}
// MARK: - Test Helpers
private struct User: Codable, Equatable {
let id: String
let name: String
}
private class MockAuthInterceptor: IAuthenticationInterceptor {
let token: String
init(token: String) {
self.token = token
}
func adapt(request: inout URLRequest, for session: URLSession) async throws {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool {
response.statusCode == 401
}
func refresh(
_ request: URLRequest,
with response: HTTPURLResponse,
for session: URLSession
) async throws {
// Mock refresh logic
}
}- Discussions - Join GitHub Discussions
- Issues - Track open issues
- Pull Requests - Review open PRs
Contributors are recognized in:
- GitHub contributors page
- Release notes
- Project README (for significant contributions)
- Check existing issues
- Search discussions
- Ask in Q&A discussions
- Email the maintainer: nv3212@gmail.com
Thank you for contributing to NetworkLayer! 🎉
Your efforts help make network communication better for everyone.