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 }