1 /++ File system path and name types and their operations.
2 
3 	See: https://en.cppreference.com/w/cpp/filesystem/path
4 	See: https://hackage.haskell.org/package/FileSystem-1.0.0/docs/System-FileSystem-Types.html
5 	See_Also: `dmd.root.filename`
6  +/
7 module nxt.path;
8 
9 import nxt.algorithm.searching : canFind, stripRight;
10 private import std.path : std_expandTilde = expandTilde, std_buildPath = buildPath, std_buildNormalizedPath = buildNormalizedPath, std_baseName = baseName, dirSeparator;
11 
12 @safe:
13 
14 /++ Path.
15 
16 	The concept of a "pure path" doesn't need to be modelled in D as
17 	it has `pure` functions.  See
18 	https://docs.python.org/3/library/pathlib.html#pure-paths.
19 
20 	See: SUMO:`ComputerPath`.
21  +/
22 struct Path {
23 	this(string str, in bool normalize = false) pure nothrow @nogc {
24 		this.str = normalize ? str.stripRight(dirSeparator) : str;
25 	}
26 	string str;
27 pure nothrow @nogc:
28 	bool opCast(T : bool)() const scope => str !is null;
29 	string toString() inout @property => str;
30 }
31 
32 ///
33 @safe pure nothrow unittest {
34 	assert(Path("/usr/bin/").toString == "/usr/bin/");
35 }
36 
37 /++ (Regular) File path.
38 	See: https://hackage.haskell.org/package/filepath-1.5.0.0/docs/System-FilePath.html#t:FilePath
39  +/
40 struct FilePath {
41 	this(string str, in bool normalize = false) pure nothrow @nogc {
42 		this.path = Path(str, normalize);
43 	}
44 	Path path;
45 	alias path this;
46 }
47 
48 ///
49 @safe pure nothrow unittest {
50 	assert(FilePath("/usr/bin/") == FilePath("/usr/bin/"));
51 	assert(FilePath("/usr/bin/") == Path("/usr/bin/"));
52 	assert(FilePath("foo") == Path("foo"));
53 	assert(FilePath("foo", false).str == "foo");
54 	assert(FilePath("foo", true).str == "foo");
55 	assert(Path("/etc/", false).str == "/etc/");
56 	assert(Path("/etc/", true).str == "/etc");
57 	assert(Path("foo", true).str == "foo");
58 }
59 
60 /++ Directory path.
61 	See: SUMO:`ComputerDirectory`.
62  +/
63 struct DirPath {
64 	this(string path, in bool normalize = false) pure nothrow @nogc {
65 		this.path = Path(path, normalize);
66 	}
67 	Path path;
68 	alias path this;
69 }
70 
71 ///
72 @safe pure nothrow unittest {
73 	assert(DirPath("/etc") == Path("/etc"));
74 	assert(DirPath("/etc") == DirPath("/etc"));
75 	assert(DirPath("/etc/", false).str == "/etc/");
76 	assert(DirPath("/etc/", true).str == "/etc");
77 }
78 
79 /++ File (local) name.
80  +/
81 struct FileName {
82 	this(string str) pure nothrow @nogc in (!str.canFind('/')) {
83 		this.str = str;
84 	}
85 	string str;
86 pure nothrow @nogc:
87 	bool opCast(T : bool)() const scope => str !is null;
88 	string toString() inout @property => str;
89 }
90 
91 ///
92 pure nothrow @safe unittest {
93 	assert(FileName(".emacs").str == ".emacs");
94 	assert(FileName(".emacs").toString == ".emacs");
95 }
96 
97 /++ Directory (local) name.
98  +/
99 struct DirName {
100 	this(string str) pure nothrow @nogc in (!str.canFind('/')) {
101 		this.str = str;
102 	}
103 	string str;
104 pure nothrow @nogc:
105 	bool opCast(T : bool)() const scope => str !is null;
106 	string toString() inout @property => str;
107 }
108 
109 ///
110 pure nothrow @safe unittest {
111 	assert(DirName(".emacs.d").str == ".emacs.d");
112 	assert(DirName(".emacs.d").toString == ".emacs.d");
113 }
114 
115 /++ Execute file path.
116  +/
117 struct ExePath {
118 	this(string path) pure nothrow @nogc {
119 		this.path = Path(path);
120 	}
121 	Path path;
122 	alias path this;
123 }
124 
125 ///
126 @safe pure nothrow unittest {
127 	assert(ExePath("/usr/bin/") == Path("/usr/bin/"));
128 }
129 
130 /++ Written (input) path. +/
131 struct RdPath {
132 	this(string str) pure nothrow @nogc {
133 		this.path = Path(str);
134 	}
135 	Path path;
136 	alias path this;
137 }
138 
139 ///
140 @safe pure nothrow unittest {
141 	assert(RdPath("/usr/bin/") == Path("/usr/bin/"));
142 }
143 
144 /++ Written (output) path. +/
145 struct WrPath {
146 	this(string str) pure nothrow @nogc {
147 		this.path = Path(str);
148 	}
149 	Path path;
150 	alias path this;
151 }
152 
153 ///
154 @safe pure nothrow unittest {
155 	assert(WrPath("/usr/bin/") == Path("/usr/bin/"));
156 }
157 
158 /// Expand tilde in `a`.
159 FilePath expandTilde(FilePath a) nothrow => typeof(return)(std_expandTilde(a.path.str));
160 /// ditto
161 DirPath expandTilde(DirPath a) nothrow => typeof(return)(std_expandTilde(a.path.str));
162 
163 ///
164 @safe nothrow unittest {
165 	assert(FilePath("~").expandTilde);
166 	assert(DirPath("~").expandTilde);
167 }
168 
169 /// Build path `a`/`b`.
170 FilePath buildPath(DirPath a, FilePath b) pure nothrow => typeof(return)(std_buildPath(a.path.str, b.path.str));
171 /// ditto
172 DirPath buildPath(DirPath a, DirPath b) pure nothrow => typeof(return)(std_buildPath(a.path.str, b.path.str));
173 /// ditto
174 DirPath buildPath(DirPath a, DirName b) pure nothrow => typeof(return)(std_buildPath(a.path.str, b.str));
175 /// ditto
176 FilePath buildPath(DirPath a, FileName b) pure nothrow => typeof(return)(std_buildPath(a.path.str, b.str));
177 
178 ///
179 @safe pure nothrow unittest {
180 	assert(DirPath("/etc").buildPath(FileName("foo")) == FilePath("/etc/foo"));
181 	assert(DirPath("/etc").buildPath(FilePath("foo")) == FilePath("/etc/foo"));
182 	assert(DirPath("/usr").buildPath(DirName("bin")) == DirPath("/usr/bin"));
183 	assert(DirPath("/usr").buildPath(DirPath("bin")) == DirPath("/usr/bin"));
184 	assert(DirPath("/usr").buildPath(DirPath("/bin")) == DirPath("/bin"));
185 }
186 
187 /// Build path `a`/`b`.
188 FilePath buildNormalizedPath(DirPath a, FilePath b) pure nothrow => typeof(return)(std_buildNormalizedPath(a.path.str, b.path.str));
189 /// ditto
190 DirPath buildNormalizedPath(DirPath a, DirPath b) pure nothrow => typeof(return)(std_buildNormalizedPath(a.path.str, b.path.str));
191 /// ditto
192 DirPath buildNormalizedPath(DirPath a, DirName b) pure nothrow => typeof(return)(std_buildNormalizedPath(a.path.str, b.str));
193 /// ditto
194 FilePath buildNormalizedPath(DirPath a, FileName b) pure nothrow => typeof(return)(std_buildNormalizedPath(a.path.str, b.str));
195 
196 ///
197 @safe pure nothrow unittest {
198 	assert(DirPath("/etc").buildNormalizedPath(FileName("foo")) == FilePath("/etc/foo"));
199 	assert(DirPath("/etc").buildNormalizedPath(FilePath("foo")) == FilePath("/etc/foo"));
200 	assert(DirPath("/usr").buildNormalizedPath(DirName("bin")) == DirPath("/usr/bin"));
201 	assert(DirPath("/usr").buildNormalizedPath(DirPath("bin")) == DirPath("/usr/bin"));
202 	assert(DirPath("/usr").buildNormalizedPath(DirPath("/bin")) == DirPath("/bin"));
203 }
204 
205 /++ URL.
206  +/
207 struct URL {
208 	string str;
209 pure nothrow @nogc:
210 	bool opCast(T : bool)() const scope => str !is null;
211 	string toString() inout @property => str;
212 }
213 
214 ///
215 @safe pure nothrow unittest {
216 	assert(URL("www.sunet.se").toString == "www.sunet.se");
217 }
218 
219 /++ File URL.
220  +/
221 struct FileURL {
222 	string str;
223 pure nothrow @nogc:
224 	bool opCast(T : bool)() const scope => str !is null;
225 	string toString() inout @property => str;
226 }
227 
228 ///
229 @safe pure nothrow unittest {
230 	assert(FileURL("www.sunet.se").toString == "www.sunet.se");
231 }
232 
233 /++ Directory URL.
234  +/
235 struct DirURL {
236 	string str;
237 pure nothrow @nogc:
238 	bool opCast(T : bool)() const scope => str !is null;
239 	string toString() inout @property => str;
240 }
241 
242 ///
243 @safe pure nothrow unittest {
244 	assert(DirURL("www.sunet.se").toString == "www.sunet.se");
245 }
246 
247 /++ File Offset URL.
248  +/
249 struct FileURLOffset {
250 	import nxt.offset : Offset;
251 	FileURL url;
252 	Offset offset;
253 }
254 
255 /++ File Region URL.
256  +/
257 struct FileURLRegion {
258 	import nxt.region : Region;
259 	FileURL url;
260 	Region region;
261 }
262 
263 /// Get basename of `a`.
264 FileName baseName(FilePath a) pure nothrow @nogc => typeof(return)(std_baseName(a.str));
265 /// ditto
266 DirName baseName(DirPath a) pure nothrow @nogc => typeof(return)(std_baseName(a.str));
267 /// ditto
268 DirName baseName(URL a) pure nothrow @nogc => typeof(return)(std_baseName(a.str));
269 /// ditto
270 FileName baseName(FileURL a) pure nothrow @nogc => typeof(return)(std_baseName(a.str));
271 /// ditto
272 DirName baseName(DirURL a) pure nothrow @nogc => typeof(return)(std_baseName(a.str));
273 
274 ///
275 version (Posix) nothrow @safe unittest {
276 	assert(FilePath("/etc/foo").baseName.str == "foo");
277 	assert(DirPath("/etc/").baseName.str == "etc");
278 	const dmd = "https://github.com/dlang/dmd/";
279 	assert(URL(dmd).baseName.str == "dmd");
280 	assert(FileURL(dmd).baseName.str == "dmd");
281 	assert(DirURL(dmd).baseName.str == "dmd");
282 }
283 
284 private import std.file : std_exists = exists;
285 
286 /// Check if `a` exists.
287 bool exists(in Path a) nothrow @nogc => typeof(return)(std_exists(a.str));
288 /// ditto
289 bool exists(in FilePath a) nothrow @nogc => typeof(return)(std_exists(a.str));
290 /// ditto
291 bool exists(in DirPath a) nothrow @nogc => typeof(return)(std_exists(a.str));
292 /// ditto
293 bool exists(in FileName a) nothrow @nogc => typeof(return)(std_exists(a.str));
294 /// ditto
295 bool exists(in DirName a) nothrow @nogc => typeof(return)(std_exists(a.str));
296 
297 /// verify `a.toString` when `a` being `scope` parameter
298 version (none) // TODO: Remove or make compile
299 @safe pure nothrow @nogc unittest {
300 	import std.meta : AliasSeq;
301 
302 	static foreach (T; AliasSeq!(Path, FilePath, DirPath, FileName, DirName)) {{
303 		static void f(in T a) { const _ = a.toString; }
304 		f(T.init);
305 	}}
306 }
307 
308 ///
309 version (Posix) nothrow @safe unittest {
310 	assert( Path("/etc/").exists);
311 	assert(!Path("/etcxyz/").exists);
312 	assert( DirPath("/etc/").exists);
313 	assert(!DirPath("/etcxyz/").exists);
314 	assert( FilePath("/etc/passwd").exists);
315 	assert(!FileName("dsfdsfdsfdsfdfdsf").exists);
316 	assert(!DirName("dsfdsfdsfdsfdfdsf").exists);
317 }