Refactor WeatherKit client to injectable interface

- Add WeatherKitClient interface and DefaultWeatherKitClient class
- WeatherKitDataSource accepts either client or credentials
- Simplify tests by injecting mock client directly
- Update fixture generation script to use new client class

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2026-01-17 01:14:18 +00:00
parent 850d1925b6
commit 6cf147989f
6 changed files with 6548 additions and 108 deletions

View File

@@ -3,42 +3,6 @@
import { type } from "arktype"
export async function fetchWeather(
options: WeatherKitClientOptions,
query: WeatherKitQueryOptions,
): Promise<WeatherKitResponse> {
const token = await generateJwt(options.credentials)
const dataSets = ["currentWeather", "forecastHourly", "forecastDaily", "weatherAlerts"].join(",")
const url = new URL(
`${WEATHERKIT_API_BASE}/weather/${query.language ?? "en"}/${query.lat}/${query.lng}`,
)
url.searchParams.set("dataSets", dataSets)
if (query.timezone) {
url.searchParams.set("timezone", query.timezone)
}
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) {
throw new Error(`WeatherKit API error: ${response.status} ${response.statusText}`)
}
const json = await response.json()
const result = weatherKitResponseSchema(json)
if (result instanceof type.errors) {
throw new Error(`WeatherKit API response validation failed: ${result.summary}`)
}
return result
}
export interface WeatherKitCredentials {
privateKey: string
keyId: string
@@ -46,10 +10,6 @@ export interface WeatherKitCredentials {
serviceId: string
}
export interface WeatherKitClientOptions {
credentials: WeatherKitCredentials
}
export interface WeatherKitQueryOptions {
lat: number
lng: number
@@ -57,6 +17,53 @@ export interface WeatherKitQueryOptions {
timezone?: string
}
export interface WeatherKitClient {
fetch(query: WeatherKitQueryOptions): Promise<WeatherKitResponse>
}
export class DefaultWeatherKitClient implements WeatherKitClient {
private readonly credentials: WeatherKitCredentials
constructor(credentials: WeatherKitCredentials) {
this.credentials = credentials
}
async fetch(query: WeatherKitQueryOptions): Promise<WeatherKitResponse> {
const token = await generateJwt(this.credentials)
const dataSets = ["currentWeather", "forecastHourly", "forecastDaily", "weatherAlerts"].join(
",",
)
const url = new URL(
`${WEATHERKIT_API_BASE}/weather/${query.language ?? "en"}/${query.lat}/${query.lng}`,
)
url.searchParams.set("dataSets", dataSets)
if (query.timezone) {
url.searchParams.set("timezone", query.timezone)
}
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) {
throw new Error(`WeatherKit API error: ${response.status} ${response.statusText}`)
}
const json = await response.json()
const result = weatherKitResponseSchema(json)
if (result instanceof type.errors) {
throw new Error(`WeatherKit API response validation failed: ${result.summary}`)
}
return result
}
}
export const Severity = {
Minor: "minor",
Moderate: "moderate",