1 /** 2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 The purpose of this module is to allow you to segregate your `string` data that 7 represent a path from the rest. A string-that-is-a-type have specific 8 characteristics that we want to represent. This module have two types that help 9 you encode these characteristics. 10 11 This allows you to construct type safe APIs wherein a parameter that takes a 12 path can be assured that the data **actually** is a path. The API can further 13 e.g. require the parameter to be have the even higher restriction that it is an 14 absolute path. 15 16 I have found it extremely useful in my own programs to internally only work 17 with `AbsolutePath` types. There is a boundary in my programs that takes data 18 and converts it appropriately to `AbsolutePath`s. This is usually configuration 19 data, command line input, external libraries etc. This conversion layer handles 20 the defensive coding, validity checking etc that is needed of the data. 21 22 This has overall lead to a significant reduction in the number of bugs I have 23 had when handling paths and simplified the code. The program normally look 24 something like this: 25 26 * user input as raw strings via e.g. `getopt`. 27 * wrap path strings as either `Path` or `AbsolutePath`. Prefer `AbsolutePath` 28 when applicable but there are cases where this is the wrong behavior. Lets 29 say that the user input is relative to some working directory. Then later on 30 in your program the two are combined to produce an `AbsolutePath`. 31 * internally in the program all parameters are `AbsolutePath`. A function that 32 takes an `AbsolutePath` can be assured it is a path, full expanded and thus 33 do not need any defensive code. It can use it as it is. 34 35 I have used an equivalent program structure when interacting with external 36 libraries. 37 */ 38 module my.path; 39 40 import std.range : isOutputRange, put; 41 import std.path : dirName, baseName, buildPath; 42 43 /** Types a string as a `Path` to provide path related operations. 44 * 45 * A `Path` is subtyped as a `string` in order to make it easy to integrate 46 * with the Phobos APIs that take a `string` as an argument. Example: 47 * --- 48 * auto a = Path("foo"); 49 * writeln(exists(a)); 50 * --- 51 */ 52 struct Path { 53 private string value_; 54 55 alias value this; 56 57 /// 58 this(string s) @safe pure nothrow @nogc { 59 value_ = s; 60 } 61 62 /// Returns: the underlying `string`. 63 string value() @safe pure nothrow const @nogc { 64 return value_; 65 } 66 67 /// 68 bool empty() @safe pure nothrow const @nogc { 69 return value_.length == 0; 70 } 71 72 /// 73 bool opEquals(const string s) @safe pure nothrow const @nogc { 74 return value_ == s; 75 } 76 77 /// 78 bool opEquals(const Path s) @safe pure nothrow const @nogc { 79 return value_ == s.value_; 80 } 81 82 /// 83 size_t toHash() @safe pure nothrow const @nogc scope { 84 return value_.hashOf; 85 } 86 87 /// 88 Path opBinary(string op)(string rhs) @safe { 89 static if (op == "~") { 90 return Path(buildPath(value_, rhs)); 91 } else 92 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 93 } 94 95 /// 96 Path opBinary(string op)(Path rhs) @safe { 97 static if (op == "~") { 98 return Path(buildPath(value_, rhs)); 99 } else 100 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 101 } 102 103 /// 104 void opOpAssign(string op)(string rhs) @safe nothrow { 105 static if (op == "~=") { 106 value_ = buldPath(value_, rhs); 107 } else 108 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 109 } 110 111 void opOpAssign(string op)(Path rhs) @safe nothrow { 112 static if (op == "~=") { 113 value_ = buildPath(value_, rhs); 114 } else 115 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 116 } 117 118 /// 119 T opCast(T : string)() const { 120 return value_; 121 } 122 123 /// 124 string toString() @safe pure nothrow const @nogc { 125 return value_; 126 } 127 128 /// 129 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 130 put(w, value_); 131 } 132 133 /// 134 Path dirName() @safe const { 135 return Path(value_.dirName); 136 } 137 138 /// 139 string baseName() @safe const { 140 return value_.baseName; 141 } 142 } 143 144 /** The path is guaranteed to be the absolute, normalized and tilde expanded 145 * path. 146 * 147 * An `AbsolutePath` is subtyped as a `Path` in order to make it easy to 148 * integrate with the Phobos APIs that take a `string` as an argument. Example: 149 * --- 150 * auto a = AbsolutePath("foo"); 151 * writeln(exists(a)); 152 * --- 153 * 154 * The type is optimized such that it avoids expensive operations when it is 155 * either constructed or assigned to from an `AbsolutePath`. 156 */ 157 struct AbsolutePath { 158 import std.path : buildNormalizedPath, absolutePath, expandTilde; 159 160 private Path value_; 161 162 alias value this; 163 164 /// 165 this(string p) @safe { 166 this(Path(p)); 167 } 168 169 /// 170 this(Path p) @safe { 171 value_ = Path(p.value_.expandTilde.absolutePath.buildNormalizedPath); 172 } 173 174 /// Returns: the underlying `Path`. 175 Path value() @safe pure nothrow const @nogc { 176 return value_; 177 } 178 179 /// 180 void opAssign(AbsolutePath p) @safe pure nothrow @nogc { 181 value_ = p.value_; 182 } 183 184 /// 185 void opAssign(Path p) @safe { 186 value_ = p.AbsolutePath.value_; 187 } 188 189 /// 190 Path opBinary(string op, T)(T rhs) @safe if (is(T == string) || is(T == Path)) { 191 static if (op == "~") { 192 return value_ ~ rhs; 193 } else 194 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 195 } 196 197 /// 198 void opOpAssign(string op)(T rhs) @safe if (is(T == string) || is(T == Path)) { 199 static if (op == "~=") { 200 value_ = AbsolutePath(value_ ~ rhs).value_; 201 } else 202 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 203 } 204 205 /// 206 string opCast(T : string)() pure nothrow const @nogc { 207 return value_; 208 } 209 210 /// 211 Path opCast(T : Path)() pure nothrow const @nogc { 212 return value_; 213 } 214 215 /// 216 bool opEquals(const string s) @safe pure nothrow const @nogc { 217 return value_ == s; 218 } 219 220 /// 221 bool opEquals(const Path s) @safe pure nothrow const @nogc { 222 return value_ == s.value_; 223 } 224 225 /// 226 bool opEquals(const AbsolutePath s) @safe pure nothrow const @nogc { 227 return value_ == s.value_; 228 } 229 230 /// 231 string toString() @safe pure nothrow const @nogc { 232 return cast(string) value_; 233 } 234 235 /// 236 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 237 put(w, value_); 238 } 239 240 /// 241 AbsolutePath dirName() @safe const { 242 // avoid the expensive expansions and normalizations. 243 AbsolutePath a; 244 a.value_ = value_.dirName; 245 return a; 246 } 247 248 /// 249 Path baseName() @safe const { 250 return value_.baseName.Path; 251 } 252 } 253 254 @("shall always be the absolute path") 255 unittest { 256 import std.algorithm : canFind; 257 import std.path; 258 259 assert(!AbsolutePath(Path("~/foo")).toString.canFind('~')); 260 assert(AbsolutePath(Path("foo")).toString.isAbsolute); 261 } 262 263 @("shall expand . without any trailing /.") 264 unittest { 265 import std.algorithm : canFind; 266 267 assert(!AbsolutePath(Path(".")).toString.canFind('.')); 268 assert(!AbsolutePath(Path(".")).toString.canFind('.')); 269 } 270 271 @("shall create a compile time Path") 272 unittest { 273 enum a = Path("A"); 274 } 275 276 @("shall subtype to a string") 277 unittest { 278 string a = Path("a"); 279 string b = AbsolutePath(Path("a")); 280 }