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
15 changes: 10 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -205,19 +206,24 @@ 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 |

### ASNDetail

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).
Expand Down
22 changes: 8 additions & 14 deletions Sources/IPData/IPDataClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<T: Decodable>(_ url: URL) async throws -> T {
var request = URLRequest(url: url)
request.setValue(apiKey, forHTTPHeaderField: "api-key")
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions Tests/IPDataTests/IPDataClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading