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 }