Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.07% covered (success)
91.07%
51 / 56
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
StructuredMentorListValidator
91.07% covered (success)
91.07%
51 / 56
60.00% covered (warning)
60.00%
3 / 5
16.18
0.00% covered (danger)
0.00%
0 / 1
 validate
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
4.13
 validateMentor
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
7
 validateMentorMessage
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 validateVariable
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getDefaultContent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace GrowthExperiments\Config\Validation;
4
5use GrowthExperiments\Mentorship\Provider\MentorProvider;
6use InvalidArgumentException;
7use StatusValue;
8
9class StructuredMentorListValidator implements IConfigValidator {
10    use DatatypeValidationTrait;
11
12    /** @var string */
13    private const TOP_LEVEL_KEY = 'Mentors';
14
15    /**
16     * @var string[]
17     *
18     * A mapping of keys to data types, used for validating mentor JSON object.
19     *
20     * All keys mentioned here (except keys listed in OPTIONAL_MENTOR_KEYS) will be required.
21     */
22    private const MENTOR_KEY_DATATYPES = [
23        'username' => 'string',
24        'message' => '?string',
25        'weight' => 'int',
26        'automaticallyAssigned' => 'bool',
27    ];
28
29    /** @var string[] List of optional keys in mentor serialization. */
30    private const OPTIONAL_MENTOR_KEYS = [
31        'username',
32        'automaticallyAssigned',
33    ];
34
35    /**
36     * @inheritDoc
37     */
38    public function validate( array $config ): StatusValue {
39        if ( !array_key_exists( self::TOP_LEVEL_KEY, $config ) ) {
40            return StatusValue::newFatal(
41                'growthexperiments-mentor-list-missing-key',
42                self::TOP_LEVEL_KEY
43            );
44        }
45
46        $mentors = $config[self::TOP_LEVEL_KEY];
47        if ( !$this->validateFieldDatatype( 'array<int,array>', $mentors ) ) {
48            return StatusValue::newFatal(
49                'growthexperiments-mentor-list-datatype-mismatch',
50                self::TOP_LEVEL_KEY,
51                'array<int,array>',
52                gettype( $mentors )
53            );
54        }
55
56        $status = StatusValue::newGood();
57        foreach ( $mentors as $userId => $mentor ) {
58            $status->merge( $this->validateMentor(
59                $mentor,
60                $userId
61            ) );
62        }
63
64        return $status;
65    }
66
67    /**
68     * Validate a mentor JSON representation
69     *
70     * @param array $mentor
71     * @param int $userId User ID of the user represented by $mentor
72     * @return StatusValue
73     */
74    private function validateMentor( array $mentor, int $userId ): StatusValue {
75        $supportedKeys = array_keys( self::MENTOR_KEY_DATATYPES );
76
77        // Ensure all supported keys are present in the mentor object
78        foreach ( $supportedKeys as $key ) {
79            if ( !array_key_exists( $key, $mentor ) && !in_array( $key, self::OPTIONAL_MENTOR_KEYS ) ) {
80                return StatusValue::newFatal(
81                    'growthexperiments-mentor-list-missing-key',
82                    $key
83                );
84            }
85        }
86
87        // Ensure all keys present in the mentor object are supported and of correct data type
88        foreach ( $mentor as $key => $value ) {
89            if ( !array_key_exists( $key, self::MENTOR_KEY_DATATYPES ) ) {
90                return StatusValue::newFatal(
91                    'growthexperiments-mentor-list-unexpected-key-mentor',
92                    $key
93                );
94            }
95
96            if ( !$this->validateFieldDatatype( self::MENTOR_KEY_DATATYPES[$key], $value ) ) {
97                return StatusValue::newFatal(
98                    'growthexperiments-mentor-list-datatype-mismatch',
99                    $key,
100                    self::MENTOR_KEY_DATATYPES[$key],
101                    gettype( $value )
102                );
103            }
104        }
105
106        // Code below assumes mentor declarations are syntactically correct.
107        $status = StatusValue::newGood();
108        $status->merge( self::validateMentorMessage( $mentor, $userId ) );
109        return $status;
110    }
111
112    /**
113     * Validate the mentor message
114     *
115     * Currently only checks MentorProvider::INTRO_TEXT_LENGTH.
116     *
117     * @param array $mentor
118     * @param int $userId User ID of the user represented by $mentor
119     * @return StatusValue Warning means "an issue, but not important enough to stop using the
120     * mentor list".
121     */
122    public static function validateMentorMessage(
123        array $mentor,
124        int $userId
125    ): StatusValue {
126        $status = StatusValue::newGood();
127
128        // Ensure message has correct length. This only warns, as we do not want to fail the
129        // validation (truncated message is better than broken mentorship).
130        if ( mb_strlen( $mentor['message'] ?? '', 'UTF-8' ) > MentorProvider::INTRO_TEXT_LENGTH ) {
131            $status->warning(
132                'growthexperiments-mentor-writer-error-message-too-long',
133                MentorProvider::INTRO_TEXT_LENGTH,
134                $userId
135            );
136        }
137
138        return $status;
139    }
140
141    /**
142     * @inheritDoc
143     */
144    public function validateVariable( string $variable, $value ): void {
145        if ( $variable !== self::TOP_LEVEL_KEY ) {
146            throw new InvalidArgumentException(
147                "Invalid variable $variable configured in the mentor list"
148            );
149        }
150    }
151
152    /**
153     * @inheritDoc
154     */
155    public function getDefaultContent(): array {
156        return [ 'Mentors' => [] ];
157    }
158}