Skip to content

Commit 543a9e4

Browse files
authored
Use URL in JSON/Data APIs (#588)
* Use URL in JSON/Data APIs * path -> URL * Use fileSystemPath instead of path(percentEncoded:) * Correct resource loading on linux
1 parent d88c245 commit 543a9e4

File tree

8 files changed

+33
-96
lines changed

8 files changed

+33
-96
lines changed

Sources/FoundationEssentials/Data/Data.swift

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,7 +2069,6 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect
20692069
}
20702070
#endif
20712071

2072-
#if FOUNDATION_FRAMEWORK
20732072
/// Initialize a `Data` with the contents of a `URL`.
20742073
///
20752074
/// - parameter url: The `URL` to read.
@@ -2083,20 +2082,17 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect
20832082
if url.isFileURL {
20842083
self = try readDataFromFile(path: .url(url), reportProgress: true, options: options)
20852084
} else {
2085+
#if FOUNDATION_FRAMEWORK
20862086
// Fallback to NSData, to read via NSURLSession
20872087
let d = try NSData(contentsOf: url, options: NSData.ReadingOptions(rawValue: options.rawValue))
20882088
self.init(referencing: d)
2089+
#else
2090+
throw CocoaError(.fileReadUnsupportedScheme)
2091+
#endif
20892092
}
20902093
#endif
20912094
}
2092-
#else
2093-
/// Temporary usage, until `URL` is ported. Non-framework only. Same as of `contentsOfFile:options:`.
2094-
public init(contentsOf path: String, options: ReadingOptions = []) throws {
2095-
self = try readDataFromFile(path: .path(path), reportProgress: true, options: options)
2096-
}
2097-
#endif
20982095

2099-
#if FOUNDATION_FRAMEWORK
21002096
internal init(contentsOfFile path: String, options: ReadingOptions = []) throws {
21012097
#if NO_FILESYSTEM
21022098
let d = try NSData(contentsOfFile: path, options: NSData.ReadingOptions(rawValue: options.rawValue))
@@ -2105,12 +2101,6 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect
21052101
self = try readDataFromFile(path: .path(path), reportProgress: true, options: options)
21062102
#endif
21072103
}
2108-
#else
2109-
/// Temporary usage, until `URL` is ported. Non-framework only.
2110-
public init(contentsOfFile path: String, options: ReadingOptions = []) throws {
2111-
self = try readDataFromFile(path: .path(path), reportProgress: true, options: options)
2112-
}
2113-
#endif
21142104

21152105
// -----------------------------------
21162106
// MARK: - Properties and Functions
@@ -2448,17 +2438,6 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect
24482438
throw CocoaError(.featureUnsupported)
24492439
#endif
24502440
}
2451-
2452-
#if !FOUNDATION_FRAMEWORK
2453-
// 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.
2454-
public func write(to path: String, options: Data.WritingOptions = []) throws {
2455-
if options.contains(.withoutOverwriting) && options.contains(.atomic) {
2456-
fatalError("withoutOverwriting is not supported with atomic")
2457-
}
2458-
2459-
try writeToFile(path: .path(path), data: self, options: options, reportProgress: true)
2460-
}
2461-
#endif
24622441

24632442
// MARK: -
24642443
//

Sources/FoundationEssentials/JSON/JSONDecoder.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -603,10 +603,10 @@ extension JSONDecoderImpl: Decoder {
603603
if type == Data.self {
604604
return try self.unwrapData(from: mapValue, for: codingPathNode, additionalKey) as! T
605605
}
606-
#if FOUNDATION_FRAMEWORK // TODO: Reenable once URL and Decimal are moved
607606
if type == URL.self {
608607
return try self.unwrapURL(from: mapValue, for: codingPathNode, additionalKey) as! T
609608
}
609+
#if FOUNDATION_FRAMEWORK // TODO: Reenable once Decimal is moved
610610
if type == Decimal.self {
611611
return try self.unwrapDecimal(from: mapValue, for: codingPathNode, additionalKey) as! T
612612
}
@@ -688,7 +688,6 @@ extension JSONDecoderImpl: Decoder {
688688
}
689689
}
690690

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

@@ -700,6 +699,7 @@ extension JSONDecoderImpl: Decoder {
700699
return url
701700
}
702701

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

Sources/FoundationEssentials/JSON/JSONEncoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1111,11 +1111,11 @@ private extension __JSONEncoder {
11111111
case is Data.Type:
11121112
// Respect Data encoding strategy
11131113
return try self.wrap(value as! Data, for: node, additionalKey)
1114-
#if FOUNDATION_FRAMEWORK // TODO: Reenable once URL and Decimal are moved
11151114
case is URL.Type:
11161115
// Encode URLs as single strings.
11171116
let url = value as! URL
11181117
return self.wrap(url.absoluteString)
1118+
#if FOUNDATION_FRAMEWORK // TODO: Reenable once Decimal is moved
11191119
case is Decimal.Type:
11201120
let decimal = value as! Decimal
11211121
return .number(decimal.description)

Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ extension _ProcessInfo {
262262
#if os(macOS)
263263
var versionString = "macOS"
264264
#elseif os(Linux)
265-
if let osReleaseContents = try? Data(contentsOf: "/etc/os-release") {
265+
if let osReleaseContents = try? Data(contentsOf: URL(filePath: "/etc/os-release", directoryHint: .notDirectory)) {
266266
let strContents = String(decoding: osReleaseContents, as: UTF8.self)
267267
if let name = strContents.split(separator: "\n").first(where: { $0.hasPrefix("PRETTY_NAME=") }) {
268268
// This is extremely simplistic but manages to work for all known cases.
@@ -466,13 +466,12 @@ extension _ProcessInfo {
466466
#if os(Linux)
467467
// Support for CFS quotas for cpu count as used by Docker.
468468
// Based on swift-nio code, https://github.com/apple/swift-nio/pull/1518
469-
private static let cfsQuotaPath = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us"
470-
private static let cfsPeriodPath = "/sys/fs/cgroup/cpu/cpu.cfs_period_us"
471-
private static let cpuSetPath = "/sys/fs/cgroup/cpuset/cpuset.cpus"
469+
private static let cfsQuotaURL = URL(filePath: "/sys/fs/cgroup/cpu/cpu.cfs_quota_us", directoryHint: .notDirectory)
470+
private static let cfsPeriodURL = URL(filePath: "/sys/fs/cgroup/cpu/cpu.cfs_period_us", directoryHint: .notDirectory)
471+
private static let cpuSetURL = URL(filePath: "/sys/fs/cgroup/cpuset/cpuset.cpus", directoryHint: .notDirectory)
472472

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

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

505-
private static func coreCount(quota quotaPath: String, period periodPath: String) -> Int? {
506-
guard let quota = try? Int(firstLineOfFile(path: quotaPath)),
504+
private static func coreCount(quota quotaURL: URL, period periodURL: URL) -> Int? {
505+
guard let quota = try? Int(firstLineOfFile(quotaURL)),
507506
quota > 0
508507
else { return nil }
509-
guard let period = try? Int(firstLineOfFile(path: periodPath)),
508+
guard let period = try? Int(firstLineOfFile(periodURL)),
510509
period > 0
511510
else { return nil }
512511

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

516515
private static func fsCoreCount() -> Int? {
517-
if let quota = coreCount(quota: cfsQuotaPath, period: cfsPeriodPath) {
516+
if let quota = coreCount(quota: cfsQuotaURL, period: cfsPeriodURL) {
518517
return quota
519-
} else if let cpusetCount = coreCount(cpuset: cpuSetPath) {
518+
} else if let cpusetCount = coreCount(cpuset: cpuSetURL) {
520519
return cpusetCount
521520
} else {
522521
return nil

Sources/FoundationEssentials/URL/URL.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1304,7 +1304,7 @@ public struct URL: Equatable, Sendable, Hashable {
13041304
return Parser.percentDecode(result, excluding: charsToLeaveEncoded) ?? ""
13051305
}
13061306

1307-
private var fileSystemPath: String {
1307+
var fileSystemPath: String {
13081308
return fileSystemPath(for: path())
13091309
}
13101310

Tests/FoundationEssentialsTests/DataIOTests.swift

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,10 @@ class DataIOTests : XCTestCase {
2828

2929
// MARK: - Helpers
3030

31-
#if FOUNDATION_FRAMEWORK
3231
func testURL() -> URL {
3332
// Generate a random file name
3433
URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("testfile-\(UUID().uuidString)")
3534
}
36-
#else
37-
/// Temporary helper until we port `URL` to swift-foundation.
38-
func testURL() -> String {
39-
// Generate a random file name
40-
String.temporaryDirectoryPath.appendingPathComponent("testfile-\(UUID().uuidString)")
41-
}
42-
#endif
4335

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

62-
#if FOUNDATION_FRAMEWORK
6354
func writeAndVerifyTestData(to url: URL, writeOptions: Data.WritingOptions = [], readOptions: Data.ReadingOptions = []) throws {
6455
let data = generateTestData()
6556
try data.write(to: url, options: writeOptions)
@@ -74,19 +65,6 @@ class DataIOTests : XCTestCase {
7465
// Ignore
7566
}
7667
}
77-
#else
78-
func writeAndVerifyTestData(to path: String, writeOptions: Data.WritingOptions = [], readOptions: Data.ReadingOptions = []) throws {
79-
let data = generateTestData()
80-
try data.write(to: path, options: writeOptions)
81-
let readData = try Data(contentsOf: path, options: readOptions)
82-
XCTAssertEqual(data, readData)
83-
}
84-
85-
func cleanup(at path: String) {
86-
_ = unlink(path)
87-
// Ignore any errors
88-
}
89-
#endif
9068

9169

9270
// MARK: - Tests
@@ -244,9 +222,9 @@ class DataIOTests : XCTestCase {
244222
throw XCTSkip("This test is only supported on Linux and Windows")
245223
#else
246224
#if os(Windows)
247-
let path = "CON"
225+
let path = URL(filePath: "CON", directoryHint: .notDirectory)
248226
#else
249-
let path = "/dev/stdout"
227+
let path = URL(filePath: "/dev/stdout", directoryHint: .notDirectory)
250228
#endif
251229
XCTAssertNoThrow(try Data("Output to STDOUT\n".utf8).write(to: path))
252230
#endif

Tests/FoundationEssentialsTests/JSONEncoderTests.swift

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2768,9 +2768,9 @@ extension JSONEncoderTests {
27682768
_testRoundTrip(of: testBigDecimal)
27692769
}
27702770
}
2771+
#endif // FOUNDATION_FRAMEWORK
27712772

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

27972796
// MARK: - Helper Global Functions
27982797
func expectEqualPaths(_ lhs: [CodingKey], _ rhs: [CodingKey], _ prefix: String) {
@@ -2942,7 +2941,6 @@ fileprivate struct Address : Codable, Equatable {
29422941
fileprivate class Person : Codable, Equatable {
29432942
let name: String
29442943
let email: String
2945-
#if FOUNDATION_FRAMEWORK
29462944
let website: URL?
29472945

29482946

@@ -2951,22 +2949,11 @@ fileprivate class Person : Codable, Equatable {
29512949
self.email = email
29522950
self.website = website
29532951
}
2954-
#else
2955-
init(name: String, email: String) {
2956-
self.name = name
2957-
self.email = email
2958-
}
2959-
#endif
29602952

29612953
func isEqual(_ other: Person) -> Bool {
2962-
#if FOUNDATION_FRAMEWORK
29632954
return self.name == other.name &&
29642955
self.email == other.email &&
29652956
self.website == other.website
2966-
#else
2967-
return self.name == other.name &&
2968-
self.email == other.email
2969-
#endif
29702957
}
29712958

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

2985-
#if FOUNDATION_FRAMEWORK
29862972
init(name: String, email: String, website: URL? = nil, id: Int) {
29872973
self.id = id
29882974
super.init(name: name, email: email, website: website)
29892975
}
2990-
#else
2991-
init(name: String, email: String, id: Int) {
2992-
self.id = id
2993-
super.init(name: name, email: email)
2994-
}
2995-
#endif
29962976

29972977
enum CodingKeys : String, CodingKey {
29982978
case id
@@ -3721,9 +3701,7 @@ extension JSONPass {
37213701
let array : [String]
37223702
let object : [String:String]
37233703
let address : String
3724-
#if FOUNDATION_FRAMEWORK
37253704
let url : URL
3726-
#endif
37273705
let comment : String
37283706
let special_sequences_key : String
37293707
let spaced : [Int]
@@ -3757,9 +3735,7 @@ extension JSONPass {
37573735
case array
37583736
case object
37593737
case address
3760-
#if FOUNDATION_FRAMEWORK
37613738
case url
3762-
#endif
37633739
case comment
37643740
case special_sequences_key = "# -- --> */"
37653741
case spaced = " s p a c e d "

Tests/FoundationEssentialsTests/ResourceUtilities.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,22 @@ func testData(forResource resource: String, withExtension ext: String, subdirect
4848
guard let url = Bundle.module.url(forResource: resource, withExtension: ext, subdirectory: subdir) else {
4949
return nil
5050
}
51+
52+
let essentialsURL = FoundationEssentials.URL(filePath: url.path)
5153

52-
return try? Data(contentsOf: url.path(percentEncoded: false))
54+
return try? Data(contentsOf: essentialsURL)
5355
#else
5456
// swiftpm drops the resources next to the executable, at:
5557
// ./FoundationPreview_FoundationEssentialsTests.resources/Resources/
5658
// Hard-coding the path is unfortunate, but a temporary need until we have a better way to handle this
57-
var path = ProcessInfo.processInfo.arguments[0].deletingLastPathComponent() + "/FoundationPreview_FoundationEssentialsTests.resources/Resources/"
59+
var path = URL(filePath: ProcessInfo.processInfo.arguments[0])
60+
.deletingLastPathComponent()
61+
.appending(component: "FoundationPreview_FoundationEssentialsTests.resources", directoryHint: .isDirectory)
62+
.appending(component: "Resources", directoryHint: .isDirectory)
5863
if let subdirectory {
59-
path += subdirectory + "/"
64+
path.append(path: subdirectory, directoryHint: .isDirectory)
6065
}
61-
path += resource + "." + ext
66+
path.append(component: resource + "." + ext, directoryHint: .notDirectory)
6267
return try? Data(contentsOf: path)
6368
#endif
6469
#endif

0 commit comments

Comments
 (0)