1 /** 2 Provides functionality for validating function arguments. 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.typecons : Nullable, NullableRef; 12 import std.range : isInputRange, hasLength; 13 import std.format : format; 14 15 private enum isValidRangeSpecifier( string spec ) = 16 spec !is null && 17 spec.length == 2 && 18 ( spec[0] == '(' || spec[0] == '[' ) && 19 ( spec[1] == ')' || spec[1] == ']' ); 20 21 private enum supportsOperation( T, string op ) = 22 is( typeof( { bool _ = mixin( "T.init " ~ op ~ " T.init" ); } ) ); 23 24 version( unittest ) 25 { 26 private 27 { 28 void mustThrow( E: Throwable, F )( F f ) 29 { 30 try 31 { 32 f(); 33 assert( false, "expression was supposed to throw but it didn't" ); 34 } catch( E ) { } 35 } 36 37 void mustNotThrow( E: Throwable, F )( F f ) 38 { 39 try 40 { 41 f(); 42 } 43 catch( E ) 44 { 45 assert( false, "expression wasn't supposed to throw but it did (threw " ~ E.stringof ~ ")" ); 46 } 47 } 48 } 49 } 50 51 /++ 52 A convenience wrapper for __traits( identifier, ... ). 53 54 Authors: Tony J. Hudgins 55 Copyright: Copyright © 2019, Tony J. Hudgins 56 License: MIT 57 +/ 58 enum nameof( alias symbol ) = __traits( identifier, symbol ); 59 60 @system unittest 61 { 62 struct Foo 63 { 64 int x; 65 } 66 67 int a; 68 Foo b; 69 70 // test locals 71 assert( nameof!a == "a" ); 72 73 // test members 74 assert( nameof!( b.x ) == "x" ); 75 76 // test types 77 assert( nameof!Foo == "Foo" ); 78 79 // test modules 80 assert( nameof!ensure == "ensure" ); 81 } 82 83 /++ 84 Tests if a type can be null. Not related to std.typecons.Nullable!T. 85 86 Authors: Tony J. Hudgins 87 Copyright: Copyright © 2019, Tony J. Hudgins 88 License: MIT 89 +/ 90 enum isNullable( T ) = is( typeof( { Unqual!T _ = null; } ) ); 91 92 @system unittest 93 { 94 assert( isNullable!string ); 95 assert( !isNullable!int ); 96 } 97 98 /++ 99 Thrown for any validation errors. 100 101 Authors: Tony J. Hudgins 102 Copyright: Copyright © 2019, Tony J. Hudgins 103 License: MIT 104 +/ 105 final class EnsureException : Exception 106 { 107 private string _paramName; 108 109 string paramName() const pure nothrow @trusted @property 110 { 111 return this._paramName; 112 } 113 114 /++ 115 Constructs a new EnsureException for a given parameter with a custom message. 116 117 Params: 118 paramName = The name of the parameter being validated. 119 msg = The message to pass to the exception's constructor. 120 file = Used for passing the appropriate file name to the exception's constructor. 121 line = Used for passing the appropriate line number to the exception's constructor. 122 next = The next throwable object in the chain. 123 124 Authors: Tony J. Hudgins 125 Copyright: Copyright © 2019, Tony J. Hudgins 126 License: MIT 127 +/ 128 this( string paramName, string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null ) 129 { 130 this._paramName = paramName; 131 auto newMsg = paramName is null || paramName.length == 0 ? msg : "%s: %s".format( paramName, msg ); 132 super( newMsg, file, line, next ); 133 } 134 } 135 136 /++ 137 Represents a single function parameter, including name and value, upon which all validation is performed. 138 139 Authors: Tony J. Hudgins 140 Copyright: Copyright © 2019, Tony J. Hudgins 141 License: MIT 142 +/ 143 struct Arg( T ) 144 { 145 private { 146 string _paramName; 147 T _value; 148 } 149 150 inout( string ) paramName() inout pure nothrow @trusted @property 151 { 152 return this._paramName; 153 } 154 155 inout( T ) value() inout pure nothrow @trusted @property 156 { 157 return this._value; 158 } 159 160 /// Default construction is not allowed. 161 this() @disable; 162 163 private this( string paramName, T value ) 164 { 165 this._paramName = paramName; 166 this._value = value; 167 } 168 169 /++ 170 Throws an EnsureException for the current parameter with the given message. 171 172 Params: 173 msg = The message to pass to the exception's constructor. 174 file = Used for passing the appropriate file name to the exception's constructor. 175 line = Used for passing the appropriate line number to the exception's constructor. 176 177 Throws: EnsureException whenever it's called. 178 Authors: Tony J. Hudgins 179 Copyright: Copyright © 2019, Tony J. Hudgins 180 License: MIT 181 +/ 182 void throwWith( string msg, string file = __FILE__, size_t line = __LINE__ ) const @trusted 183 { 184 throw new EnsureException( this._paramName, msg, file, line ); 185 } 186 } 187 188 /++ 189 Wrap a function argument to perform validation. 190 191 Authors: Tony J. Hudgins 192 Copyright: Copyright © 2019, Tony J. Hudgins 193 License: MIT 194 +/ 195 Arg!( typeof( what ) ) ensure( alias what )() 196 { 197 return Arg!( typeof( what ) )( nameof!what, what ); 198 } 199 200 /++ 201 Ensures that the argument is not null. 202 203 Params: 204 arg = A wrapped argument, obtained with ensure. 205 206 See_Also: ensure 207 Throws: EnsureException when validation fails. 208 Authors: Tony J. Hudgins 209 Copyright: Copyright © 2019, Tony J. Hudgins 210 License: MIT 211 +/ 212 Arg!T isNotNull( T )( Arg!T arg ) if( isNullable!T ) 213 { 214 if( arg.value is null ) 215 arg.throwWith( "argument cannot be null" ); 216 217 return arg; 218 } 219 220 @system unittest 221 { 222 string a = null; 223 string b = ""; 224 225 mustThrow!EnsureException( { ensure!a.isNotNull; } ); 226 227 ensure!b.isNotNull; 228 } 229 230 /++ 231 Ensures that the std.typecons.Nullable is not in a null state. 232 233 Params: 234 arg = A wrapped argument, obtained with ensure. 235 236 See_Also: ensure 237 Throws: EnsureException when validation fails. 238 Authors: Tony J. Hudgins 239 Copyright: Copyright © 2019, Tony J. Hudgins 240 License: MIT 241 +/ 242 Arg!T isNotNull( T: Nullable!U, U )( Arg!T arg ) 243 { 244 if( arg.value.isNull ) 245 arg.throwWith( "nullable cannot be null" ); 246 247 return arg; 248 } 249 250 @system unittest 251 { 252 import std.typecons : Nullable, nullable; 253 254 Nullable!int a; 255 auto b = nullable( 5 ); 256 257 mustThrow!EnsureException( { ensure!a.isNotNull; } ); 258 ensure!b.isNotNull; 259 } 260 261 /++ 262 Ensures that the std.typecons.NullableRef is not in a null state. 263 264 Params: 265 arg = A wrapped argument, obtained with ensure. 266 267 See_Also: ensure 268 Throws: EnsureException when validation fails. 269 Authors: Tony J. Hudgins 270 Copyright: Copyright © 2019, Tony J. Hudgins 271 License: MIT 272 +/ 273 Arg!T isNotNull( T: NullableRef!U, U )( Arg!T arg ) 274 { 275 if( arg.value.isNull ) 276 arg.throwWith( "nullable cannot be null" ); 277 278 return arg; 279 } 280 281 @system unittest 282 { 283 import std.typecons : NullableRef, nullableRef; 284 285 int naked = 5; 286 287 NullableRef!int a; 288 auto b = nullableRef( &naked ); 289 290 mustThrow!EnsureException( { ensure!a.isNotNull; } ); 291 292 ensure!b.isNotNull; 293 } 294 295 /++ 296 Ensures that an array, associative array, string, or input range is not empty. 297 298 Params: 299 arg = A wrapped argument, obtained with ensure. 300 301 See_Also: ensure 302 Throws: EnsureException when validation fails. 303 Authors: Tony J. Hudgins 304 Copyright: Copyright © 2019, Tony J. Hudgins 305 License: MIT 306 +/ 307 Arg!T isNotEmpty( T )( Arg!T arg ) if( isInputRange!T || isSomeString!T || isArray!T || isAssociativeArray!T ) 308 { 309 static if( isInputRange!T ) 310 enum kind = "range"; 311 else static if( isSomeString!T ) 312 enum kind = "string"; 313 else static if( isArray!T ) 314 enum kind = "array"; 315 else static if( isAssociativeArray!T ) 316 enum kind = "associative array"; 317 else 318 static assert( false, "unsupported type for isNotEmpty: " ~ T.stringof ); 319 320 static if( isInputRange!T && !isSomeString!T && !isArray!T ) 321 { 322 if( arg.value.empty ) 323 arg.throwWith( kind ~ " cannot be empty" ); 324 } 325 else 326 { 327 if( arg.value.length == 0 ) 328 arg.throwWith( kind ~ " cannot be empty" ); 329 } 330 331 return arg; 332 } 333 334 @system unittest 335 { 336 import std.range : iota; 337 import std.algorithm : map; 338 339 string str = "1"; 340 int[] arr = [ 1 ]; 341 int[string] assoc = [ "one": 1 ]; 342 auto some = iota( 0, 5, 1 ); 343 auto none = iota( 0, 0, 0 ); 344 345 ensure!str.isNotEmpty; 346 ensure!arr.isNotEmpty; 347 ensure!assoc.isNotEmpty; 348 349 str = ""; 350 arr = []; 351 assoc = typeof( assoc ).init; 352 353 mustThrow!EnsureException( { ensure!str.isNotEmpty; } ); 354 mustThrow!EnsureException( { ensure!arr.isNotEmpty; } ); 355 mustThrow!EnsureException( { ensure!assoc.isNotEmpty; } ); 356 357 ensure!(some).isNotEmpty; 358 359 mustThrow!EnsureException( { ensure!(none).isNotEmpty; } ); 360 } 361 362 /++ 363 Ensures that a string does not contain only whitespace characters. 364 365 Params: 366 arg = A wrapped argument, obtained with ensure. 367 368 See_Also: ensure 369 Throws: EnsureException when validation fails. 370 Authors: Tony J. Hudgins 371 Copyright: Copyright © 2019, Tony J. Hudgins 372 License: MIT 373 +/ 374 Arg!S isNotWhitespace( S )( Arg!S arg ) if( isSomeString!S ) 375 { 376 import std.algorithm : all; 377 import std.uni : isWhite; 378 379 if( arg.isNotNull.value.all!isWhite ) 380 arg.throwWith( "string cannot consist of only whitespace characters" ); 381 382 return arg; 383 } 384 385 @system unittest 386 { 387 auto a = "123"; 388 auto b = " "; 389 390 ensure!a.isNotWhitespace; 391 392 mustThrow!EnsureException( { ensure!b.isNotWhitespace; } ); 393 } 394 395 /++ 396 Ensures that a number is between an upper bound and a lower bound. 397 398 By default, this function's bounds are **inclusive**, inclusivity/exclusivity can be changed 399 on a per-bound basis by passing in a string template argument similar to std.random.uniform. 400 401 The string template parameter expects a 2-character long string consisting of an 402 opening parenthesis or opening square bracket, followed by a closing parenthesis or closing square bracket. 403 The opening bracket is for the lower bound, while the closing bracket is for the upper bound. 404 Parenteses indicate that the boundary is inclusive, and square brackets indicate the boundary is exclusive. 405 406 All possible combinations: 407 408 - **()** - Both lower bound and upper bound are inclusive. 409 - **(]** - Lower bound is inclusive and upper bound is exclusive. 410 - **[)** - Lower bound is exclusive and upper bound is inclusive. 411 - **[]** - Both lower bound and upper bound are exclusive. 412 413 Params: 414 arg = A wrapped argument, obtained with ensure. 415 416 Examples: 417 --------- 418 const zero = 0; 419 420 ensure!(zero).between!"[)"( 0, 5 ); // exception, lower bound is exclusive 421 --------- 422 423 See_Also: ensure 424 Throws: EnsureException when validation fails. 425 Authors: Tony J. Hudgins 426 Copyright: Copyright © 2019, Tony J. Hudgins 427 License: MIT 428 +/ 429 Arg!N between( string how = "()", N )( Arg!N arg, N lowerBound, N upperBound ) 430 if( isNumeric!N && isValidRangeSpecifier!how ) 431 { 432 enum lowerOp = how[0] == '(' ? "<" : "<="; 433 enum upperOp = how[1] == ')' ? ">" : ">="; 434 435 static enum Bound { lower, upper } 436 437 string err( Bound bound, bool inclusive, N value ) 438 { 439 import std.array : appender; 440 441 auto msg = appender!string; 442 msg.put( "argument (%s) is ".format( arg.value ) ); 443 444 with( Bound ) 445 final switch( bound ) 446 { 447 case lower: 448 msg.put( "less than " ); 449 break; 450 451 case upper: 452 msg.put( "greater than " ); 453 break; 454 } 455 456 if( !inclusive ) 457 msg.put( "or equal to " ); 458 459 msg.put( "the " ); 460 461 with( Bound ) 462 final switch( bound ) 463 { 464 case lower: 465 msg.put( "lower " ); 466 break; 467 468 case upper: 469 msg.put( "upper " ); 470 break; 471 } 472 473 msg.put( "bound (%s)".format( value ) ); 474 475 return msg.data; 476 } 477 478 if( mixin( "arg.value" ~ lowerOp ~ "lowerBound" ) ) 479 arg.throwWith( err( Bound.lower, how[0] == '(', lowerBound ) ); 480 481 if( mixin( "arg.value" ~ upperOp ~ "upperBound" ) ) 482 arg.throwWith( err( Bound.upper, how[1] == ')', upperBound ) ); 483 484 return arg; 485 } 486 487 @system unittest 488 { 489 auto lower = 0; 490 auto upper = 5; 491 492 ensure!(lower).between!"()"( lower, upper ); 493 ensure!(upper).between!"()"( lower, upper ); 494 495 mustThrow!EnsureException( { ensure!(lower).between!"[]"( lower, upper ); } ); 496 mustThrow!EnsureException( { ensure!(upper).between!"[]"( lower, upper ); } ); 497 } 498 499 // auto-generate some functions for common boolean operations. 500 private enum ops = [ 501 ">": [ "greaterThan", "gt" ], 502 ">=": [ "greaterThanOrEqualTo", "gte" ], 503 504 "<": [ "lessThan", "lt" ], 505 "<=": [ "lessThanOrEqualTo", "lte" ], 506 507 "==": [ "equalTo", "eq" ], 508 "!=": [ "notEqualTo", "ne", "neq" ], 509 ]; 510 511 static foreach( op, names; ops ) 512 static foreach( name; names ) 513 { 514 mixin( ` 515 Arg!T %01$s( T )( Arg!T arg, T value ) if( supportsOperation!( T, "%02$s" ) ) 516 { 517 if( !( arg.value %02$s value ) ) 518 arg.throwWith( 519 "argument (%%01$s) does not match expression (%%01$s %02$s %%02$s)" 520 .format( arg.value, value ) 521 ); 522 523 return arg; 524 } 525 `.format( name, op ) ); 526 }