Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
58.89% |
53 / 90 |
|
16.67% |
1 / 6 |
CRAP | |
0.00% |
0 / 1 |
LexiconSpeechoidStorage | |
58.89% |
53 / 90 |
|
16.67% |
1 / 6 |
97.77 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
findLexiconNameByLanguage | |
45.45% |
5 / 11 |
|
0.00% |
0 / 1 |
2.65 | |||
getEntry | |
80.95% |
17 / 21 |
|
0.00% |
0 / 1 |
7.34 | |||
createEntryItem | |
66.67% |
14 / 21 |
|
0.00% |
0 / 1 |
10.37 | |||
updateEntryItem | |
71.43% |
15 / 21 |
|
0.00% |
0 / 1 |
9.49 | |||
deleteEntryItem | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Wikispeech\Lexicon; |
4 | |
5 | /** |
6 | * @file |
7 | * @ingroup Extensions |
8 | * @license GPL-2.0-or-later |
9 | */ |
10 | |
11 | use InvalidArgumentException; |
12 | use LogicException; |
13 | use MediaWiki\Wikispeech\SpeechoidConnector; |
14 | use MediaWiki\Wikispeech\SpeechoidConnectorException; |
15 | use RuntimeException; |
16 | use WANObjectCache; |
17 | |
18 | /** |
19 | * @since 0.1.8 |
20 | */ |
21 | class LexiconSpeechoidStorage implements LexiconStorage { |
22 | |
23 | /** @var string */ |
24 | public const CACHE_CLASS = 'Wikispeech.LexiconSpeechoidStorage.lexiconName'; |
25 | |
26 | /** @var SpeechoidConnector */ |
27 | private $speechoidConnector; |
28 | |
29 | /** |
30 | * @var mixed Some sort of cache, used to keep track of lexicons per language. |
31 | * Needs to support makeKey(), get() and set(). |
32 | * Normally this would be a WANObjectCache. |
33 | */ |
34 | private $cache; |
35 | |
36 | /** |
37 | * @since 0.1.8 |
38 | * @param SpeechoidConnector $speechoidConnector |
39 | * @param mixed $cache |
40 | */ |
41 | public function __construct( |
42 | SpeechoidConnector $speechoidConnector, |
43 | $cache |
44 | ) { |
45 | $this->speechoidConnector = $speechoidConnector; |
46 | $this->cache = $cache; |
47 | } |
48 | |
49 | /** |
50 | * @since 0.1.8 |
51 | * @param string $language ISO 639 language code passed down to Speechoid |
52 | * @return string|null |
53 | * @throws InvalidArgumentException If $language is not 2 characters long. |
54 | */ |
55 | private function findLexiconNameByLanguage( |
56 | string $language |
57 | ): ?string { |
58 | $language = strtolower( $language ); |
59 | $cacheKey = $this->cache->makeKey( self::CACHE_CLASS, $language ); |
60 | $lexiconName = $this->cache->get( $cacheKey ); |
61 | if ( !$lexiconName ) { |
62 | $lexiconName = $this->speechoidConnector->findLexiconByLanguage( $language ); |
63 | // @todo Consider, if null we'll request on each attempt. |
64 | // @todo Rather we could store as 'NULL' or something and keep track of that. |
65 | // @todo But perhaps it's nicer to allow for new languages without the hour delay? |
66 | $this->cache->set( |
67 | $cacheKey, |
68 | $lexiconName, |
69 | WANObjectCache::TTL_HOUR |
70 | ); |
71 | } |
72 | return $lexiconName; |
73 | } |
74 | |
75 | /** |
76 | * @since 0.1.8 |
77 | * @param string $language |
78 | * @param string $key |
79 | * @return LexiconEntry|null |
80 | * @throws RuntimeException If no lexicon is available for language. |
81 | * @throws SpeechoidConnectorException On unexpected response from Speechoid. |
82 | */ |
83 | public function getEntry( |
84 | string $language, |
85 | string $key |
86 | ): ?LexiconEntry { |
87 | if ( !$language || !$key ) { |
88 | return null; |
89 | } |
90 | |
91 | $lexiconName = $this->findLexiconNameByLanguage( $language ); |
92 | if ( $lexiconName === null ) { |
93 | throw new RuntimeException( "No lexicon available for language $language" ); |
94 | } |
95 | $status = $this->speechoidConnector->lookupLexiconEntries( $lexiconName, [ $key ] ); |
96 | if ( !$status->isOK() ) { |
97 | throw new SpeechoidConnectorException( "Unexpected response from Speechoid: $status" ); |
98 | } |
99 | $deserializedItems = $status->getValue(); |
100 | if ( $deserializedItems === [] ) { |
101 | // no such key in lexicon |
102 | return null; |
103 | } |
104 | $items = []; |
105 | foreach ( $deserializedItems as $deserializedItem ) { |
106 | $item = new LexiconEntryItem(); |
107 | $item->setProperties( $deserializedItem ); |
108 | $items[] = $item; |
109 | } |
110 | |
111 | $entry = new LexiconEntry(); |
112 | $entry->setLanguage( $language ); |
113 | $entry->setKey( $key ); |
114 | $entry->setItems( $items ); |
115 | return $entry; |
116 | } |
117 | |
118 | /** |
119 | * @since 0.1.8 |
120 | * @param string $language |
121 | * @param string $key |
122 | * @param LexiconEntryItem $item |
123 | * @throws InvalidArgumentException If $item->item is null. |
124 | * If Speechoid identity is already set. |
125 | * @throws RuntimeException |
126 | * If no lexicon is available for language. |
127 | * If failed to encode lexicon entry item properties to JSON. |
128 | * If unable to add lexicon entry to Speechoid. |
129 | * If unable to retrieve the created lexicon entry item from Speechoid. |
130 | */ |
131 | public function createEntryItem( |
132 | string $language, |
133 | string $key, |
134 | LexiconEntryItem $item |
135 | ): void { |
136 | if ( $item->getProperties() === null ) { |
137 | // @todo Better sanity check, ensure that required values (IPA, etc) are set. |
138 | throw new InvalidArgumentException( '$item->item must not be null.' ); |
139 | } |
140 | if ( $item->getSpeechoidIdentity() ) { |
141 | throw new InvalidArgumentException( 'Speechoid identity is already set.' ); |
142 | } |
143 | $lexiconName = $this->findLexiconNameByLanguage( $language ); |
144 | if ( $lexiconName === null ) { |
145 | throw new RuntimeException( "No lexicon available for language $language" ); |
146 | } |
147 | $json = $item->toJson(); |
148 | if ( $json === '' ) { |
149 | throw new RuntimeException( 'Failed to encode lexicon entry item properties to JSON.' ); |
150 | } |
151 | $status = $this->speechoidConnector->addLexiconEntry( $lexiconName, $json ); |
152 | if ( !$status->isOK() ) { |
153 | throw new RuntimeException( "Failed to add lexicon entry: $status" ); |
154 | } |
155 | // Speechoid returns the identity. We need the actual entry. |
156 | // Thus we make a new request and find that entry. |
157 | // @todo Implement this when done on server side. https://phabricator.wikimedia.org/T277852 |
158 | |
159 | /** @var int $speechoidIdentity */ |
160 | $speechoidIdentity = $status->getValue(); |
161 | $speechoidEntry = $this->getEntry( $language, $key ); |
162 | if ( $speechoidEntry === null ) { |
163 | throw new LogicException( "Expected the created lexicon entry to exist." ); |
164 | } |
165 | $speechoidEntryItem = $speechoidEntry->findItemBySpeechoidIdentity( $speechoidIdentity ); |
166 | if ( $speechoidEntryItem === null ) { |
167 | throw new LogicException( 'Expected the created lexicon entry item to exist.' ); |
168 | } |
169 | $item->copyFrom( $speechoidEntryItem ); |
170 | } |
171 | |
172 | /** |
173 | * @since 0.1.8 |
174 | * @param string $language |
175 | * @param string $key |
176 | * @param LexiconEntryItem $item |
177 | * @throws InvalidArgumentException If $item->item is null. |
178 | * If Speechoid identity is not set. |
179 | * @throws RuntimeException |
180 | * If no lexicon is available for language. |
181 | * If failed to encode lexicon entry item properties to JSON. |
182 | * @throws LogicException |
183 | * If updated entry should have existed |
184 | */ |
185 | public function updateEntryItem( |
186 | string $language, |
187 | string $key, |
188 | LexiconEntryItem $item |
189 | ): void { |
190 | if ( $item->getProperties() === null ) { |
191 | // @todo Better sanity check, ensure that required values (IPA, etc) are set. |
192 | throw new InvalidArgumentException( '$item->item must not be null.' ); |
193 | } |
194 | $speechoidIdentity = $item->getSpeechoidIdentity(); |
195 | if ( $speechoidIdentity === null ) { |
196 | throw new InvalidArgumentException( 'Speechoid identity not set.' ); |
197 | } |
198 | $lexiconName = $this->findLexiconNameByLanguage( $language ); |
199 | if ( $lexiconName === null ) { |
200 | throw new RuntimeException( "No lexicon available for language $language" ); |
201 | } |
202 | $json = $item->toJson(); |
203 | if ( $json === '' ) { |
204 | throw new RuntimeException( 'Failed to encode lexicon entry item properties to JSON.' ); |
205 | } |
206 | // @todo The lexicon name is embedded in $json here. |
207 | // @todo We want to use our own data model and produce a speechoid object from that instead. |
208 | $status = $this->speechoidConnector->updateLexiconEntry( $json ); |
209 | if ( !$status->isOK() ) { |
210 | throw new RuntimeException( "Failed to update lexicon entry item: $status" ); |
211 | } |
212 | |
213 | // SpeechoidConnector::updateLexiconEntry does not return dbRef, |
214 | // So we need to request the entry again from Speechoid. |
215 | // @todo Ask STTS to return complete result at update. |
216 | $speechoidEntry = $this->getEntry( $language, $key ); |
217 | if ( $speechoidEntry === null ) { |
218 | throw new LogicException( "Expected the updated lexicon entry to exist." ); |
219 | } |
220 | $speechoidEntryItem = $speechoidEntry->findItemBySpeechoidIdentity( $speechoidIdentity ); |
221 | if ( $speechoidEntryItem === null ) { |
222 | throw new LogicException( 'Expected the updated lexicon entry item to exist.' ); |
223 | } |
224 | $item->copyFrom( $speechoidEntryItem ); |
225 | } |
226 | |
227 | /** |
228 | * @since 0.1.8 |
229 | * @param string $language |
230 | * @param string $key |
231 | * @param LexiconEntryItem $item |
232 | * @throws InvalidArgumentException If $item->item is null. |
233 | * If Speechoid identity is not set. |
234 | * @throws RuntimeException |
235 | * If no lexicon is available for language. |
236 | * If failed to delete the lexicon entry item. |
237 | */ |
238 | public function deleteEntryItem( |
239 | string $language, |
240 | string $key, |
241 | LexiconEntryItem $item |
242 | ): void { |
243 | if ( $item->getProperties() === null ) { |
244 | throw new InvalidArgumentException( '$item->item must not be null.' ); |
245 | } |
246 | $itemSpeechoidIdentity = $item->getSpeechoidIdentity(); |
247 | if ( $itemSpeechoidIdentity === null ) { |
248 | throw new InvalidArgumentException( 'Speechoid identity not set.' ); |
249 | } |
250 | $lexiconName = $this->findLexiconNameByLanguage( $language ); |
251 | if ( $lexiconName === null ) { |
252 | throw new RuntimeException( "No lexicon available for language $language" ); |
253 | } |
254 | $status = $this->speechoidConnector->deleteLexiconEntry( |
255 | $lexiconName, |
256 | $itemSpeechoidIdentity |
257 | ); |
258 | if ( !$status->isOK() ) { |
259 | throw new RuntimeException( "Failed to delete lexicon entry item: $status" ); |
260 | } |
261 | } |
262 | |
263 | } |