1: <?php
2:
3: 4: 5: 6: 7: 8:
9:
10: namespace Chromabits\Nucleus\Meditation;
11:
12: use Chromabits\Nucleus\Control\Maybe;
13: use Chromabits\Nucleus\Data\ArrayList;
14: use Chromabits\Nucleus\Data\ArrayMap;
15: use Chromabits\Nucleus\Data\Interfaces\LeftKeyFoldableInterface;
16: use Chromabits\Nucleus\Data\Iterable;
17: use Chromabits\Nucleus\Exceptions\CoreException;
18: use Chromabits\Nucleus\Foundation\BaseObject;
19: use Chromabits\Nucleus\Meditation\Constraints\AbstractConstraint;
20: use Chromabits\Nucleus\Meditation\Interfaces\CheckableInterface;
21: use Chromabits\Nucleus\Support\Std;
22:
23: 24: 25: 26: 27: 28:
29: class Spec extends BaseObject implements CheckableInterface
30: {
31: const ANNOTATION_CONSTRAINTS = 'constraints';
32:
33: const ANNOTATION_DEFAULT = 'default';
34:
35: const ANNOTATION_REQUIRED = 'required';
36:
37: 38: 39:
40: protected $annotations;
41:
42: 43: 44: 45: 46: 47: 48:
49: public function __construct(
50: array $constraints = [],
51: array $defaults = [],
52: array $required = []
53: ) {
54: parent::__construct();
55:
56: $annotations = ArrayMap::zero();
57:
58: $annotations = ArrayMap::of($constraints)
59: ->foldlWithKeys(
60: function (ArrayMap $acc, $key, $value) {
61: return $acc->update(
62: $key,
63: function (ArrayMap $field) use ($value) {
64: return $field->insert(
65: static::ANNOTATION_CONSTRAINTS,
66: $value
67: );
68: },
69: ArrayMap::zero()
70: );
71: },
72: $annotations
73: );
74:
75: $annotations = ArrayMap::of($defaults)
76: ->foldlWithKeys(
77: function (ArrayMap $acc, $key, $value) {
78: return $acc->update(
79: $key,
80: function (ArrayMap $field) use ($value) {
81: return $field->insert(
82: static::ANNOTATION_DEFAULT,
83: $value
84: );
85: },
86: ArrayMap::zero()
87: );
88: },
89: $annotations
90: );
91:
92: $annotations = ArrayList::of($required)
93: ->foldl(
94: function (ArrayMap $acc, $value) {
95: return $acc->update(
96: $value,
97: function (ArrayMap $field) {
98: return $field->insert(
99: static::ANNOTATION_REQUIRED,
100: true
101: );
102: },
103: ArrayMap::zero()
104: );
105: },
106: $annotations
107: );
108:
109: $this->annotations = $annotations;
110: }
111:
112: 113: 114: 115: 116: 117: 118: 119: 120:
121: public static function define(
122: array $constraints = [],
123: array $defaults = [],
124: array $required = []
125: ) {
126: return new static($constraints, $defaults, $required);
127: }
128:
129: 130: 131: 132: 133: 134: 135:
136: public function check(array $input)
137: {
138: $missing = [];
139: $invalid = [];
140:
141: $check = function ($constraint, $key, $value, $input) use (
142: &$missing,
143: &$invalid
144: ) {
145: if ($constraint instanceof AbstractConstraint) {
146: if (!$constraint->check($value, $input)) {
147: $invalid[$key][] = $constraint;
148: }
149: } elseif ($constraint instanceof CheckableInterface) {
150: $result = $constraint->check($value);
151:
152: $missing = Std::concat(
153: $missing,
154: array_map(
155: function ($subKey) use ($key) {
156: return vsprintf('%s.%s', [$key, $subKey]);
157: },
158: $result->getMissing()
159: )
160: );
161:
162: foreach ($result->getFailed() as $failedField => $constraints) {
163: $fullPath = vsprintf('%s.%s', [$key, $failedField]);
164:
165: if (array_key_exists($fullPath, $invalid)) {
166: $invalid[$fullPath] = array_merge(
167: $invalid[$fullPath],
168: $constraints
169: );
170: } else {
171: $invalid[$fullPath] = $constraints;
172: }
173: }
174: } else {
175: throw new CoreException(
176: vsprintf(
177: 'Unexpected constraint type: %s.',
178: [
179: TypeHound::fetch($constraint),
180: ]
181: )
182: );
183: }
184: };
185:
186: $inputMap = ArrayMap::of($input);
187:
188: $this->annotations->each(
189: function ($value, $key) use (
190: $check,
191: $input,
192: $inputMap,
193: &$missing
194: ) {
195:
196: if (Maybe::fromMaybe(
197: false,
198: $value->lookup(static::ANNOTATION_REQUIRED)
199: )
200: && $inputMap->member($key) === false
201: ) {
202: $missing[] = $key;
203:
204:
205:
206: return;
207: } elseif ($inputMap->member($key) === false) {
208:
209:
210: return;
211: }
212:
213: $fieldValue = Maybe::fromJust($inputMap->lookup($key));
214:
215: $this
216: ->getInternalFieldConstraints($key)
217: ->each(
218: function ($constraint) use (
219: $check,
220: $key,
221: $fieldValue,
222: $input
223: ) {
224: $check($constraint, $key, $fieldValue, $input);
225: }
226: );
227: }
228: );
229:
230: if (count($missing) === 0 && count($invalid) === 0) {
231: return new SpecResult($missing, $invalid, SpecResult::STATUS_PASS);
232: }
233:
234: return new SpecResult($missing, $invalid, SpecResult::STATUS_FAIL);
235: }
236:
237: 238: 239: 240: 241: 242: 243: 244:
245: protected function getInternalFieldConstraints($fieldName)
246: {
247: return $this->getFieldConstraints($fieldName);
248: }
249:
250: 251: 252: 253: 254: 255: 256:
257: public function getFieldConstraints($fieldName)
258: {
259: $maybeConstraints = $this->annotations->lookupIn(
260: [$fieldName, static::ANNOTATION_CONSTRAINTS]
261: );
262:
263: if ($maybeConstraints->isNothing()) {
264: return ArrayList::zero();
265: }
266:
267: $constraints = Maybe::fromJust($maybeConstraints);
268:
269: if (is_array($constraints)) {
270: return ArrayList::of($constraints);
271: } elseif ($constraints instanceof Iterable) {
272: return $constraints;
273: }
274:
275: return ArrayList::of([$constraints]);
276: }
277:
278: 279: 280:
281: public function getConstraints()
282: {
283: return $this->getAnnotation(static::ANNOTATION_CONSTRAINTS)->toArray();
284: }
285:
286: 287: 288: 289: 290:
291: public function getAnnotation($name)
292: {
293: return $this->annotations
294: ->map(
295: function (ArrayMap $value) use ($name) {
296: return $value->lookup($name);
297: }
298: )
299: ->filter(
300: function (Maybe $value) {
301: return $value->isJust();
302: }
303: )
304: ->map(
305: function (Maybe $value) {
306: return Maybe::fromJust($value);
307: }
308: );
309: }
310:
311: 312: 313:
314: public function getDefaults()
315: {
316: return $this->getAnnotation(static::ANNOTATION_DEFAULT)->toArray();
317: }
318:
319: 320: 321: 322: 323:
324: public function getFieldDefault($fieldName)
325: {
326: return $this->getFieldAnnotation(
327: $fieldName,
328: static::ANNOTATION_DEFAULT
329: );
330: }
331:
332: 333: 334: 335: 336: 337: 338: 339:
340: public function getFieldAnnotation($fieldName, $name)
341: {
342: return $this->annotations->lookupIn([$fieldName, $name]);
343: }
344:
345: 346: 347: 348: 349:
350: public function getFieldRequired($fieldName)
351: {
352: return Maybe::fromMaybe(
353: false,
354: $this->getFieldAnnotation(
355: $fieldName,
356: static::ANNOTATION_REQUIRED
357: )
358: );
359: }
360:
361: 362: 363:
364: public function getRequired()
365: {
366: return $this
367: ->getAnnotation(static::ANNOTATION_DEFAULT)
368: ->filter(
369: function ($value) {
370: return $value;
371: }
372: )
373: ->keys()
374: ->toArray();
375: }
376:
377: 378: 379: 380: 381:
382: public function getAnnotations()
383: {
384: return $this->annotations;
385: }
386:
387: 388: 389: 390: 391: 392: 393:
394: public function getFieldAnnotations($fieldName)
395: {
396: return $this->annotations->lookup($fieldName);
397: }
398:
399: 400: 401: 402: 403: 404: 405: 406:
407: public function withFieldConstraints($fieldName, $constraints)
408: {
409: return $this->withFieldAnnotation(
410: $fieldName,
411: static::ANNOTATION_CONSTRAINTS,
412: $constraints
413: );
414: }
415:
416: 417: 418: 419: 420: 421: 422: 423: 424:
425: public function withFieldAnnotation($fieldName, $name, $value)
426: {
427: $copy = clone $this;
428:
429: $copy->annotations = $this->annotations->update(
430: $fieldName,
431: function (ArrayMap $fieldAnnotations) use ($name, $value) {
432: return $fieldAnnotations->insert($name, $value);
433: },
434: ArrayMap::zero()
435: );
436:
437: return $copy;
438: }
439:
440: 441: 442: 443: 444: 445: 446: 447:
448: public function withFieldDefault($fieldName, $default)
449: {
450: return $this->withFieldAnnotation(
451: $fieldName,
452: static::ANNOTATION_DEFAULT,
453: $default
454: );
455: }
456:
457: 458: 459: 460: 461: 462: 463: 464:
465: public function withFieldRequired($fieldName, $value = true)
466: {
467: return $this->withFieldAnnotation(
468: $fieldName,
469: static::ANNOTATION_REQUIRED,
470: $value
471: );
472: }
473:
474: 475: 476: 477: 478: 479: 480: 481:
482: public function withAnnotation($name, LeftKeyFoldableInterface $map)
483: {
484: return $map->foldlWithKeys(
485: function (self $acc, $value, $fieldName) use ($name) {
486: return $acc->withFieldAnnotation($fieldName, $name, $value);
487: },
488: $this
489: );
490: }
491:
492: 493: 494: 495: 496: 497: 498: 499:
500: public function withDefaults(LeftKeyFoldableInterface $map)
501: {
502: return $this->withAnnotation(static::ANNOTATION_DEFAULT, $map);
503: }
504: }
505: