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 }