1 /**
2 Provides some convenience functionality for parsing command-line arguments by piggy-backing off std.getopt.
3 
4 Authors: Tony J. Hudgins
5 Copyright: Copyright © 2019, Tony J. Hudgins
6 License: MIT
7 */
8 module ensure;
9 
10 import std.traits;
11 import std.range : isInputRange, hasLength;
12 import std.format : format;
13 
14 private enum isValidRangeSpecifier( string spec ) =
15     spec !is null &&
16     spec.length == 2 &&
17     ( spec[0] == '(' || spec[0] == '[' ) &&
18     ( spec[1] == ')' || spec[1] == ']' );
19 
20 private enum supportsOperation( T, string op ) =
21     is( typeof( { bool _ = mixin( "T.init " ~ op ~ " T.init" ); } ) );
22 
23 /++
24 A convenience wrapper for __traits( identifier, ... ).
25 
26 Authors: Tony J. Hudgins
27 Copyright: Copyright © 2019, Tony J. Hudgins
28 License: MIT
29 +/
30 enum nameof( alias symbol ) = __traits( identifier, symbol );
31 
32 @system unittest
33 {
34     struct Foo
35     {
36         int x;
37     }
38 
39     int a;
40     Foo b;
41 
42     // test locals
43     assert( nameof!a == "a" );
44 
45     // test members
46     assert( nameof!( b.x ) == "x" );
47 
48     // test types
49     assert( nameof!Foo == "Foo" );
50 
51     // test modules
52     assert( nameof!ensure == "ensure" );
53 }
54 
55 /++
56 Tests if a type can null. Not related to std.typecons.Nullable!T.
57 
58 Authors: Tony J. Hudgins
59 Copyright: Copyright © 2019, Tony J. Hudgins
60 License: MIT
61 +/
62 enum isNullable( T ) = is( typeof( { Unqual!T _ = null; } ) );
63 
64 @system unittest
65 {
66     assert( isNullable!string );
67     assert( !isNullable!int );
68 }
69 
70 /++
71 Thrown for any validation errors.
72 
73 Authors: Tony J. Hudgins
74 Copyright: Copyright © 2019, Tony J. Hudgins
75 License: MIT
76 +/
77 final class EnsureException : Exception
78 {
79     private string _paramName;
80 
81     string paramName() const pure nothrow @trusted @property
82     {
83         return this._paramName;
84     }
85 
86     this( string paramName, string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null )
87     {
88         this._paramName = paramName;
89         auto newMsg = paramName is null || paramName.length == 0 ? msg : "%s: %s".format( paramName, msg );
90         super( newMsg, file, line, next );
91     }
92 }
93 
94 /++
95 Represents a single function parameter, including name and value, upon which all validation is performed.
96 
97 Authors: Tony J. Hudgins
98 Copyright: Copyright © 2019, Tony J. Hudgins
99 License: MIT
100 +/
101 struct Arg( T )
102 {
103     private {
104         string _paramName;
105         T _value;
106     }
107 
108     string paramName() const pure nothrow @trusted @property
109     {
110         return this._paramName;
111     }
112 
113     T value() const pure nothrow @trusted @property
114     {
115         return this._value;
116     }
117 
118     /// Default construction is not allowed.
119     this() @disable;
120 
121     private this( string paramName, T value )
122     {
123         this._paramName = paramName;
124         this._value = value;
125     }
126 
127     /++
128     Throws an EnsureException for the current parameter with the given message.
129 
130     Params:
131         msg = The message to pass to the exception's constructor.
132         file = Used for passing the appropriate file name to the exception's constructor.
133         line = Used for passing the appropriate line number to the exception's constructor.
134 
135     Throws: EnsureException whenever it's called.
136     Authors: Tony J. Hudgins
137     Copyright: Copyright © 2019, Tony J. Hudgins
138     License: MIT
139     +/
140     void throwWith( string msg, string file = __FILE__, size_t line = __LINE__ ) const @trusted
141     {
142         throw new EnsureException( this._paramName, msg, file, line );
143     }
144 }
145 
146 /++
147 Wrap a function argument to perform validation.
148 
149 Authors: Tony J. Hudgins
150 Copyright: Copyright © 2019, Tony J. Hudgins
151 License: MIT
152 +/
153 Arg!( typeof( what ) ) ensure( alias what )()
154 {
155     return Arg!( typeof( what ) )( nameof!what, what );
156 }
157 
158 /++
159 Ensures that the argument is not null.
160 
161 Params:
162     arg = A wrapped argument, obtained with ensure.
163 
164 See_Also: ensure
165 Throws: EnsureException when validation fails.
166 Authors: Tony J. Hudgins
167 Copyright: Copyright © 2019, Tony J. Hudgins
168 License: MIT
169 +/
170 Arg!T isNotNull( T )( Arg!T arg ) //if( isNullable!T )
171 {
172     if( arg.value is null )
173         arg.throwWith( "argument cannot be null" );
174 
175     return arg;
176 }
177 
178 /++
179 Ensures that an array is not empty.
180 
181 Params:
182     arg = A wrapped argument, obtained with ensure.
183 
184 See_Also: ensure
185 Throws: EnsureException when validation fails.
186 Authors: Tony J. Hudgins
187 Copyright: Copyright © 2019, Tony J. Hudgins
188 License: MIT
189 +/
190 Arg!T isNotEmpty( T )( Arg!T arg ) if( isArray!T )
191 {
192     static if( isSomeString!T )
193         enum kind = "string";
194     else static if( isAssociativeArray!T )
195         enum kind = "associative array";
196     else
197         enum kind = "array";
198 
199     if( arg.value.length == 0 )
200         arg.throwWith( kind ~ " cannot be empty" );
201 
202     return arg;
203 }
204 
205 /++
206 Ensures that a range is not empty.
207 
208 Params:
209     arg = A wrapped argument, obtained with ensure.
210 
211 See_Also: ensure
212 Throws: EnsureException when validation fails.
213 Authors: Tony J. Hudgins
214 Copyright: Copyright © 2019, Tony J. Hudgins
215 License: MIT
216 +/
217 Arg!T isNotEmpty( T )( Arg!T arg ) if( !isArray!T && isInputRange!T )
218 {
219     if( arg.value.empty )
220         arg.throwWith( "range cannot be empty" );
221 
222     return arg;
223 }
224 
225 /++
226 Ensures that a string does not contain only whitespace characters.
227 
228 Params:
229     arg = A wrapped argument, obtained with ensure.
230 
231 See_Also: ensure
232 Throws: EnsureException when validation fails.
233 Authors: Tony J. Hudgins
234 Copyright: Copyright © 2019, Tony J. Hudgins
235 License: MIT
236 +/
237 Arg!S isNotWhitespace( S )( Arg!S arg ) if( isSomeString!S )
238 {
239     import std.algorithm : all;
240     import std.uni : isWhite;
241 
242     if( arg.isNotNull.value.all!isWhite )
243         arg.throwWith( "string cannot consist of only whitespace characters" );
244 
245     return arg;
246 }
247 
248 /++
249 Ensures that a number is between an upper bound and a lower bound.
250 
251 By default, this function's bounds are **inclusive**, inclusivity/exclusivity can be changed
252 on a per-bound basis by passing in a string template argument similar to std.random.uniform.
253 
254 The string template parameter expects a 2-character long string consisting of an
255 opening parenthesis or opening square bracket, followed by a closing parenthesis or closing square bracket.
256 The opening bracket is for the lower bound, while the closing bracket is for the upper bound.
257 Parenteses indicate that the boundary is inclusive, and square brackets indicate the boundary is exclusive.
258 
259 All possible combinations:
260 
261 - **()** - Both lower bound and upper bound are inclusive.
262 - **(]** - Lower bound is inclusive and upper bound is exclusive.
263 - **[)** - Lower bound is exclusive and upper bound is inclusive.
264 - **[]** - Both lower bound and upper bound are exclusive.
265 
266 Params:
267     arg = A wrapped argument, obtained with ensure.
268 
269 Examples:
270 ---------
271 const zero = 0;
272 const five = 5;
273 
274 ensure!(zero).between!"[)"( 0, 5 ); // exception, lower bound is exclusive
275 ---------
276 
277 See_Also: ensure
278 Throws: EnsureException when validation fails.
279 Authors: Tony J. Hudgins
280 Copyright: Copyright © 2019, Tony J. Hudgins
281 License: MIT
282 +/
283 Arg!N between( string how = "()", N )( Arg!N arg, N lowerBound, N upperBound )
284     if( isNumeric!N && isValidRangeSpecifier!how )
285 {
286     enum lowerOp = how[0] == '(' ? "<" : "<=";
287     enum upperOp = how[1] == ')' ? ">" : ">=";
288 
289     static enum Bound { lower, upper }
290 
291     string err( Bound bound, bool inclusive, N value )
292     {
293         import std.array : appender;
294 
295         auto msg = appender!string;
296         msg.put( "argument (%s) is ".format( arg.value ) );
297 
298         with( Bound )
299         final switch( bound )
300         {
301             case lower:
302                 msg.put( "less than " );
303                 break;
304 
305             case upper:
306                 msg.put( "greater than " );
307                 break;
308         }
309 
310         if( !inclusive )
311             msg.put( "or equal to " );
312 
313         msg.put( "the " );
314 
315         with( Bound )
316         final switch( bound )
317         {
318             case lower:
319                 msg.put( "lower " );
320                 break;
321 
322             case upper:
323                 msg.put( "upper " );
324                 break;
325         }
326 
327         msg.put( "bound (%s)".format( value ) );
328 
329         return msg.data;
330     }
331 
332     if( mixin( "arg.value" ~ lowerOp ~ "lowerBound" ) )
333         arg.throwWith( err( Bound.lower, how[0] == '(', lowerBound ) );
334 
335     if( mixin( "arg.value" ~ upperOp ~ "upperBound" ) )
336         arg.throwWith( err( Bound.upper, how[1] == ')', upperBound ) );
337 
338     return arg;
339 }
340 
341 // auto-generate some functions for common boolean operations.
342 private static immutable ops = [
343     ">": [ "greaterThan", "gt" ],
344     ">=": [ "greaterThanOrEqualTo", "gte" ],
345 
346     "<": [ "lessThan", "lt" ],
347     "<=": [ "lessThanOrEqualTo", "lte" ],
348 
349     "==": [ "equalTo", "eq" ],
350     "!=": [ "notEqualTo", "ne", "neq" ],
351 ];
352 
353 static foreach( op, names; ops )
354 static foreach( name; names )
355 {
356     mixin( `
357         Arg!T %01$s( T )( Arg!T arg, T value ) if( supportsOperation!( T, "%02$s" ) )
358         {
359             if( !( arg.value %02$s value ) )
360                 arg.throwWith(
361                     "argument (%%01$s) does not match expression (%%01$s %02$s %%02$s)"
362                     .format( arg.value, value )
363                 );
364 
365             return arg;
366         }
367     `.format( name, op ) );
368 }