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
20 changes: 5 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Category A (tools-version 6.2 + macOS 26 min): macos-26 + Swift 6.2
# Category B (tools-version 6.2 + older macOS): macos-15 + 6.2, macos-26 + 6.2
# Category C (tools-version 6.0): macos-15 + 6.0, macos-15 + 6.2, macos-26 + 6.2
# Linux (if viable): ubuntu + Swift 6.0, ubuntu + Swift 6.2
# Linux: ubuntu + Swift 6.3
#
# When Swift 6.3 ships: bump 6.0→6.1 and 6.2→6.3 in Category C
# When bumping tools-version to 6.2: drop 6.0/6.1, move to Category A or B
Expand All @@ -29,6 +29,8 @@ jobs:
include:
- os: macos-26
swift: "6.2"
- os: ubuntu-latest
swift: "6.3"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
Expand All @@ -39,24 +41,12 @@ jobs:
run: swift build -v
- name: Test
run: swift test -v
test-linux:
name: Test on Linux
runs-on: ubuntu-latest
container:
image: swift:6.2
steps:
- uses: actions/checkout@v6
- name: Build
run: swift build -v
- name: Test
run: swift test -v
lint:
name: Run SwiftLint
runs-on: macos-latest
runs-on: ubuntu-latest
container: ghcr.io/realm/swiftlint:latest
steps:
- uses: actions/checkout@v6
- name: Install SwiftLint
run: brew install swiftlint
- name: Run SwiftLint
run: swiftlint --strict
swift-format:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ concurrency:
jobs:
build:
name: Generate Documentation
runs-on: macos-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: SwiftyLab/setup-swift@latest
Expand Down
24 changes: 11 additions & 13 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,15 @@ let package = Package(
swiftLanguageModes: [.v5, .v6]
)

#if os(macOS)
package.targets.append(
.executableTarget(
name: "SwiftDOF_E2E",
dependencies: [
"SwiftDOF",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
.product(name: "Progress", package: "Progress.swift")
],
swiftSettings: approachableConcurrency
)
package.targets.append(
.executableTarget(
name: "SwiftDOF_E2E",
dependencies: [
"SwiftDOF",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
.product(name: "Progress", package: "Progress.swift")
],
swiftSettings: approachableConcurrency
)
#endif
)
20 changes: 20 additions & 0 deletions Sources/SwiftDOF/LinuxLocalization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// `String(localized:)` comes from FoundationInternationalization on Apple platforms but
// is unavailable on Linux. This package uses its default ("en") localization text as the
// lookup key, so on Linux we resolve each key to itself. Excluded on Apple, where the
// real Foundation API is used.

#if !canImport(Darwin)
import Foundation

extension String {
init(
localized key: String,
table _: String? = nil,
bundle _: Bundle? = nil,
locale _: Locale? = nil,
comment _: StaticString? = nil
) {
self = key
}
}
#endif
162 changes: 43 additions & 119 deletions Sources/SwiftDOF/Parser/DOFError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,156 +44,80 @@ public enum DOFError: Error, LocalizedError, Sendable {
public var errorDescription: String? {
switch self {
case .invalidEncoding:
#if canImport(Darwin)
String(localized: "File encoding was invalid.", bundle: .module)
#else
"File encoding was invalid."
#endif
String(localized: "File encoding was invalid.", bundle: .module)
case .invalidFormat:
#if canImport(Darwin)
String(localized: "DOF data format was invalid.", bundle: .module)
#else
"DOF data format was invalid."
#endif
String(localized: "DOF data format was invalid.", bundle: .module)
case .parseError:
#if canImport(Darwin)
String(localized: "DOF data could not be parsed.", bundle: .module)
#else
"DOF data could not be parsed."
#endif
String(localized: "DOF data could not be parsed.", bundle: .module)
case .fileNotFound:
#if canImport(Darwin)
String(localized: "DOF file was not found.", bundle: .module)
#else
"DOF file was not found."
#endif
String(localized: "DOF file was not found.", bundle: .module)
case .streamError:
#if canImport(Darwin)
String(localized: "A stream error occurred.", bundle: .module)
#else
"A stream error occurred."
#endif
String(localized: "A stream error occurred.", bundle: .module)
case .lineTooShort:
#if canImport(Darwin)
String(localized: "Line was too short.", bundle: .module)
#else
"Line was too short."
#endif
String(localized: "Line was too short.", bundle: .module)
}
}

public var failureReason: String? {
switch self {
case .invalidEncoding:
#if canImport(Darwin)
String(localized: "The file is not valid Latin-1 encoding.", bundle: .module)
#else
"The file is not valid Latin-1 encoding."
#endif
String(localized: "The file is not valid Latin-1 encoding.", bundle: .module)
case .invalidFormat(let error):
switch error {
case .missingCurrencyDate:
#if canImport(Darwin)
String(localized: "The DOF header does not contain a currency date.", bundle: .module)
#else
"The DOF header does not contain a currency date."
#endif
String(localized: "The DOF header does not contain a currency date.", bundle: .module)
case .currencyDateHeaderNotFound:
#if canImport(Darwin)
String(
localized: "The “CURRENCY DATE = ” pattern was not found in the header.",
bundle: .module
)
#else
"The “CURRENCY DATE = ” pattern was not found in the header."
#endif
String(
localized: "The “CURRENCY DATE = ” pattern was not found in the header.",
bundle: .module
)
case .invalidCurrencyDateFormat:
#if canImport(Darwin)
String(localized: "The currency date is not in MM/DD/YY format.", bundle: .module)
#else
"The currency date is not in MM/DD/YY format."
#endif
String(localized: "The currency date is not in MM/DD/YY format.", bundle: .module)
case .invalidCurrencyDateComponents:
#if canImport(Darwin)
String(
localized: "The currency date contains non-numeric components.",
bundle: .module
)
#else
"The currency date contains non-numeric components."
#endif
String(
localized: "The currency date contains non-numeric components.",
bundle: .module
)
case .invalidLatitudeDirection(let char):
#if canImport(Darwin)
String(
localized: "Latitude direction “\(String(char))” is invalid. Expected “N” or “S”.",
bundle: .module
)
#else
"Latitude direction “\(String(char))” is invalid. Expected “N” or “S”."
#endif
String(
localized: "Latitude direction “\(String(char))” is invalid. Expected “N” or “S”.",
bundle: .module
)
case .invalidLongitudeDirection(let char):
#if canImport(Darwin)
String(
localized: "Longitude direction “\(String(char))” is invalid. Expected “E” or “W”.",
bundle: .module
)
#else
"Longitude direction “\(String(char))” is invalid. Expected “E” or “W”."
#endif
String(
localized: "Longitude direction “\(String(char))” is invalid. Expected “E” or “W”.",
bundle: .module
)
}
case let .parseError(field, value, line):
#if canImport(Darwin)
String(localized: "Failed to parse \(field) “\(value)” at line \(line).", bundle: .module)
#else
"Failed to parse \(field) “\(value)” at line \(line)."
#endif
String(localized: "Failed to parse \(field) “\(value)” at line \(line).", bundle: .module)
case .fileNotFound(let url):
#if canImport(Darwin)
String(localized: "The file at “\(url.path)” could not be found.", bundle: .module)
#else
"The file at “\(url.path)” could not be found."
#endif
String(localized: "The file at “\(url.path)” could not be found.", bundle: .module)
case .streamError(let error):
#if canImport(Darwin)
String(
localized: "An error occurred while reading: \(error.localizedDescription)",
bundle: .module
)
#else
"An error occurred while reading: \(error.localizedDescription)"
#endif
String(
localized: "An error occurred while reading: \(error.localizedDescription)",
bundle: .module
)
case let .lineTooShort(expected, actual, line):
#if canImport(Darwin)
String(
localized: "Line \(line) has \(actual) characters but \(expected) are required.",
bundle: .module
)
#else
"Line \(line) has \(actual) characters but \(expected) are required."
#endif
String(
localized: "Line \(line) has \(actual) characters but \(expected) are required.",
bundle: .module
)
}
}

public var recoverySuggestion: String? {
switch self {
case .invalidEncoding, .invalidFormat:
#if canImport(Darwin)
String(
localized: "Verify the file is a valid FAA Digital Obstacle File.",
bundle: .module
)
#else
"Verify the file is a valid FAA Digital Obstacle File."
#endif
String(
localized: "Verify the file is a valid FAA Digital Obstacle File.",
bundle: .module
)
case .fileNotFound:
#if canImport(Darwin)
String(
localized: "Verify that the file exists and has not been moved or deleted.",
bundle: .module
)
#else
"Verify that the file exists and has not been moved or deleted."
#endif
String(
localized: "Verify that the file exists and has not been moved or deleted.",
bundle: .module
)
case .parseError, .streamError, .lineTooShort:
nil
}
Expand Down
56 changes: 38 additions & 18 deletions Sources/SwiftDOF_E2E/DOFDataLoader.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import SwiftDOF

/// Protocol for loading DOF data from various sources.
Expand Down Expand Up @@ -63,26 +66,43 @@ struct URLZipLoader: DOFDataLoader {
struct URLStreamLoader: DOFDataLoader {
let url: URL

func load(
progressHandler: @Sendable (Progress) -> Void,
errorCallback: @escaping (Error, Int) -> Void
) async throws -> DOF {
let (bytes, response) = try await URLSession.shared.bytes(from: url)
// `URLSession.bytes(from:)` isn’t available in FoundationNetworking, so Linux falls back to
// buffering the whole response and parsing it synchronously instead of streaming.
#if canImport(FoundationNetworking)
func load(
progressHandler: @Sendable (Progress) -> Void,
errorCallback: @escaping (Error, Int) -> Void
) async throws -> DOF {
let (data, response) = try await URLSession.shared.data(from: url)

if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
throw DOFDataLoaderError.downloadFailed(statusCode: httpResponse.statusCode)
}

// Get content length for progress tracking
let contentLength = (response as? HTTPURLResponse)?.expectedContentLength ?? -1
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
throw DOFDataLoaderError.downloadFailed(statusCode: httpResponse.statusCode)
}

return try await DOF(
bytes: bytes,
totalBytes: contentLength > 0 ? contentLength : nil,
progressHandler: progressHandler,
errorCallback: errorCallback
)
}
return try DOF(data: data, progressHandler: progressHandler, errorCallback: errorCallback)
}
#else
func load(
progressHandler: @Sendable (Progress) -> Void,
errorCallback: @escaping (Error, Int) -> Void
) async throws -> DOF {
let (bytes, response) = try await URLSession.shared.bytes(from: url)

if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
throw DOFDataLoaderError.downloadFailed(statusCode: httpResponse.statusCode)
}

// Get content length for progress tracking
let contentLength = (response as? HTTPURLResponse)?.expectedContentLength ?? -1

return try await DOF(
bytes: bytes,
totalBytes: contentLength > 0 ? contentLength : nil,
progressHandler: progressHandler,
errorCallback: errorCallback
)
}
#endif
}

// MARK: - Errors
Expand Down
Loading
Loading