1 /++ 2 Input validation 3 +/ 4 module oceandrift.validation.validate; 5 6 import oceandrift.validation.constraints; 7 import std.conv : to; 8 9 /++ 10 Validates data according to its type’s constraints 11 +/ 12 ValidationResult!T validate(bool bailOut = false, T)(T input) 13 if (is(T == struct) || is(T == class)) 14 { 15 auto output = ValidationResult!T(true); 16 17 // foreach field in `T` 18 static foreach (idx, field; T.tupleof) 19 { 20 { 21 enum string fieldName = __traits(identifier, field); 22 23 // foreach @UDA of `field` 24 alias attributes = __traits(getAttributes, field); 25 static foreach (attribute; attributes) 26 { 27 { 28 static if (is(attribute)) 29 { 30 alias attributeType = attribute; 31 enum attributeValue = attributeType.init; 32 } 33 else 34 { 35 enum attributeValue = attribute; 36 alias attributeType = typeof(attributeValue); 37 } 38 39 static assert( 40 !is(attributeType == void), 41 "Potentially broken attribute `@" 42 ~ attributeValue.stringof 43 ~ "` on field `" 44 ~ fieldName 45 ~ '`' 46 ); 47 48 enum attributeName = __traits(identifier, attributeType); 49 50 // is UDA a @constraint? 51 static if (isConstraint!attributeType) 52 { 53 // validate constraint implementation 54 static assert( 55 __traits(compiles, () { 56 string s = attributeValue.errorMessage; 57 }), 58 "Invalid Constraint: `@" 59 ~ attributeName ~ "` does not implement `string errorMessage` (at least not properly)" 60 ); 61 static assert( 62 __traits(compiles, () { 63 mixin(`bool valid = attributeValue.check(input.` ~ fieldName ~ `);`); 64 }), 65 "Invalid Constraint: `@" 66 ~ attributeName 67 ~ "` does not implement `bool check(" 68 ~ typeof( 69 field 70 ).stringof ~ ") {…}`" 71 ); 72 73 // apply constraint 74 mixin( 75 `immutable bool valid = attributeValue.check(input.` ~ fieldName ~ `);` 76 ); 77 78 // check failed? 79 if (!valid) 80 { 81 enum em = attributeValue.errorMessage; 82 enum ve = ValidationError(fieldName, em); 83 output._errors[idx] = ve; 84 output._ok = false; 85 86 static if (bailOut) 87 return output; 88 } 89 } 90 } 91 } 92 } 93 } 94 95 output._data = input; 96 return output; 97 } 98 99 /// 100 @safe unittest 101 { 102 struct Person 103 { 104 @notEmpty 105 string name; 106 107 @notNegative 108 int age; 109 } 110 111 auto personA = Person("Somebody", 32); 112 ValidationResult!Person rA = validate(personA); 113 assert(rA.ok); 114 assert(rA.data.name == "Somebody"); // accessing validated data 115 116 auto personB = Person("", 32); 117 ValidationResult!Person rB = validate(personB); 118 assert(!rB.ok); 119 120 auto personC = Person("Tom", -1); 121 ValidationResult!Person rC = validate(personC); 122 assert(!rC.ok); 123 } 124 125 /// Validation Error 126 struct ValidationError 127 { 128 @safe pure nothrow: 129 130 /// 131 string field; 132 133 /// 134 string message; 135 136 /// 137 string toString() inout 138 { 139 return field ~ ": " ~ message; 140 } 141 } 142 143 /// Validation Result 144 struct ValidationResult(Data) if (is(Data == struct) || is(Data == class)) 145 { 146 @safe pure nothrow @nogc: 147 148 private 149 { 150 bool _ok = false; 151 Data _data; 152 } 153 154 // public on purpose, though undocumented; “to be used with care” 155 public 156 { 157 ValidationError[Data.tupleof.length] _errors; 158 } 159 160 /// 161 bool ok() inout 162 { 163 return this._ok; 164 } 165 166 /++ 167 Validated data 168 169 (only valid if `.ok` == `true`) 170 +/ 171 Data data() inout 172 in (this.ok, "Trying to access data that did not pass validation.") 173 { 174 return _data; 175 } 176 177 /++ 178 Returns: 179 InputRange containing all [ValidationError]s 180 +/ 181 auto errors() 182 { 183 import std.algorithm : filter; 184 185 return _errors[].filter!(e => e.message !is null); 186 } 187 }