Skip to content
Merged

V2 #42

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
13 changes: 7 additions & 6 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
{
"name": "Swift",
"image": "swift:5.10",
"image": "swift:6.1",
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": "false",
"username": "vscode",
"userUid": "1000",
"userGid": "1000",
"userUid": "1001",
"userGid": "1001",
"upgradePackages": "false"
},
"ghcr.io/devcontainers/features/git:1": {
"version": "os-provided",
"ppa": "false"
}
},
"ghcr.io/devcontainers/features/node:1": { }
},
"runArgs": [
"--cap-add=SYS_PTRACE",
Expand All @@ -29,15 +30,15 @@
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"sswg.swift-lang"
"swiftlang.swift-vscode"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "swift --version",
"postCreateCommand": "./scripts/post_create_container.sh",

// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
Expand Down
18 changes: 14 additions & 4 deletions .github/workflows/swift-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

name: Swift Ubuntu

on:
on:
push:
branches:
- main
Expand All @@ -25,11 +25,21 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Swift
- name: Install Dependencies
run: sudo apt-get -y install libcurl4-openssl-dev pkg-config python3-lldb-13

- name: Install Swiftly
run: |
curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz && \
tar zxf swiftly-$(uname -m).tar.gz && \
./swiftly init --skip-install --assume-yes

. ~/.local/share/swiftly/env.sh
echo "PATH=$PATH" >> $GITHUB_ENV

- name: Install Swift
run: |
curl -L https://swiftlang.github.io/swiftly/swiftly-install.sh | bash -s -- --disable-confirmation
swiftly install ${{ matrix.swift }}
swiftly use ${{ matrix.swift }}

- name: Get Swift version
run: swift --version
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.vscode
.claude*
6 changes: 6 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[tools]
swiftlint = "latest"
claude = "latest"

[settings]
experimental = true
76 changes: 76 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
included:
- Sources
- Package.swift

excluded:
- .build
- Submodules

opt_in_rules:
- array_init
- attributes
- closure_end_indentation
- closure_spacing
- empty_count
- explicit_init
- extension_access_modifier
- fatal_error_message
- first_where
- let_var_whitespace
- literal_expression_end_indentation
- nimble_operator
- operator_usage_whitespace
- overridden_super_call
- pattern_matching_keywords
- private_outlet
- prohibited_super_call
- redundant_nil_coalescing
- unneeded_parentheses_in_closure_argument
- vertical_parameter_alignment_on_call

disabled_rules:
- attributes
- multiple_closures_with_trailing_closure
- trailing_comma
- vertical_parameter_alignment_on_call
- void_return

custom_rules:
disable_print:
name: "print usage"
regex: "((\\bprint)|(Swift\\.print))\\s*\\("
message: "User app.logger or request.logger instead of print"
severity: warning

# Default Rule Configuration
type_name:
min_length: 2

identifier_name:
min_length: 2
allowed_symbols: "_"
excluded:
- x
- f
- i

file_length:
warning: 500
error: 1000

function_body_length: 50

line_length: 200

cyclomatic_complexity:
ignores_case_statements: true

large_tuple:
warning: 6
error: 10

nesting:
type_level:
warning: 2
function_level:
warning: 10
199 changes: 199 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

OpenWeatherKit is a Swift wrapper around the WeatherKit REST API, bringing native Swift WeatherKit functionality to platforms Apple doesn't currently support (particularly Linux). The API mirrors Apple's WeatherKit as closely as possible.

## Build and Test Commands

### Building
```bash
swift build
```

### Running Tests
```bash
swift test
```

### Running a Single Test
```bash
swift test --filter <TestClassName>.<testMethodName>
# Example: swift test --filter OpenWeatherKitTests.testWeather
```

### Platform-Specific Testing
The test suite has conditional compilation for Apple platforms vs Linux:
- Apple platforms: Tests include CoreLocation-based geocoding
- Linux: Tests require explicit countryCode and timezone parameters

## Architecture

### Core Request Flow

1. **WeatherService** (public API) - Entry point for all weather requests
- Accepts `LocationProtocol` (latitude/longitude)
- Variadic methods support 1-6 different `WeatherQuery<T>` datasets in a single call
- On Apple platforms: automatically geocodes location to get country code and timezone
- On Linux: requires explicit `countryCode` and `timezone` parameters

2. **NetworkClient** (internal) - HTTP layer
- Builds URLs via `Route` enum
- Handles JWT bearer token authentication
- Uses `URLSession` on Apple platforms, `AsyncHTTPClient` on Linux
- Fetches data in parallel using `TaskGroup` when multiple queries are requested
- Returns `WeatherProxy` which aggregates partial results

3. **WeatherProxy** (internal) - Result aggregation
- Container for all possible weather data types
- Supports combining multiple partial proxies (for parallel fetches)
- Maps from API models to public models

4. **WeatherQuery<T>** (public) - Type-safe dataset requests
- Generic query structure with static factory methods (`.current`, `.daily`, `.hourly`, etc.)
- Contains both the query type and a closure to extract results from `WeatherProxy`
- Supports updating with country codes for alerts/availability queries

### Platform Differences

**Apple Platforms** (`#if canImport(CoreLocation)`):
- Use `URLSession` for networking
- Include `Geocoder` for automatic country code/timezone resolution
- Support simplified API without explicit country code

**Linux** (`#if os(Linux)`):
- Use `AsyncHTTPClient` from swift-server
- Require explicit `countryCode` and `timezone` on all requests
- `WeatherService` includes `shutdown()` method to clean up HTTP client

### Data Flow

```
API Response (APIWeather/APIForecastDaily/etc.)
↓ (via extension +Map.swift files)
WeatherProxy (aggregates partial results)
↓ (via WeatherQuery.result closure)
Public Models (CurrentWeather/Forecast<DayWeather>/etc.)
```

### Internal vs Public Separation

- **Internal/Models**: API response models prefixed with `API*` (e.g., `APIWeather`, `APICurrentWeather`)
- **Internal/Extensions**: Mapping logic from API models to public models (`+Map.swift` files)
- **Public**: User-facing types matching Apple's WeatherKit API

### Key Protocols

- **LocationProtocol**: Abstraction for any type with `latitude` and `longitude`
- **Client**: Protocol for HTTP clients (implemented by `URLSession` wrapper and `AsyncHTTPClient` wrapper)

## Testing

Tests use a `MockClient` that returns predefined JSON responses from `MockData`. The mock client:
- Accepts an `Include` set to control which datasets to return
- Simulates API responses without real network calls
- On Apple platforms, uses `Geocoder.mock` for testing geocoding

## Dependencies

- **Swift 5.9+** minimum
- **No dependencies** on Apple platforms (uses `URLSession`)
- **AsyncHTTPClient** dependency on Linux (conditionally added via `#if os(Linux)` in Package.swift)

## JWT Authentication

The library does NOT generate JWTs. Users must provide a closure that returns a valid JWT string when initializing `WeatherService.Configuration`. The README recommends using Vapor's `jwt-kit` for JWT generation.

Required JWT claims:
- `exp` (expiration)
- `iat` (issued at)
- `iss` (issuer - Team ID)
- `sub` (subject - Service Identifier)
- Must be signed with ES256 using the private key from Apple Developer Portal
- Must include Key ID in header

## API Endpoint Structure

Base URL: `https://weatherkit.apple.com`

- Weather: `/api/v2/weather/{language}/{latitude}/{longitude}?dataSets=...&timezone=...`
- Availability: `/api/v2/availability/{latitude}/{longitude}?country={countryCode}`

## Date Handling

- JSON responses use Unix epoch timestamps (`secondsSince1970`)
- JSONDecoder configured with `.dateDecodingStrategy = .secondsSince1970`
- Date extensions provide utilities like `.hoursFromNow(24)` and `.daysFromNow(10)`

## Coding Conventions

- `@usableFromInline` on internal types/methods that are called from `@inlinable` public APIs
- Extensive use of `@inlinable` on public API surface for performance
- Sendable conformance throughout for Swift 6 compatibility
- TextCaseCoding for snake_case/camelCase conversion between API and Swift

## SwiftLint Integration

### Automatic Linting Hook

This repository includes a PostToolUse hook that automatically runs SwiftLint when Claude Code edits Swift files. The hook:
- Runs after every Edit or Write operation on Swift files
- Automatically fixes correctable violations (trailing whitespace, formatting issues, etc.)
- Reports remaining violations back to Claude for resolution
- Only processes Swift files in `Sources/`, `Tests/`, or `Package.swift`
- Completes within 15 seconds (timeout protection)

### Setup Requirements

**Required:** SwiftLint 0.50.0 or later must be installed:
```bash
brew install swiftlint
```

**Activation:** Copy the example settings to enable the hook:
```bash
cp .claude/settings.example.json .claude/settings.local.json
```

The hook is configured in `.claude/settings.local.json` (gitignored for local flexibility).

### Hook Behavior

**When you edit a Swift file:**
1. Hook receives the file path from Claude Code
2. Validates the file is a Swift file in target directories
3. Runs `swiftlint --fix` to auto-correct violations
4. Runs `swiftlint lint` to check for remaining violations
5. If violations remain: Reports them to Claude with file:line:column references and blocks the edit
6. If no violations: Completes silently

**Example violation report:**
```
SwiftLint found 2 violation(s) in Sources/OpenWeatherKit/Public/Weather.swift:
Sources/OpenWeatherKit/Public/Weather.swift:42:1: warning: Line should be 200 characters or less (line_length)
Sources/OpenWeatherKit/Public/Weather.swift:55:10: error: Print statement must not be used (custom_rules)
```

### Performance Impact

- Adds approximately 0.2-0.6 seconds per Swift file edit
- Runs only on Swift files (non-Swift files pass through instantly)
- Timeout protection prevents indefinite blocking

### Disabling the Hook

To disable SwiftLint integration:
```bash
rm .claude/settings.local.json
```

Or comment out the hook configuration in the settings file.

### Missing SwiftLint

If SwiftLint is not installed, the hook will:
- Exit gracefully with a warning message
- NOT block edits
- Suggest installation with `brew install swiftlint`
Loading