100% documentation please

This commit is contained in:
Max Howell
2019-01-19 14:36:27 -05:00
parent 29149da72b
commit 3eda9a9741
8 changed files with 146 additions and 9 deletions

View File

@@ -1,26 +1,31 @@
import Foundation import Foundation
public extension Bundle { public extension Bundle {
/// Returns the path for requested resource in this bundle.
func path(forResource: String, ofType: String?) -> Path? { func path(forResource: String, ofType: String?) -> Path? {
let f: (String?, String?) -> String? = path(forResource:ofType:) let f: (String?, String?) -> String? = path(forResource:ofType:)
let str = f(forResource, ofType) let str = f(forResource, ofType)
return str.flatMap(Path.init) return str.flatMap(Path.init)
} }
/// Returns the path for the shared-frameworks directory in this bundle.
public var sharedFrameworks: Path? { public var sharedFrameworks: Path? {
return sharedFrameworksPath.flatMap(Path.init) return sharedFrameworksPath.flatMap(Path.init)
} }
/// Returns the path for the resources directory in this bundle.
public var resources: Path? { public var resources: Path? {
return resourcePath.flatMap(Path.init) return resourcePath.flatMap(Path.init)
} }
/// Returns the path for this bundle.
public var path: Path { public var path: Path {
return Path(string: bundlePath) return Path(string: bundlePath)
} }
} }
public extension String { public extension String {
/// Initializes this `String` with the contents of the provided path.
@inlinable @inlinable
init(contentsOf path: Path) throws { init(contentsOf path: Path) throws {
try self.init(contentsOfFile: path.string) try self.init(contentsOfFile: path.string)
@@ -36,6 +41,7 @@ public extension String {
} }
public extension Data { public extension Data {
/// Initializes this `Data` with the contents of the provided path.
@inlinable @inlinable
init(contentsOf path: Path) throws { init(contentsOf path: Path) throws {
try self.init(contentsOf: path.url) try self.init(contentsOf: path.url)

View File

@@ -43,14 +43,18 @@ public extension Path {
return self return self
} }
/// - Returns: modification-time or creation-time if none /**
Returns the modification-time.
- Note: Returns the creation time if there is no modification time.
- Note: Returns UNIX-time-zero if neither are available, though this *should* be impossible.
*/
public var mtime: Date { public var mtime: Date {
do { do {
let attrs = try FileManager.default.attributesOfItem(atPath: string) let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date() return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date()
} catch { } catch {
//TODO print(error) //TODO log error
return Date() return Date(timeIntervalSince1970: 0)
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import Foundation import Foundation
public extension CodingUserInfoKey { public extension CodingUserInfoKey {
/// If set paths are encoded as relative to this path.
static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")! static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")!
} }

View File

@@ -41,6 +41,14 @@ public extension Path {
return rv return rv
} }
/**
Moves a file.
- Note: `throws` if `to` is a directory.
- Parameter to: Destination filename.
- Parameter overwrite: If true overwrites any file that already exists at `to`.
- Returns: `to` to allow chaining
- SeeAlso: move(into:overwrite:)
*/
@discardableResult @discardableResult
public func move(to: Path, overwrite: Bool = false) throws -> Path { public func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists { if overwrite, to.exists {
@@ -50,6 +58,17 @@ public extension Path {
return to return to
} }
/**
Moves a file into a directory
If the destination does not exist, this function creates the directory first.
- Note: `throws` if `into` is a file.
- Parameter into: Destination directory
- Parameter overwrite: If true overwrites any file that already exists at `into`.
- Returns: The `Path` of destination filename.
- SeeAlso: move(into:overwrite:)
*/
@discardableResult @discardableResult
public func move(into: Path) throws -> Path { public func move(into: Path) throws -> Path {
if !into.exists { if !into.exists {
@@ -62,17 +81,23 @@ public extension Path {
return rv return rv
} }
/// Deletes the path, recursively if a directory.
@inlinable @inlinable
public func delete() throws { public func delete() throws {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
} }
/**
Creates an empty file at this path.
- Returns: `self` to allow chaining.
*/
@inlinable @inlinable
@discardableResult @discardableResult
func touch() throws -> Path { func touch() throws -> Path {
return try "".write(to: self) return try "".write(to: self)
} }
/// Helper due to Linux Swift being incomplete.
private func _foo(go: () throws -> Void) throws { private func _foo(go: () throws -> Void) throws {
#if !os(Linux) #if !os(Linux)
do { do {
@@ -92,6 +117,11 @@ public extension Path {
#endif #endif
} }
/**
Creates the directory at this path.
- Note: Does not create any intermediary directories.
- Returns: `self` to allow chaining.
*/
@discardableResult @discardableResult
public func mkdir() throws -> Path { public func mkdir() throws -> Path {
try _foo { try _foo {
@@ -100,6 +130,11 @@ public extension Path {
return self return self
} }
/**
Creates the directory at this path.
- Note: Creates any intermediary directories, if required.
- Returns: `self` to allow chaining.
*/
@discardableResult @discardableResult
public func mkpath() throws -> Path { public func mkpath() throws -> Path {
try _foo { try _foo {
@@ -108,8 +143,15 @@ public extension Path {
return self return self
} }
/// - Note: If file doesnt exist, creates file /**
/// - Note: If file is not writable, makes writable first, resetting permissions after the write Replaces the contents of the file at this path with the provided string.
- Note: If file doesnt exist, creates file
- Note: If file is not writable, makes writable first, resetting permissions after the write
- Parameter contents: The string that will become the contents of this file.
- Parameter atomically: If `true` the operation will be performed atomically.
- Parameter encoding: The string encoding to use.
- Returns: `self` to allow chaining.
*/
@discardableResult @discardableResult
public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path { public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path {
let resetPerms: Int? let resetPerms: Int?

View File

@@ -9,12 +9,14 @@ extension Path: LosslessStringConvertible {
} }
extension Path: CustomStringConvertible { extension Path: CustomStringConvertible {
/// Returns `Path.string`
public var description: String { public var description: String {
return string return string
} }
} }
extension Path: CustomDebugStringConvertible { extension Path: CustomDebugStringConvertible {
/// Returns eg. `Path(string: "/foo")`
public var debugDescription: String { public var debugDescription: String {
return "Path(string: \(string))" return "Path(string: \(string))"
} }

View File

@@ -13,12 +13,14 @@ public extension Path {
} }
public extension Array where Element == Path.Entry { public extension Array where Element == Path.Entry {
/// Filters the list of entries to be a list of Paths that are directories.
var directories: [Path] { var directories: [Path] {
return compactMap { return compactMap {
$0.kind == .directory ? $0.path : nil $0.kind == .directory ? $0.path : nil
} }
} }
/// Filters the list of entries to be a list of Paths that are files with the specified extension
func files(withExtension ext: String) -> [Path] { func files(withExtension ext: String) -> [Path] {
return compactMap { return compactMap {
$0.kind == .file && $0.path.extension == ext ? $0.path : nil $0.kind == .file && $0.path.extension == ext ? $0.path : nil

View File

@@ -1,24 +1,29 @@
import Foundation import Foundation
public extension Path { public extension Path {
/// Returns true if the path represents an actual file that is also writable by the current user.
var isWritable: Bool { var isWritable: Bool {
return FileManager.default.isWritableFile(atPath: string) return FileManager.default.isWritableFile(atPath: string)
} }
/// Returns true if the path represents an actual directory.
var isDirectory: Bool { var isDirectory: Bool {
var isDir: ObjCBool = false var isDir: ObjCBool = false
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue
} }
/// Returns true if the path represents an actual filesystem entry that is *not* a directory.
var isFile: Bool { var isFile: Bool {
var isDir: ObjCBool = true var isDir: ObjCBool = true
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue
} }
/// Returns true if the path represents an actual file that is also executable by the current user.
var isExecutable: Bool { var isExecutable: Bool {
return FileManager.default.isExecutableFile(atPath: string) return FileManager.default.isExecutableFile(atPath: string)
} }
/// Returns true if the path represents an actual filesystem entry.
var exists: Bool { var exists: Bool {
return FileManager.default.fileExists(atPath: string) return FileManager.default.fileExists(atPath: string)
} }

View File

@@ -1,16 +1,32 @@
import Foundation import Foundation
/**
Represents a platform filesystem absolute path.
The recommended conversions from string are:
let p1 = Path.root/pathString
let p2 = Path.root/url.path
let p3 = Path.cwd/relativePathString
let p4 = Path(userInput) ?? Path.cwd/userInput
- Note: There may not be an actual filename at the path.
*/
public struct Path: Equatable, Hashable, Comparable { public struct Path: Equatable, Hashable, Comparable {
/// The underlying filesystem path
public let string: String public let string: String
/// Returns a `Path` containing ``FileManager.default.currentDirectoryPath`.
public static var cwd: Path { public static var cwd: Path {
return Path(string: FileManager.default.currentDirectoryPath) return Path(string: FileManager.default.currentDirectoryPath)
} }
/// Returns a `Path` representing the root path.
public static var root: Path { public static var root: Path {
return Path(string: "/") return Path(string: "/")
} }
/// Returns a `Path` representing the users home directory
public static var home: Path { public static var home: Path {
let string: String let string: String
#if os(macOS) #if os(macOS)
@@ -25,21 +41,42 @@ public struct Path: Equatable, Hashable, Comparable {
return Path(string: string) return Path(string: string)
} }
/**
Returns the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`.
*/
@inlinable @inlinable
public var `extension`: String { public var `extension`: String {
return (string as NSString).pathExtension return (string as NSString).pathExtension
} }
/// - Note: always returns a valid path, `Path.root.parent` *is* `Path.root` /**
Returns the parent directory for this path.
Path is not aware of the nature of the underlying file, but this is
irrlevant since the operation is the same irrespective of this fact.
- Note: always returns a valid path, `Path.root.parent` *is* `Path.root`.
*/
public var parent: Path { public var parent: Path {
return Path(string: (string as NSString).deletingLastPathComponent) return Path(string: (string as NSString).deletingLastPathComponent)
} }
/// Returns a `URL` representing this file path.
@inlinable @inlinable
public var url: URL { public var url: URL {
return URL(fileURLWithPath: string) return URL(fileURLWithPath: string)
} }
/**
The basename for the provided file, optionally dropping the file extension.
Path.root.join("foo.swift").basename() // => "foo.swift"
Path.root.join("foo.swift").basename(dropExtension: true) // => "foo"
- 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 { public func basename(dropExtension: Bool = false) -> String {
let str = string as NSString let str = string as NSString
if !dropExtension { if !dropExtension {
@@ -54,7 +91,13 @@ public struct Path: Equatable, Hashable, Comparable {
} }
} }
//TODO another variant that returns `nil` if result would start with `..` /**
Returns a string representing the relative path to `base`.
- Note: If `base` is not a logical prefix for `self` your result will be prefixed some number of `../` components.
- 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 { public func relative(to base: Path) -> String {
// Split the two paths into their components. // Split the two paths into their components.
// FIXME: The is needs to be optimized to avoid unncessary copying. // FIXME: The is needs to be optimized to avoid unncessary copying.
@@ -85,27 +128,59 @@ public struct Path: Equatable, Hashable, Comparable {
} }
} }
public func join<S>(_ part: S) -> Path where S: StringProtocol { /**
Joins a path and a string to produce a new path.
Path.root.join("a") // => /a
Path.root.join("a/b") // => /a/b
Path.root.join("a").join("b") // => /a/b
Path.root.join("a").join("/b") // => /a/b
- Parameter pathComponent: The string to join with this path.
- Returns: A new joined path.
- SeeAlso: /(:Path,:String)
*/
public func join<S>(_ pathComponent: S) -> Path where S: StringProtocol {
//TODO standardizingPath does more than we want really (eg tilde expansion) //TODO standardizingPath does more than we want really (eg tilde expansion)
let str = (string as NSString).appendingPathComponent(String(part)) let str = (string as NSString).appendingPathComponent(String(pathComponent))
return Path(string: (str as NSString).standardizingPath) return Path(string: (str as NSString).standardizingPath)
} }
/// Returns the locale-aware sort order for the two paths.
@inlinable @inlinable
public static func <(lhs: Path, rhs: Path) -> Bool { public static func <(lhs: Path, rhs: Path) -> Bool {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
} }
/// A file entry from a directory listing.
public struct Entry { public struct Entry {
/// The kind of this directory entry.
public enum Kind { public enum Kind {
/// The path is a file.
case file case file
/// The path is a directory.
case directory case directory
} }
/// The kind of this entry.
public let kind: Kind public let kind: Kind
/// The path of this entry.
public let path: Path public let path: Path
} }
} }
/**
Joins a path and a string to produce a new path.
Path.root/"a" // => /a
Path.root/"a/b" // => /a/b
Path.root/"a"/"b" // => /a/b
Path.root/"a"/"/b" // => /a/b
- Parameter lhs: The base path to join with `rhs`.
- Parameter rhs: The string to join with this `lhs`.
- Returns: A new joined path.
- SeeAlso: Path.join(_:)
*/
@inlinable @inlinable
public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol { public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs) return lhs.join(rhs)