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 }