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 size_t length() @safe pure nothrow const @nogc { 74 return value_.length; 75 } 76 77 /// 78 bool opEquals(const string s) @safe pure nothrow const @nogc { 79 return value_ == s; 80 } 81 82 /// 83 bool opEquals(const Path s) @safe pure nothrow const @nogc { 84 return value_ == s.value_; 85 } 86 87 /// 88 size_t toHash() @safe pure nothrow const @nogc scope { 89 return value_.hashOf; 90 } 91 92 /// 93 Path opBinary(string op)(string rhs) @safe const { 94 static if (op == "~") { 95 return Path(buildPath(value_, rhs)); 96 } else { 97 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 98 } 99 } 100 101 /// 102 inout(Path) opBinary(string op)(const Path rhs) @safe inout { 103 static if (op == "~") { 104 return Path(buildPath(value_, rhs.value)); 105 } else 106 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 107 } 108 109 /// 110 void opOpAssign(string op)(string rhs) @safe nothrow { 111 static if (op == "~=") { 112 value_ = buldPath(value_, rhs); 113 } else 114 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 115 } 116 117 void opOpAssign(string op)(const Path rhs) @safe nothrow { 118 static if (op == "~=") { 119 value_ = buildPath(value_, rhs); 120 } else 121 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 122 } 123 124 /// 125 T opCast(T : string)() const { 126 return value_; 127 } 128 129 /// 130 string toString() @safe pure nothrow const @nogc { 131 return value_; 132 } 133 134 /// 135 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 136 put(w, value_); 137 } 138 139 /// 140 Path dirName() @safe const { 141 return Path(value_.dirName); 142 } 143 144 /// 145 string baseName() @safe const { 146 return value_.baseName; 147 } 148 } 149 150 /** The path is guaranteed to be the absolute, normalized and tilde expanded 151 * path. 152 * 153 * An `AbsolutePath` is subtyped as a `Path` in order to make it easy to 154 * integrate with the Phobos APIs that take a `string` as an argument. Example: 155 * --- 156 * auto a = AbsolutePath("foo"); 157 * writeln(exists(a)); 158 * --- 159 * 160 * The type is optimized such that it avoids expensive operations when it is 161 * either constructed or assigned to from an `AbsolutePath`. 162 */ 163 struct AbsolutePath { 164 import std.path : buildNormalizedPath, absolutePath, expandTilde; 165 166 private Path value_; 167 168 alias value this; 169 170 /// 171 this(string p) @safe { 172 this(Path(p)); 173 } 174 175 /// 176 this(Path p) @safe { 177 value_ = Path(p.value_.expandTilde.absolutePath.buildNormalizedPath); 178 } 179 180 /// 181 bool empty() @safe pure nothrow const @nogc { 182 return value_.length == 0; 183 } 184 185 /// Returns: the underlying `Path`. 186 Path value() @safe pure nothrow const @nogc { 187 return value_; 188 } 189 190 size_t length() @safe pure nothrow const @nogc { 191 return value.length; 192 } 193 194 /// 195 void opAssign(AbsolutePath p) @safe pure nothrow @nogc { 196 value_ = p.value_; 197 } 198 199 /// 200 void opAssign(Path p) @safe { 201 value_ = p.AbsolutePath.value_; 202 } 203 204 /// 205 Path opBinary(string op, T)(T rhs) @safe if (is(T == string) || is(T == Path)) { 206 static if (op == "~") { 207 return value_ ~ rhs; 208 } else 209 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 210 } 211 212 /// 213 void opOpAssign(string op)(T rhs) @safe if (is(T == string) || is(T == Path)) { 214 static if (op == "~=") { 215 value_ = AbsolutePath(value_ ~ rhs).value_; 216 } else 217 static assert(false, typeof(this).stringof ~ " does not have operator " ~ op); 218 } 219 220 /// 221 string opCast(T : string)() pure nothrow const @nogc { 222 return value_; 223 } 224 225 /// 226 Path opCast(T : Path)() pure nothrow const @nogc { 227 return value_; 228 } 229 230 /// 231 bool opEquals(const string s) @safe pure nothrow const @nogc { 232 return value_ == s; 233 } 234 235 /// 236 bool opEquals(const Path s) @safe pure nothrow const @nogc { 237 return value_ == s.value_; 238 } 239 240 /// 241 bool opEquals(const AbsolutePath s) @safe pure nothrow const @nogc { 242 return value_ == s.value_; 243 } 244 245 /// 246 string toString() @safe pure nothrow const @nogc { 247 return cast(string) value_; 248 } 249 250 /// 251 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 252 put(w, value_); 253 } 254 255 /// 256 AbsolutePath dirName() @safe const { 257 // avoid the expensive expansions and normalizations. 258 AbsolutePath a; 259 a.value_ = value_.dirName; 260 return a; 261 } 262 263 /// 264 Path baseName() @safe const { 265 return value_.baseName.Path; 266 } 267 } 268 269 @("shall always be the absolute path") 270 unittest { 271 import std.algorithm : canFind; 272 import std.path; 273 274 assert(!AbsolutePath(Path("~/foo")).toString.canFind('~')); 275 assert(AbsolutePath(Path("foo")).toString.isAbsolute); 276 } 277 278 @("shall expand . without any trailing /.") 279 unittest { 280 import std.algorithm : canFind; 281 282 assert(!AbsolutePath(Path(".")).toString.canFind('.')); 283 assert(!AbsolutePath(Path(".")).toString.canFind('.')); 284 } 285 286 @("shall create a compile time Path") 287 unittest { 288 enum a = Path("A"); 289 } 290 291 @("shall subtype to a string") 292 unittest { 293 string a = Path("a"); 294 string b = AbsolutePath(Path("a")); 295 } 296 297 @("shall build path from path ~ string") 298 unittest { 299 import std.file : getcwd; 300 import std.meta : AliasSeq; 301 import std.stdio; 302 303 static foreach (T; AliasSeq!(string, Path)) { 304 { 305 const a = Path("foo"); 306 const T b = "smurf"; 307 Path c = a ~ b; 308 assert(c.value == "foo/smurf"); 309 } 310 } 311 312 static foreach (T; AliasSeq!(string, Path)) { 313 { 314 const a = Path("foo"); 315 const T b = "smurf"; 316 AbsolutePath c = a ~ b; 317 assert(c.value.value == buildPath(getcwd, "foo", "smurf")); 318 } 319 } 320 }