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 }