Remove @dynamicMember generally

This commit is contained in:
Max Howell
2019-07-21 15:28:01 -04:00
parent d2bb2a1fdc
commit 621d1b0160
13 changed files with 198 additions and 131 deletions

View File

@@ -13,31 +13,31 @@ public extension Bundle {
Returns the path for the shared-frameworks directory in this bundle.
- Note: This is typically `ShareFrameworks`
*/
var sharedFrameworks: Path {
return sharedFrameworksPath.flatMap(Path.init) ?? defaultSharedFrameworksPath
var sharedFrameworks: DynamicPath {
return sharedFrameworksPath.flatMap(DynamicPath.init) ?? defaultSharedFrameworksPath
}
/**
Returns the path for the private-frameworks directory in this bundle.
- Note: This is typically `Frameworks`
*/
var privateFrameworks: Path {
return privateFrameworksPath.flatMap(Path.init) ?? defaultSharedFrameworksPath
var privateFrameworks: DynamicPath {
return privateFrameworksPath.flatMap(DynamicPath.init) ?? defaultSharedFrameworksPath
}
/// Returns the path for the resources directory in this bundle.
var resources: Path {
return resourcePath.flatMap(Path.init) ?? defaultResourcesPath
var resources: DynamicPath {
return resourcePath.flatMap(DynamicPath.init) ?? defaultResourcesPath
}
/// Returns the path for this bundle.
var path: Path {
return Path(string: bundlePath)
var path: DynamicPath {
return DynamicPath(string: bundlePath)
}
/// Returns the executable for this bundle, if there is one, not all bundles have one hence `Optional`.
var executable: Path? {
return executablePath.flatMap(Path.init)
var executable: DynamicPath? {
return executablePath.flatMap(DynamicPath.init)
}
}
@@ -45,14 +45,14 @@ public extension Bundle {
public extension String {
/// Initializes this `String` with the contents of the provided path.
@inlinable
init(contentsOf path: Path) throws {
init<P: Pathish>(contentsOf path: P) throws {
try self.init(contentsOfFile: path.string)
}
/// - Returns: `to` to allow chaining
@inlinable
@discardableResult
func write(to: Path, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path {
func write<P: Pathish>(to: P, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> P {
try write(toFile: to.string, atomically: atomically, encoding: encoding)
return to
}
@@ -62,14 +62,14 @@ public extension String {
public extension Data {
/// Initializes this `Data` with the contents of the provided path.
@inlinable
init(contentsOf path: Path) throws {
init<P: Pathish>(contentsOf path: P) throws {
try self.init(contentsOf: path.url)
}
/// - Returns: `to` to allow chaining
@inlinable
@discardableResult
func write(to: Path, atomically: Bool = false) throws -> Path {
func write<P: Pathish>(to: P, atomically: Bool = false) throws -> P {
let opts: NSData.WritingOptions
if atomically {
#if !os(Linux)
@@ -89,39 +89,39 @@ public extension Data {
public extension FileHandle {
/// Initializes this `FileHandle` for reading at the location of the provided path.
@inlinable
convenience init(forReadingAt path: Path) throws {
convenience init<P: Pathish>(forReadingAt path: P) throws {
try self.init(forReadingFrom: path.url)
}
/// Initializes this `FileHandle` for writing at the location of the provided path.
@inlinable
convenience init(forWritingAt path: Path) throws {
convenience init<P: Pathish>(forWritingAt path: P) throws {
try self.init(forWritingTo: path.url)
}
/// Initializes this `FileHandle` for reading and writing at the location of the provided path.
@inlinable
convenience init(forUpdatingAt path: Path) throws {
convenience init<P: Pathish>(forUpdatingAt path: P) throws {
try self.init(forUpdating: path.url)
}
}
internal extension Bundle {
var defaultSharedFrameworksPath: Path {
var defaultSharedFrameworksPath: DynamicPath {
#if os(macOS)
return path.join("Contents/Frameworks")
return path.Contents.Frameworks
#elseif os(Linux)
return path.join("lib")
return path.lib
#else
return path.join("Frameworks")
return path.Frameworks
#endif
}
var defaultResourcesPath: Path {
var defaultResourcesPath: DynamicPath {
#if os(macOS)
return path.join("Contents/Resources")
return path.Contents.Resources
#elseif os(Linux)
return path.join("share")
return path.share
#else
return path
#endif

View File

@@ -1,6 +1,6 @@
import Foundation
public extension Path {
public extension Pathish {
//MARK: Filesystem Attributes
/**
@@ -38,7 +38,7 @@ public extension Path {
@discardableResult
func chmod(_ octal: Int) throws -> Path {
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
return self
return Path(self)
}
/**
@@ -57,7 +57,7 @@ public extension Path {
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
#endif
return self
return Path(self)
}
/**
@@ -73,7 +73,7 @@ public extension Path {
do {
attrs = try FileManager.default.attributesOfItem(atPath: string)
} catch CocoaError.fileReadNoSuchFile {
return self
return Path(self)
}
let b = attrs[.immutable] as? Bool ?? false
if b {
@@ -81,14 +81,10 @@ public extension Path {
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
#endif
return self
return Path(self)
}
enum Kind {
case file, symlink, directory
}
var kind: Kind? {
var kind: Path.Kind? {
var buf = stat()
guard lstat(string, &buf) == 0 else {
return nil
@@ -102,3 +98,9 @@ public extension Path {
}
}
}
public extension Path {
enum Kind {
case file, symlink, directory
}
}

View File

@@ -23,18 +23,19 @@ public extension CodingUserInfoKey {
Provided for relative-path coding. See the instructions in our
[README](https://github.com/mxcl/Path.swift/#codable).
*/
extension Path: Codable {
extension Path: Codable {
/// - SeeAlso: `CodingUserInfoKey.relativePath`
/// :nodoc:
public init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer().decode(String.self)
if value.hasPrefix("/") {
string = value
} else {
guard let root = decoder.userInfo[.relativePath] as? Path else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Path cannot decode a relative path if `userInfo[.relativePath]` not set to a Path object."))
}
} else if let root = decoder.userInfo[.relativePath] as? Path {
string = (root/value).string
} else if let root = decoder.userInfo[.relativePath] as? DynamicPath {
string = (root/value).string
} else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Path cannot decode a relative path if `userInfo[.relativePath]` not set to a Path object."))
}
}
@@ -44,6 +45,8 @@ extension Path: Codable {
var container = encoder.singleValueContainer()
if let root = encoder.userInfo[.relativePath] as? Path {
try container.encode(relative(to: root))
} else if let root = encoder.userInfo[.relativePath] as? DynamicPath {
try container.encode(relative(to: root))
} else {
try container.encode(string)
}

View File

@@ -4,17 +4,17 @@ extension Path {
//MARK: Common Directories
/// Returns a `Path` containing `FileManager.default.currentDirectoryPath`.
public static var cwd: Path {
return Path(string: FileManager.default.currentDirectoryPath)
public static var cwd: DynamicPath {
return .init(string: FileManager.default.currentDirectoryPath)
}
/// Returns a `Path` representing the root path.
public static var root: Path {
return Path(string: "/")
public static var root: DynamicPath {
return .init(string: "/")
}
/// Returns a `Path` representing the users home directory
public static var home: Path {
public static var home: DynamicPath {
let string: String
#if os(macOS)
if #available(OSX 10.12, *) {
@@ -25,30 +25,30 @@ extension Path {
#else
string = NSHomeDirectory()
#endif
return Path(string: string)
return .init(string: string)
}
/// Helper to allow search path and domain mask to be passed in.
private static func path(for searchPath: FileManager.SearchPathDirectory) -> Path {
private static func path(for searchPath: FileManager.SearchPathDirectory) -> DynamicPath {
#if os(Linux)
// the urls(for:in:) function is not implemented on Linux
//TODO strictly we should first try to use the provided binary tool
let foo = { ProcessInfo.processInfo.environment[$0].flatMap(Path.init) ?? $1 }
let foo = { ProcessInfo.processInfo.environment[$0].flatMap(Path.init).map(DynamicPath.init) ?? $1 }
switch searchPath {
case .documentDirectory:
return Path.home/"Documents"
return Path.home.Documents
case .applicationSupportDirectory:
return foo("XDG_DATA_HOME", Path.home/".local/share")
return foo("XDG_DATA_HOME", Path.home[dynamicMember: ".local/share"])
case .cachesDirectory:
return foo("XDG_CACHE_HOME", Path.home/".cache")
return foo("XDG_CACHE_HOME", Path.home[dynamicMember: ".cache"])
default:
fatalError()
}
#else
guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else { return defaultUrl(for: searchPath) }
return Path(string: pathString)
return DynamicPath(string: pathString)
#endif
}
@@ -57,7 +57,7 @@ extension Path {
- Note: There is no standard location for documents on Linux, thus we return `~/Documents`.
- Note: You should create a subdirectory before creating any files.
*/
public static var documents: Path {
public static var documents: DynamicPath {
return path(for: .documentDirectory)
}
@@ -66,7 +66,7 @@ extension Path {
- Note: On Linux this is `XDG_CACHE_HOME`.
- Note: You should create a subdirectory before creating any files.
*/
public static var caches: Path {
public static var caches: DynamicPath {
return path(for: .cachesDirectory)
}
@@ -75,20 +75,20 @@ extension Path {
- Note: On Linux is `XDG_DATA_HOME`.
- Note: You should create a subdirectory before creating any files.
*/
public static var applicationSupport: Path {
public static var applicationSupport: DynamicPath {
return path(for: .applicationSupportDirectory)
}
}
#if !os(Linux)
func defaultUrl(for searchPath: FileManager.SearchPathDirectory) -> Path {
func defaultUrl(for searchPath: FileManager.SearchPathDirectory) -> DynamicPath {
switch searchPath {
case .documentDirectory:
return Path.home/"Documents"
return Path.home.Documents
case .applicationSupportDirectory:
return Path.home/"Library/Application Support"
return Path.home.Library[dynamicMember: "Application Support"]
case .cachesDirectory:
return Path.home/"Library/Caches"
return Path.home.Library.Caches
default:
fatalError()
}

View File

@@ -3,7 +3,7 @@ import Foundation
import Glibc
#endif
public extension Path {
public extension Pathish {
//MARK: File Management
/**
@@ -24,7 +24,7 @@ public extension Path {
- SeeAlso: `copy(into:overwrite:)`
*/
@discardableResult
func copy(to: Path, overwrite: Bool = false) throws -> Path {
func copy<P: Pathish>(to: P, overwrite: Bool = false) throws -> Path {
if overwrite, let tokind = to.kind, tokind != .directory, kind != .directory {
try FileManager.default.removeItem(at: to.url)
}
@@ -34,7 +34,7 @@ public extension Path {
}
#endif
try FileManager.default.copyItem(atPath: string, toPath: to.string)
return to
return Path(to)
}
/**
@@ -60,7 +60,7 @@ public extension Path {
- SeeAlso: `copy(to:overwrite:)`
*/
@discardableResult
func copy(into: Path, overwrite: Bool = false) throws -> Path {
func copy<P: Pathish>(into: P, overwrite: Bool = false) throws -> Path {
if into.kind == nil {
try into.mkdir(.p)
}
@@ -94,12 +94,12 @@ public extension Path {
- SeeAlso: `move(into:overwrite:)`
*/
@discardableResult
func move(to: Path, overwrite: Bool = false) throws -> Path {
func move<P: Pathish>(to: P, overwrite: Bool = false) throws -> Path {
if overwrite, let kind = to.kind, kind != .directory {
try FileManager.default.removeItem(at: to.url)
}
try FileManager.default.moveItem(at: url, to: to.url)
return to
return Path(to)
}
/**
@@ -118,7 +118,7 @@ public extension Path {
- SeeAlso: `move(to:overwrite:)`
*/
@discardableResult
func move(into: Path, overwrite: Bool = false) throws -> Path {
func move<P: Pathish>(into: P, overwrite: Bool = false) throws -> Path {
switch into.kind {
case nil:
try into.mkdir(.p)
@@ -154,7 +154,7 @@ public extension Path {
/**
Creates an empty file at this path or if the file exists, updates its modification time.
- Returns: `self` to allow chaining.
- Returns: A copy of `self` to allow chaining.
*/
@inlinable
@discardableResult
@@ -172,7 +172,7 @@ public extension Path {
try FileManager.default.setAttributes([.modificationDate: Date()], ofItemAtPath: string)
#endif
}
return self
return Path(self)
}
/**
@@ -180,7 +180,7 @@ public extension Path {
- Parameter options: Specify `mkdir(.p)` to create intermediary directories.
- Note: We do not error if the directory already exists (even without `.p`)
because *Path.swift* noops if the desired end result preexists.
- Returns: `self` to allow chaining.
- Returns: A copy of `self` to allow chaining.
*/
@discardableResult
func mkdir(_ options: MakeDirectoryOptions? = nil) throws -> Path {
@@ -199,7 +199,7 @@ public extension Path {
throw error
#endif
}
return self
return Path(self)
}
/**
@@ -222,9 +222,9 @@ public extension Path {
- Note: If `self` does not exist, is **not** an error.
*/
@discardableResult
func symlink(as: Path) throws -> Path {
func symlink<P: Pathish>(as: P) throws -> Path {
try FileManager.default.createSymbolicLink(atPath: `as`.string, withDestinationPath: string)
return `as`
return Path(`as`)
}
/**
@@ -232,7 +232,7 @@ public extension Path {
- Note: If into does not exist, creates the directory with intermediate directories if necessary.
*/
@discardableResult
func symlink(into dir: Path) throws -> Path {
func symlink<P: Pathish>(into dir: P) throws -> Path {
switch dir.kind {
case nil, .symlink?:
try dir.mkdir(.p)

View File

@@ -11,3 +11,17 @@ extension Path: CustomDebugStringConvertible {
return "Path(\(string))"
}
}
extension DynamicPath: CustomStringConvertible {
/// Returns `Path.string`
public var description: String {
return string
}
}
extension DynamicPath: CustomDebugStringConvertible {
/// Returns eg. `Path(string: "/foo")`
public var debugDescription: String {
return "Path(\(string))"
}
}

View File

@@ -5,7 +5,7 @@ import func Glibc.access
import Darwin
#endif
public extension Path {
public extension Pathish {
//MARK: Filesystem Properties
/**

View File

@@ -32,11 +32,15 @@ let _realpath = Glibc.realpath
let p1 = Path.root.usr.bin.ls // => /usr/bin/ls
However we only provide this support off of the static members like `root` due
to the anti-pattern where Path.swift suddenly feels like Javascript otherwise.
- Note: A `Path` does not necessarily represent an actual filesystem entry.
*/
public struct Path: Pathish {
@dynamicMemberLookup
public struct Path: Equatable, Hashable, Comparable {
/// The normalized string representation of the underlying filesystem path
public let string: String
init(string: String) {
assert(string.first == "/")
@@ -70,11 +74,11 @@ public struct Path: Equatable, Hashable, Comparable {
ifExists(withPrefix: "/var/automount", removeFirst: 2)
ifExists(withPrefix: "/private", removeFirst: 1)
#endif
self.string = join_(prefix: "/", pathComponents: pathComponents)
string = join_(prefix: "/", pathComponents: pathComponents)
case "~":
if description == "~" {
self = Path.home
string = Path.home.string
return
}
let tilded: String
@@ -96,7 +100,7 @@ public struct Path: Equatable, Hashable, Comparable {
#endif
}
pathComponents.remove(at: 0)
self.string = join_(prefix: tilded, pathComponents: pathComponents)
string = join_(prefix: tilded, pathComponents: pathComponents)
default:
return nil
@@ -124,20 +128,15 @@ public struct Path: Equatable, Hashable, Comparable {
// ^^ works even if the url is a file-reference url
}
/// :nodoc:
public subscript(dynamicMember addendum: String) -> Path {
//NOTE its possible for the string to be anything if we are invoked via
// explicit subscript thus we use our fully sanitized `join` function
return Path(string: join_(prefix: string, appending: addendum))
/// Converts anything that is `Pathish` to a `Path`
public init<P: Pathish>(_ path: P) {
string = path.string
}
}
//MARK: Properties
/// The underlying filesystem path
public let string: String
public extension Pathish {
/// Returns a `URL` representing this file path.
public var url: URL {
var url: URL {
return URL(fileURLWithPath: string)
}
@@ -147,7 +146,7 @@ public struct Path: Equatable, Hashable, Comparable {
- SeeAlso: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl
- Important: On Linux returns an file scheme NSURL for this path string.
*/
public var fileReferenceURL: NSURL? {
var fileReferenceURL: NSURL? {
#if !os(Linux)
// https://bugs.swift.org/browse/SR-2728
return (url as NSURL).perform(#selector(NSURL.fileReferenceURL))?.takeUnretainedValue() as? NSURL
@@ -164,7 +163,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Note: always returns a valid path, `Path.root.parent` *is* `Path.root`.
*/
public var parent: Path {
var parent: Path {
let index = string.lastIndex(of: "/")!
let substr = string[string.indices.startIndex..<index]
return Path(string: String(substr))
@@ -177,7 +176,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Note: We special case eg. `foo.tar.gz`.
*/
@inlinable
public var `extension`: String {
var `extension`: String {
//FIXME efficiency
switch true {
case string.hasSuffix(".tar.gz"):
@@ -204,7 +203,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Important: The first element is always "/" to be consistent with `NSString.pathComponents`.
*/
@inlinable
public var components: [String] {
var components: [String] {
return ["/"] + string.split(separator: "/").map(String.init)
}
@@ -225,7 +224,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Returns: A new joined path.
- SeeAlso: `Path./(_:_:)`
*/
public func join<S>(_ addendum: S) -> Path where S: StringProtocol {
func join<S>(_ addendum: S) -> Path where S: StringProtocol {
return Path(string: join_(prefix: string, appending: addendum))
}
@@ -246,7 +245,7 @@ public struct Path: Equatable, Hashable, Comparable {
- SeeAlso: `join(_:)`
*/
@inlinable
public static func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
static func /<S>(lhs: Self, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs)
}
@@ -257,7 +256,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Parameter base: The base to which we calculate the relative path.
- ToDo: Another variant that returns `nil` if result would start with `..`
*/
public func relative(to base: Path) -> String {
func relative<P: Pathish>(to base: P) -> String {
// Split the two paths into their components.
// FIXME: The is needs to be optimized to avoid unncessary copying.
let pathComps = (string as NSString).pathComponents
@@ -296,7 +295,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Returns: A string that is the filenames basename.
- Parameter dropExtension: If `true` returns the basename without its file extension.
*/
public func basename(dropExtension: Bool = false) -> String {
func basename(dropExtension: Bool = false) -> String {
var lastPathComponent: Substring {
let slash = string.lastIndex(of: "/")!
let index = string.index(after: slash)
@@ -321,27 +320,25 @@ public struct Path: Equatable, Hashable, Comparable {
If the path represents an actual entry that is a symlink, returns the symlinks
absolute destination.
- Important: This is not exhaustive, the resulting path may still contain
symlink.
- Important: The path will only be different if the last path component is a
symlink, any symlinks in prior components are not resolved.
- Important: This is not exhaustive, the resulting path may still contain a symlink.
- Important: The path will only be different if the last path component is a symlink, any symlinks in prior components are not resolved.
- Note: If file exists but isnt a symlink, returns `self`.
- Note: If symlink destination does not exist, is **not** an error.
*/
public func readlink() throws -> Path {
func readlink() throws -> Path {
do {
let rv = try FileManager.default.destinationOfSymbolicLink(atPath: string)
return Path(rv) ?? parent/rv
} catch CocoaError.fileReadUnknown {
// file is not symlink, return `self`
assert(exists)
return self
return Path(string: string)
} catch {
#if os(Linux)
// ugh: Swift on Linux
let nsError = error as NSError
if nsError.domain == NSCocoaErrorDomain, nsError.code == CocoaError.fileReadUnknown.rawValue, exists {
return self
return Path(self)
}
#endif
throw error
@@ -349,7 +346,7 @@ public struct Path: Equatable, Hashable, Comparable {
}
/// Recursively resolves symlinks in this path.
public func realpath() throws -> Path {
func realpath() throws -> Path {
guard let rv = _realpath(string, nil) else { throw CocoaError.error(.fileNoSuchFile) }
defer { free(rv) }
guard let rvv = String(validatingUTF8: rv) else { throw CocoaError.error(.fileReadUnknownStringEncoding) }
@@ -367,7 +364,7 @@ public struct Path: Equatable, Hashable, Comparable {
/// Returns the locale-aware sort order for the two paths.
/// :nodoc:
@inlinable
public static func <(lhs: Path, rhs: Path) -> Bool {
static func <(lhs: Self, rhs: Self) -> Bool {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
}
}
@@ -406,3 +403,27 @@ private func join_<S>(prefix: String, pathComponents: S) -> String where S: Sequ
}
return rv
}
/// A path that supports arbituary dot notation, eg. Path.root.usr.bin
@dynamicMemberLookup
public struct DynamicPath: Pathish {
/// The normalized string representation of the underlying filesystem path
public let string: String
init(string: String) {
assert(string.hasPrefix("/"))
self.string = string
}
/// Converts a `Path` to a `DynamicPath`
public init(_ path: Path) {
string = path.string
}
/// :nodoc:
public subscript(dynamicMember addendum: String) -> DynamicPath {
//NOTE its possible for the string to be anything if we are invoked via
// explicit subscript thus we use our fully sanitized `join` function
return DynamicPath(string: join_(prefix: string, appending: addendum))
}
}

13
Sources/Pathish.swift Normal file
View File

@@ -0,0 +1,13 @@
/// A type that represents a filesystem path, if you conform your type
/// to `Pathish` it is your responsibility to ensure the string is correctly normalized
public protocol Pathish: Hashable, Comparable {
/// The normalized string representation of the underlying filesystem path
var string: String { get }
}
public extension Pathish {
static func ==<P: Pathish> (lhs: Self, rhs: P) -> Bool {
return lhs.string == rhs.string
}
}