diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 972693f..df2fdb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,22 +11,27 @@ jobs: name: Swift ${{ matrix.swift }} on Ubuntu runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - swift: ["5.9", "5.10"] + swift: ["5.9", "5.10", "6.0", "6.1", "6.2", "6.3", "6.4"] container: image: swift:${{ matrix.swift }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Build run: swift build - name: Test run: swift test test-macos: - name: Swift on macOS - runs-on: macos-14 + name: Swift on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-15, macos-26] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Build run: swift build - name: Test diff --git a/README.md b/README.md index 9d6bbce..5c8fc52 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ The primary response type for IP lookups. | `city` | `String?` | City name | | `region` | `String?` | Region / state | | `regionCode` | `String?` | Region code | +| `regionType` | `String?` | Kind of region (for example `state` or `province`) | | `countryName` | `String?` | Country name | | `countryCode` | `String?` | ISO 3166-1 alpha-2 code | | `continentName` | `String?` | Continent name | @@ -205,12 +206,13 @@ The primary response type for IP lookups. | `callingCode` | `String?` | International calling code | | `flag` | `String?` | Flag image URL | | `emojiFlag` | `String?` | Flag emoji | +| `emojiUnicode` | `String?` | Flag emoji as Unicode code points (for example `U+1F1FA U+1F1F8`) | | `asn` | `ASN?` | Autonomous system info | | `company` | `Company?` | Organization that owns the IP | | `carrier` | `Carrier?` | Mobile carrier info | | `languages` | `[Language]?` | Languages spoken in the country | | `currency` | `Currency?` | Local currency | -| `timeZone` | `TimeZone?` | Time zone details | +| `timeZone` | `TimeZoneInfo?` | Time zone details | | `threat` | `Threat?` | Threat intelligence data | | `count` | `Int?` | API requests in last 24 hours | @@ -218,6 +220,10 @@ The primary response type for IP lookups. Detailed ASN data from the dedicated `/asn` endpoint. Includes `ipv4Prefixes`, `ipv6Prefixes`, `numIPs`, `upstream`, `downstream`, and `peers`. +### TimeZoneInfo + +Time zone of the IP address location: `name`, `abbr`, `offset`, `isDST` and `currentTime`. The type is called `TimeZoneInfo` so that it does not shadow Foundation's `TimeZone`. + ### Threat Threat intelligence flags: `isTor`, `isVPN`, `isICloudRelay`, `isProxy`, `isDatacenter`, `isAnonymous`, `isKnownAttacker`, `isKnownAbuser`, `isThreat`, `isBogon`. Includes `blocklists` and reputation `scores` (0-100 for VPN, proxy, threat, and trust). diff --git a/Sources/IPData/IPDataClient.swift b/Sources/IPData/IPDataClient.swift index 5963b00..f86b750 100644 --- a/Sources/IPData/IPDataClient.swift +++ b/Sources/IPData/IPDataClient.swift @@ -134,11 +134,9 @@ public final class IPDataClient: @unchecked Sendable { /// - fields: Optional list of fields to return. /// - Returns: An array of ``IPResponse`` results, one per IP. public func bulkLookup(_ ips: [String], fields: [String]? = nil) async throws -> [IPResponse] { - var url = buildURL(path: "/bulk", fields: fields) - // For bulk, the api-key must be a query parameter - if !url.absoluteString.contains("api-key") { - url = appendAPIKey(to: url) - } + // For bulk, the api-key must be a query parameter; `buildURL` adds it to + // every URL, so nothing else is needed here. + let url = buildURL(path: "/bulk", fields: fields) var request = URLRequest(url: url) request.httpMethod = "POST" @@ -177,14 +175,6 @@ public final class IPDataClient: @unchecked Sendable { return components.url! } - private func appendAPIKey(to url: URL) -> URL { - var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! - var items = components.queryItems ?? [] - items.append(URLQueryItem(name: "api-key", value: apiKey)) - components.queryItems = items - return components.url! - } - private func fetch(_ url: URL) async throws -> T { var request = URLRequest(url: url) request.setValue(apiKey, forHTTPHeaderField: "api-key") @@ -254,7 +244,11 @@ public final class IPDataClient: @unchecked Sendable { // MARK: - Linux Async Compatibility -#if canImport(FoundationNetworking) +// swift-corelibs-foundation ships the async `URLSession.data(for:delegate:)` from +// Swift 6.0 on; the 5.9 and 5.10 toolchains on Linux lack it and use this shim instead. +// It is compiled only for those toolchains so that it never sits next to the native +// method. +#if canImport(FoundationNetworking) && compiler(<6.0) extension URLSession { func data(for request: URLRequest) async throws -> (Data, URLResponse) { try await withCheckedThrowingContinuation { continuation in diff --git a/Tests/IPDataTests/IPDataClientTests.swift b/Tests/IPDataTests/IPDataClientTests.swift index 2cdf3a5..7684bae 100644 --- a/Tests/IPDataTests/IPDataClientTests.swift +++ b/Tests/IPDataTests/IPDataClientTests.swift @@ -222,6 +222,21 @@ final class IPDataClientTests: XCTestCase { XCTAssertEqual(results[1].ip, "8.8.8.8") } + func testBulkLookupSendsTheAPIKeyOnceAsAQueryItem() async throws { + MockURLProtocol.requestHandler = { request in + let components = URLComponents(url: request.url!, resolvingAgainstBaseURL: false)! + let items = components.queryItems ?? [] + let apiKeyItems = items.filter { $0.name == "api-key" } + XCTAssertEqual(apiKeyItems.count, 1, "Query: \(components.query ?? "")") + XCTAssertEqual(apiKeyItems.first?.value, "test-api-key") + XCTAssertTrue(items.contains(where: { $0.name == "fields" && $0.value == "ip" })) + return okResponse(for: request, json: #"[{"ip": "1.1.1.1"}]"#) + } + + _ = try await client.bulkLookup(["1.1.1.1"], fields: ["ip"]) + XCTAssertEqual(MockURLProtocol.requestCount, 1) + } + // MARK: - Connection Reuse func testConsecutiveRequestsGoThroughTheSameInjectedSession() async throws {