Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 64 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
MessageGroupConfigurationParser | |
0.00% |
0 / 64 |
|
0.00% |
0 / 9 |
1122 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getHopefullyValidConfigurations | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
56 | |||
getDocumentsFromYaml | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
parseDocuments | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 | |||
getBaseSchema | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
validate | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
mergeTemplate | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
30 | |||
getFilesSchemaExtra | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
callGetExtraSchema | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageGroupConfiguration; |
5 | |
6 | use AggregateMessageGroup; |
7 | use Exception; |
8 | use MediaWiki\Extension\Translate\FileFormatSupport\FileFormatFactory; |
9 | use MediaWiki\Extension\Translate\MessageProcessing\StringMatcher; |
10 | use MediaWiki\Extension\Translate\Services; |
11 | use MediaWiki\Extension\Translate\Utilities\Yaml; |
12 | use RomaricDrigon\MetaYaml\MetaYaml; |
13 | |
14 | /** |
15 | * Utility class to parse and validate message group configurations. |
16 | * @author Niklas Laxström |
17 | * @license GPL-2.0-or-later |
18 | */ |
19 | class MessageGroupConfigurationParser { |
20 | private ?array $baseSchema = null; |
21 | private FileFormatFactory $fileFormatFactory; |
22 | |
23 | public function __construct() { |
24 | // Don't perform validations if library not available |
25 | if ( class_exists( MetaYaml::class ) ) { |
26 | $this->baseSchema = $this->getBaseSchema(); |
27 | } |
28 | |
29 | $this->fileFormatFactory = Services::getInstance()->getFileFormatFactory(); |
30 | } |
31 | |
32 | /** |
33 | * Easy to use function to get valid group configurations from YAML. Those not matching |
34 | * schema will be ignored, if schema validation is enabled. |
35 | * |
36 | * @param string $data Yaml |
37 | * @param callable|null $callback Optional callback which is called on errors. Parameters are |
38 | * document index, processed configuration and error message. |
39 | * @return array Group configurations indexed by message group id. |
40 | */ |
41 | public function getHopefullyValidConfigurations( string $data, ?callable $callback = null ): array { |
42 | if ( !is_callable( $callback ) ) { |
43 | $callback = static function ( $unused1, $unused2, $unused3 ) { |
44 | /*noop*/ |
45 | }; |
46 | } |
47 | |
48 | $documents = self::getDocumentsFromYaml( $data ); |
49 | $configurations = self::parseDocuments( $documents ); |
50 | $groups = []; |
51 | |
52 | if ( is_array( $this->baseSchema ) ) { |
53 | foreach ( $configurations as $index => $config ) { |
54 | try { |
55 | $this->validate( $config ); |
56 | $groups[$config['BASIC']['id']] = $config; |
57 | } catch ( Exception $e ) { |
58 | $callback( $index, $config, $e->getMessage() ); |
59 | } |
60 | } |
61 | } else { |
62 | foreach ( $configurations as $index => $config ) { |
63 | if ( isset( $config['BASIC']['id'] ) ) { |
64 | $groups[$config['BASIC']['id']] = $config; |
65 | } else { |
66 | $callback( $index, $config, 'id is missing' ); |
67 | } |
68 | } |
69 | } |
70 | |
71 | return $groups; |
72 | } |
73 | |
74 | /** |
75 | * Given a Yaml string, returns the non-empty documents as an array. |
76 | * @return string[] |
77 | */ |
78 | public function getDocumentsFromYaml( string $data ): array { |
79 | return preg_split( "/^---$/m", $data, -1, PREG_SPLIT_NO_EMPTY ); |
80 | } |
81 | |
82 | /** |
83 | * Returns group configurations from YAML documents. If there is document containing template, |
84 | * it will be merged with other configurations. |
85 | * |
86 | * @return array[][] Unvalidated group configurations |
87 | */ |
88 | public function parseDocuments( array $documents ): array { |
89 | $groups = []; |
90 | $template = []; |
91 | |
92 | foreach ( $documents as $document ) { |
93 | $document = Yaml::loadString( $document ); |
94 | |
95 | if ( isset( $document['TEMPLATE'] ) ) { |
96 | $template = $document['TEMPLATE']; |
97 | } else { |
98 | $groups[] = $document; |
99 | } |
100 | } |
101 | |
102 | if ( $template ) { |
103 | foreach ( $groups as $i => $group ) { |
104 | $groups[$i] = self::mergeTemplate( $template, $group ); |
105 | // Little hack to allow aggregate groups to be defined in same file with other groups. |
106 | if ( $groups[$i]['BASIC']['class'] === AggregateMessageGroup::class ) { |
107 | unset( $groups[$i]['FILES'] ); |
108 | } |
109 | } |
110 | } |
111 | |
112 | return $groups; |
113 | } |
114 | |
115 | public function getBaseSchema(): array { |
116 | return Yaml::load( __DIR__ . '/../../data/group-yaml-schema.yaml' ); |
117 | } |
118 | |
119 | /** |
120 | * Validates group configuration against schema. |
121 | * @throws Exception If configuration is not valid. |
122 | */ |
123 | public function validate( array $config ): void { |
124 | $schema = $this->baseSchema; |
125 | |
126 | foreach ( $config as $key => $section ) { |
127 | $extra = []; |
128 | if ( $key === 'FILES' ) { |
129 | $extra = $this->getFilesSchemaExtra( $section ); |
130 | } elseif ( $key === 'MANGLER' ) { |
131 | $class = $section[ 'class' ] ?? null; |
132 | // FIXME: UGLY HACK: StringMatcher is now under a namespace so use the fully prefixed |
133 | // class to check if it has the getExtraSchema method |
134 | if ( $class === 'StringMatcher' ) { |
135 | $extra = StringMatcher::getExtraSchema(); |
136 | } |
137 | } else { |
138 | $extra = $this->callGetExtraSchema( $section[ 'class' ] ?? null ); |
139 | } |
140 | |
141 | $schema = array_replace_recursive( $schema, $extra ); |
142 | } |
143 | |
144 | $schema = new MetaYaml( $schema ); |
145 | $schema->validate( $config ); |
146 | } |
147 | |
148 | /** Merges a document template (base) to actual definition (specific) */ |
149 | public static function mergeTemplate( array $base, array $specific ): array { |
150 | foreach ( $specific as $key => $value ) { |
151 | if ( is_array( $value ) && isset( $base[$key] ) && is_array( $base[$key] ) ) { |
152 | $base[$key] = self::mergeTemplate( $base[$key], $value ); |
153 | } else { |
154 | $base[$key] = $value; |
155 | } |
156 | } |
157 | |
158 | return $base; |
159 | } |
160 | |
161 | private function getFilesSchemaExtra( array $section ): array { |
162 | $class = $section['class'] ?? null; |
163 | $format = $section['format'] ?? null; |
164 | $className = null; |
165 | |
166 | if ( $format ) { |
167 | $className = $this->fileFormatFactory->getClassname( $format ); |
168 | } elseif ( $class ) { |
169 | $className = $class; |
170 | } |
171 | |
172 | return $this->callGetExtraSchema( $className ); |
173 | } |
174 | |
175 | private function callGetExtraSchema( ?string $className ): array { |
176 | if ( $className && is_callable( [ $className, 'getExtraSchema' ] ) ) { |
177 | return call_user_func( [ $className, 'getExtraSchema' ] ); |
178 | } |
179 | |
180 | return []; |
181 | } |
182 | } |