Skip to content

Commit

Permalink
Use URL in JSON/Data APIs (#588)
Browse files Browse the repository at this point in the history
* Use URL in JSON/Data APIs

* path -> URL

* Use fileSystemPath instead of path(percentEncoded:)

* Correct resource loading on linux
  • Loading branch information
jmschonfeld committed May 8, 2024
1 parent d88c245 commit 543a9e4
Show file tree
Hide file tree
Showing 8 changed files with 33 additions and 96 deletions.
29 changes: 4 additions & 25 deletions Sources/FoundationEssentials/Data/Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2069,7 +2069,6 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect
}
#endif

#if FOUNDATION_FRAMEWORK
/// Initialize a `Data` with the contents of a `URL`.
///
/// - parameter url: The `URL` to read.
Expand All @@ -2083,20 +2082,17 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect
if url.isFileURL {
self = try readDataFromFile(path: .url(url), reportProgress: true, options: options)
} else {
#if FOUNDATION_FRAMEWORK
// Fallback to NSData, to read via NSURLSession
let d = try NSData(contentsOf: url, options: NSData.ReadingOptions(rawValue: options.rawValue))
self.init(referencing: d)
#else
throw CocoaError(.fileReadUnsupportedScheme)
#endif
}
#endif
}
#else
/// Temporary usage, until `URL` is ported. Non-framework only. Same as of `contentsOfFile:options:`.
public init(contentsOf path: String, options: ReadingOptions = []) throws {
self = try readDataFromFile(path: .path(path), reportProgress: true, options: options)
}
#endif

#if FOUNDATION_FRAMEWORK
internal init(contentsOfFile path: String, options: ReadingOptions = []) throws {
#if NO_FILESYSTEM
let d = try NSData(contentsOfFile: path, options: NSData.ReadingOptions(rawValue: options.rawValue))
Expand All @@ -2105,12 +2101,6 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect
self = try readDataFromFile(path: .path(path), reportProgress: true, options: options)
#endif
}
#else
/// Temporary usage, until `URL` is ported. Non-framework only.
public init(contentsOfFile path: String, options: ReadingOptions = []) throws {
self = try readDataFromFile(path: .path(path), reportProgress: true, options: options)
}
#endif

// -----------------------------------
// MARK: - Properties and Functions
Expand Down Expand Up @@ -2448,17 +2438,6 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect
throw CocoaError(.featureUnsupported)
#endif
}

#if !FOUNDATION_FRAMEWORK
// We don't intend for this to be long-term API, preferring URL. But for now, this allows us to provide the functionality until URL is fully ported.
public func write(to path: String, options: Data.WritingOptions = []) throws {
if options.contains(.withoutOverwriting) && options.contains(.atomic) {
fatalError("withoutOverwriting is not supported with atomic")
}

try writeToFile(path: .path(path), data: self, options: options, reportProgress: true)
}
#endif

// MARK: -
//
Expand Down
4 changes: 2 additions & 2 deletions Sources/FoundationEssentials/JSON/JSONDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -603,10 +603,10 @@ extension JSONDecoderImpl: Decoder {
if type == Data.self {
return try self.unwrapData(from: mapValue, for: codingPathNode, additionalKey) as! T
}
#if FOUNDATION_FRAMEWORK // TODO: Reenable once URL and Decimal are moved
if type == URL.self {
return try self.unwrapURL(from: mapValue, for: codingPathNode, additionalKey) as! T
}
#if FOUNDATION_FRAMEWORK // TODO: Reenable once Decimal is moved
if type == Decimal.self {
return try self.unwrapDecimal(from: mapValue, for: codingPathNode, additionalKey) as! T
}
Expand Down Expand Up @@ -688,7 +688,6 @@ extension JSONDecoderImpl: Decoder {
}
}

#if FOUNDATION_FRAMEWORK // TODO: Reenable once URL and Decimal has been moved
private func unwrapURL(from mapValue: JSONMap.Value, for codingPathNode: _CodingPathNode, _ additionalKey: (some CodingKey)? = nil) throws -> URL {
try checkNotNull(mapValue, expectedType: URL.self, for: codingPathNode, additionalKey)

Expand All @@ -700,6 +699,7 @@ extension JSONDecoderImpl: Decoder {
return url
}

#if FOUNDATION_FRAMEWORK // TODO: Reenable once Decimal has been moved
private func unwrapDecimal(from mapValue: JSONMap.Value, for codingPathNode: _CodingPathNode, _ additionalKey: (some CodingKey)? = nil) throws -> Decimal {
try checkNotNull(mapValue, expectedType: Decimal.self, for: codingPathNode, additionalKey)

Expand Down
2 changes: 1 addition & 1 deletion Sources/FoundationEssentials/JSON/JSONEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1111,11 +1111,11 @@ private extension __JSONEncoder {
case is Data.Type:
// Respect Data encoding strategy
return try self.wrap(value as! Data, for: node, additionalKey)
#if FOUNDATION_FRAMEWORK // TODO: Reenable once URL and Decimal are moved
case is URL.Type:
// Encode URLs as single strings.
let url = value as! URL
return self.wrap(url.absoluteString)
#if FOUNDATION_FRAMEWORK // TODO: Reenable once Decimal is moved
case is Decimal.Type:
let decimal = value as! Decimal
return .number(decimal.description)
Expand Down
27 changes: 13 additions & 14 deletions Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ extension _ProcessInfo {
#if os(macOS)
var versionString = "macOS"
#elseif os(Linux)
if let osReleaseContents = try? Data(contentsOf: "/etc/os-release") {
if let osReleaseContents = try? Data(contentsOf: URL(filePath: "/etc/os-release", directoryHint: .notDirectory)) {
let strContents = String(decoding: osReleaseContents, as: UTF8.self)
if let name = strContents.split(separator: "\n").first(where: { $0.hasPrefix("PRETTY_NAME=") }) {
// This is extremely simplistic but manages to work for all known cases.
Expand Down Expand Up @@ -466,13 +466,12 @@ extension _ProcessInfo {
#if os(Linux)
// Support for CFS quotas for cpu count as used by Docker.
// Based on swift-nio code, https://github.com/apple/swift-nio/pull/1518
private static let cfsQuotaPath = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us"
private static let cfsPeriodPath = "/sys/fs/cgroup/cpu/cpu.cfs_period_us"
private static let cpuSetPath = "/sys/fs/cgroup/cpuset/cpuset.cpus"
private static let cfsQuotaURL = URL(filePath: "/sys/fs/cgroup/cpu/cpu.cfs_quota_us", directoryHint: .notDirectory)
private static let cfsPeriodURL = URL(filePath: "/sys/fs/cgroup/cpu/cpu.cfs_period_us", directoryHint: .notDirectory)
private static let cpuSetURL = URL(filePath: "/sys/fs/cgroup/cpuset/cpuset.cpus", directoryHint: .notDirectory)

private static func firstLineOfFile(path: String) throws -> Substring {
// TODO: Replace with URL version once that is available in FoundationEssentials
let data = try Data(contentsOf: path)
private static func firstLineOfFile(_ url: URL) throws -> Substring {
let data = try Data(contentsOf: url)
if let string = String(data: data, encoding: .utf8), let line = string.split(separator: "\n").first {
return line
} else {
Expand All @@ -491,8 +490,8 @@ extension _ProcessInfo {
return 1 + last - first
}

private static func coreCount(cpuset cpusetPath: String) -> Int? {
guard let cpuset = try? firstLineOfFile(path: cpusetPath).split(separator: ","),
private static func coreCount(cpuset cpusetURL: URL) -> Int? {
guard let cpuset = try? firstLineOfFile(cpusetURL).split(separator: ","),
!cpuset.isEmpty
else { return nil }
if let first = cpuset.first, let count = countCoreIds(cores: first) {
Expand All @@ -502,21 +501,21 @@ extension _ProcessInfo {
}
}

private static func coreCount(quota quotaPath: String, period periodPath: String) -> Int? {
guard let quota = try? Int(firstLineOfFile(path: quotaPath)),
private static func coreCount(quota quotaURL: URL, period periodURL: URL) -> Int? {
guard let quota = try? Int(firstLineOfFile(quotaURL)),
quota > 0
else { return nil }
guard let period = try? Int(firstLineOfFile(path: periodPath)),
guard let period = try? Int(firstLineOfFile(periodURL)),
period > 0
else { return nil }

return (quota - 1 + period) / period // always round up if fractional CPU quota requested
}

private static func fsCoreCount() -> Int? {
if let quota = coreCount(quota: cfsQuotaPath, period: cfsPeriodPath) {
if let quota = coreCount(quota: cfsQuotaURL, period: cfsPeriodURL) {
return quota
} else if let cpusetCount = coreCount(cpuset: cpuSetPath) {
} else if let cpusetCount = coreCount(cpuset: cpuSetURL) {
return cpusetCount
} else {
return nil
Expand Down
2 changes: 1 addition & 1 deletion Sources/FoundationEssentials/URL/URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1304,7 +1304,7 @@ public struct URL: Equatable, Sendable, Hashable {
return Parser.percentDecode(result, excluding: charsToLeaveEncoded) ?? ""
}

private var fileSystemPath: String {
var fileSystemPath: String {
return fileSystemPath(for: path())
}

Expand Down
26 changes: 2 additions & 24 deletions Tests/FoundationEssentialsTests/DataIOTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,10 @@ class DataIOTests : XCTestCase {

// MARK: - Helpers

#if FOUNDATION_FRAMEWORK
func testURL() -> URL {
// Generate a random file name
URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("testfile-\(UUID().uuidString)")
}
#else
/// Temporary helper until we port `URL` to swift-foundation.
func testURL() -> String {
// Generate a random file name
String.temporaryDirectoryPath.appendingPathComponent("testfile-\(UUID().uuidString)")
}
#endif

func generateTestData() -> Data {
// 16 MB file, big enough to trigger things like chunking
Expand All @@ -59,7 +51,6 @@ class DataIOTests : XCTestCase {
return Data(bytesNoCopy: ptr, count: count, deallocator: .free)
}

#if FOUNDATION_FRAMEWORK
func writeAndVerifyTestData(to url: URL, writeOptions: Data.WritingOptions = [], readOptions: Data.ReadingOptions = []) throws {
let data = generateTestData()
try data.write(to: url, options: writeOptions)
Expand All @@ -74,19 +65,6 @@ class DataIOTests : XCTestCase {
// Ignore
}
}
#else
func writeAndVerifyTestData(to path: String, writeOptions: Data.WritingOptions = [], readOptions: Data.ReadingOptions = []) throws {
let data = generateTestData()
try data.write(to: path, options: writeOptions)
let readData = try Data(contentsOf: path, options: readOptions)
XCTAssertEqual(data, readData)
}

func cleanup(at path: String) {
_ = unlink(path)
// Ignore any errors
}
#endif


// MARK: - Tests
Expand Down Expand Up @@ -244,9 +222,9 @@ class DataIOTests : XCTestCase {
throw XCTSkip("This test is only supported on Linux and Windows")
#else
#if os(Windows)
let path = "CON"
let path = URL(filePath: "CON", directoryHint: .notDirectory)
#else
let path = "/dev/stdout"
let path = URL(filePath: "/dev/stdout", directoryHint: .notDirectory)
#endif
XCTAssertNoThrow(try Data("Output to STDOUT\n".utf8).write(to: path))
#endif
Expand Down
26 changes: 1 addition & 25 deletions Tests/FoundationEssentialsTests/JSONEncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2768,9 +2768,9 @@ extension JSONEncoderTests {
_testRoundTrip(of: testBigDecimal)
}
}
#endif // FOUNDATION_FRAMEWORK

// MARK: - URL Tests
// TODO: Reenable these tests once URL is moved
extension JSONEncoderTests {
func testInterceptURL() {
// Want to make sure JSONEncoder writes out single-value URLs, not the keyed encoding.
Expand All @@ -2792,7 +2792,6 @@ extension JSONEncoderTests {
_testRoundTrip(of: Optional(url), expectedJSON: expectedJSON, outputFormatting: [.withoutEscapingSlashes])
}
}
#endif // FOUNDATION_FRAMEWORK

// MARK: - Helper Global Functions
func expectEqualPaths(_ lhs: [CodingKey], _ rhs: [CodingKey], _ prefix: String) {
Expand Down Expand Up @@ -2942,7 +2941,6 @@ fileprivate struct Address : Codable, Equatable {
fileprivate class Person : Codable, Equatable {
let name: String
let email: String
#if FOUNDATION_FRAMEWORK
let website: URL?


Expand All @@ -2951,22 +2949,11 @@ fileprivate class Person : Codable, Equatable {
self.email = email
self.website = website
}
#else
init(name: String, email: String) {
self.name = name
self.email = email
}
#endif

func isEqual(_ other: Person) -> Bool {
#if FOUNDATION_FRAMEWORK
return self.name == other.name &&
self.email == other.email &&
self.website == other.website
#else
return self.name == other.name &&
self.email == other.email
#endif
}

static func ==(_ lhs: Person, _ rhs: Person) -> Bool {
Expand All @@ -2982,17 +2969,10 @@ fileprivate class Person : Codable, Equatable {
fileprivate class Employee : Person {
let id: Int

#if FOUNDATION_FRAMEWORK
init(name: String, email: String, website: URL? = nil, id: Int) {
self.id = id
super.init(name: name, email: email, website: website)
}
#else
init(name: String, email: String, id: Int) {
self.id = id
super.init(name: name, email: email)
}
#endif

enum CodingKeys : String, CodingKey {
case id
Expand Down Expand Up @@ -3721,9 +3701,7 @@ extension JSONPass {
let array : [String]
let object : [String:String]
let address : String
#if FOUNDATION_FRAMEWORK
let url : URL
#endif
let comment : String
let special_sequences_key : String
let spaced : [Int]
Expand Down Expand Up @@ -3757,9 +3735,7 @@ extension JSONPass {
case array
case object
case address
#if FOUNDATION_FRAMEWORK
case url
#endif
case comment
case special_sequences_key = "# -- --> */"
case spaced = " s p a c e d "
Expand Down
13 changes: 9 additions & 4 deletions Tests/FoundationEssentialsTests/ResourceUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,22 @@ func testData(forResource resource: String, withExtension ext: String, subdirect
guard let url = Bundle.module.url(forResource: resource, withExtension: ext, subdirectory: subdir) else {
return nil
}

let essentialsURL = FoundationEssentials.URL(filePath: url.path)

return try? Data(contentsOf: url.path(percentEncoded: false))
return try? Data(contentsOf: essentialsURL)
#else
// swiftpm drops the resources next to the executable, at:
// ./FoundationPreview_FoundationEssentialsTests.resources/Resources/
// Hard-coding the path is unfortunate, but a temporary need until we have a better way to handle this
var path = ProcessInfo.processInfo.arguments[0].deletingLastPathComponent() + "/FoundationPreview_FoundationEssentialsTests.resources/Resources/"
var path = URL(filePath: ProcessInfo.processInfo.arguments[0])
.deletingLastPathComponent()
.appending(component: "FoundationPreview_FoundationEssentialsTests.resources", directoryHint: .isDirectory)
.appending(component: "Resources", directoryHint: .isDirectory)
if let subdirectory {
path += subdirectory + "/"
path.append(path: subdirectory, directoryHint: .isDirectory)
}
path += resource + "." + ext
path.append(component: resource + "." + ext, directoryHint: .notDirectory)
return try? Data(contentsOf: path)
#endif
#endif
Expand Down

0 comments on commit 543a9e4

Please sign in to comment.