1 module dutils.validation.validate; 2 3 struct ValidationError { 4 string path; 5 string type; 6 string message; 7 double[string] parameters; 8 9 string toString() { 10 return this.message ~ " (at path " ~ this.path ~ ")."; 11 } 12 } 13 14 class ValidationErrors : Throwable { 15 private ValidationError[] _errors; 16 17 this(ValidationError[] errors) { 18 super("Invalid structure: " ~ this.concatenateErrors(errors)); 19 this._errors = errors; 20 } 21 22 static private string concatenateErrors(ValidationError[] errors) { 23 auto result = ""; 24 foreach (error; errors) { 25 if (result != "") { 26 result ~= " "; 27 } 28 result ~= error.toString(); 29 } 30 return result; 31 } 32 33 @property ValidationError[] errors() { 34 return this._errors; 35 } 36 } 37 38 void validate(T)(ref T object) { 39 validate(object, ""); 40 } 41 42 void validate(T)(ref T object, string pathPrefix) { 43 import std.traits : isSomeFunction; // , isBuiltinType, isArray; 44 45 ValidationError[] errors; 46 47 static foreach (member; __traits(derivedMembers, T)) { 48 static if (!isSomeFunction!(__traits(getMember, T, member))) { 49 if (__traits(getMember, object, member)) { 50 auto value = __traits(getMember, object, member); 51 52 foreach (attribute; __traits(getAttributes, __traits(getMember, T, member))) { 53 static if (isSomeFunction!(attribute.getError)) { 54 auto path = pathPrefix.length > 0 ? pathPrefix ~ "." ~ member : member; 55 auto result = attribute.getError(value, path); 56 if (!result.isNull) { 57 errors ~= result; 58 } 59 } 60 } 61 62 /* 63 import std.stdio; 64 65 // TODO: add recursive call here and add them to the errors array 66 writeln("built in: ", typeof(value).stringof); 67 if (isArray!(typeof(value))) { 68 foreach (child; value) { 69 auto path = pathPrefix.length > 0 ? pathPrefix ~ "." ~ member : member; 70 validate(child, path); 71 } 72 } else if (!isBuiltinType!(typeof(value))) { 73 writeln("!built in type"); 74 //auto childMembers = __traits(derivedMembers, typeof(value)); 75 //writeln("childMembers: ", childMembers); 76 //validate(__traits(getMember, object, member)); 77 } 78 */ 79 } else { 80 import dutils.validation.constraints : ValidateRequired; 81 82 foreach (attribute; __traits(getAttributes, __traits(getMember, T, member))) { 83 static if (is(typeof(attribute) == ValidateRequired)) { 84 auto path = pathPrefix.length > 0 ? pathPrefix ~ "." ~ member : member; 85 auto result = attribute.getError(__traits(getMember, object, member), path); 86 if (!result.isNull) { 87 errors ~= result; 88 } 89 } 90 } 91 } 92 } 93 94 } 95 96 if (errors.length > 0 && pathPrefix == "") { 97 throw new ValidationErrors(errors); 98 } 99 } 100 101 /** 102 * validate - Should throw array of ValidationError 103 */ 104 unittest { 105 import dutils.validation.constraints : ValidateRequired, 106 ValidateMinimumLength, ValidateMaximumLength, ValidateMinimum, ValidateEmail; 107 108 struct Person { 109 @ValidateRequired() 110 @ValidateMinimumLength(2) 111 @ValidateMaximumLength(100) 112 string name; 113 114 @ValidateMinimum!float(20) float height; 115 116 @ValidateEmail() 117 string email; 118 119 // TODO: add when recustion is working 120 // Person[] children; 121 } 122 123 // TODO: add when recustion is working 124 // auto person = Person("a", -1, "notanemail", [Person()]); 125 auto person = Person("a", -1, "notanemail"); 126 127 auto catched = false; 128 try { 129 validate(person); 130 } catch (ValidationErrors validation) { 131 import std.conv : to; 132 133 catched = true; 134 assert(validation.errors.length == 3, 135 "expected 3 errors, got " ~ validation.errors.length.to!string 136 ~ " with message: " ~ validation.msg); 137 assert(validation.errors[0].type == "minimumLength", "expected minimumLength error"); 138 assert(validation.errors[1].type == "minimum", "expected minimum error"); 139 assert(validation.errors[2].type == "email", "expected email error"); 140 } 141 142 assert(catched == true, "did not catch the expected errors"); 143 } 144 145 /** 146 * validate - Should not throw validation errors 147 */ 148 unittest { 149 import dutils.validation.constraints : ValidateMinimumLength, 150 ValidateMaximumLength, ValidateMinimum, ValidateEmail; 151 152 struct Person { 153 @ValidateMinimumLength(2) 154 @ValidateMaximumLength(100) 155 string name; 156 157 @ValidateMinimum!float(20) float height; 158 159 @ValidateEmail() 160 string email; 161 } 162 163 Person person; 164 person.name = "Anna"; 165 person.height = 167; 166 167 validate(person); 168 } 169 170 /** 171 * validate - Should not throw validation errors for nested structs 172 */ 173 unittest { 174 import dutils.validation.constraints : ValidateRequired, 175 ValidateMinimumLength, ValidateMaximumLength, ValidateMinimum, ValidateEmail; 176 177 struct Person { 178 @ValidateRequired() 179 @ValidateMinimumLength(2) 180 @ValidateMaximumLength(100) 181 string name; 182 183 @ValidateMinimum!float(20) float height; 184 185 @ValidateEmail() 186 string email; 187 188 // TODO: add when recustion is working 189 // Person[] children; 190 } 191 192 // TODO: add when recustion is working 193 // Person child; 194 // child.name = "Sofia"; 195 196 Person person; 197 person.name = "Anna"; 198 person.height = 167; 199 // TODO: add when recustion is working 200 // person.children ~= child; 201 202 validate(person); 203 }