Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.70% covered (warning)
58.70%
54 / 92
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LexiconSpeechoidStorage
58.70% covered (warning)
58.70%
54 / 92
16.67% covered (danger)
16.67%
1 / 6
104.16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 findLexiconNameByLanguage
45.45% covered (danger)
45.45%
5 / 11
0.00% covered (danger)
0.00%
0 / 1
2.65
 getEntry
80.95% covered (warning)
80.95%
17 / 21
0.00% covered (danger)
0.00%
0 / 1
7.34
 createEntryItem
65.22% covered (warning)
65.22%
15 / 23
0.00% covered (danger)
0.00%
0 / 1
12.41
 updateEntryItem
71.43% covered (warning)
71.43%
15 / 21
0.00% covered (danger)
0.00%
0 / 1
9.49
 deleteEntryItem
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace MediaWiki\Wikispeech\Lexicon;
4
5/**
6 * @file
7 * @ingroup Extensions
8 * @license GPL-2.0-or-later
9 */
10
11use InvalidArgumentException;
12use LogicException;
13use MediaWiki\Wikispeech\SpeechoidConnector;
14use MediaWiki\Wikispeech\SpeechoidConnectorException;
15use RuntimeException;
16use Wikimedia\ObjectCache\WANObjectCache;
17
18/**
19 * @since 0.1.8
20 */
21class 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        if ( !is_int( $speechoidIdentity ) ) {
162            throw new LogicException( 'Expected speechoid identity to be an int.' );
163        }
164        $speechoidEntry = $this->getEntry( $language, $key );
165        if ( $speechoidEntry === null ) {
166            throw new LogicException( "Expected the created lexicon entry to exist." );
167        }
168        $speechoidEntryItem = $speechoidEntry->findItemBySpeechoidIdentity( $speechoidIdentity );
169        if ( $speechoidEntryItem === null ) {
170            throw new LogicException( 'Expected the created lexicon entry item to exist.' );
171        }
172        $item->copyFrom( $speechoidEntryItem );
173    }
174
175    /**
176     * @since 0.1.8
177     * @param string $language
178     * @param string $key
179     * @param LexiconEntryItem $item
180     * @throws InvalidArgumentException If $item->item is null.
181     *  If Speechoid identity is not set.
182     * @throws RuntimeException
183     *  If no lexicon is available for language.
184     *  If failed to encode lexicon entry item properties to JSON.
185     * @throws LogicException
186     * If updated entry should have existed
187     */
188    public function updateEntryItem(
189        string $language,
190        string $key,
191        LexiconEntryItem $item
192    ): void {
193        if ( $item->getProperties() === null ) {
194            // @todo Better sanity check, ensure that required values (IPA, etc) are set.
195            throw new InvalidArgumentException( '$item->item must not be null.' );
196        }
197        $speechoidIdentity = $item->getSpeechoidIdentity();
198        if ( $speechoidIdentity === null ) {
199            throw new InvalidArgumentException( 'Speechoid identity not set.' );
200        }
201        $lexiconName = $this->findLexiconNameByLanguage( $language );
202        if ( $lexiconName === null ) {
203            throw new RuntimeException( "No lexicon available for language $language" );
204        }
205        $json = $item->toJson();
206        if ( $json === '' ) {
207            throw new RuntimeException( 'Failed to encode lexicon entry item properties to JSON.' );
208        }
209        // @todo The lexicon name is embedded in $json here.
210        // @todo We want to use our own data model and produce a speechoid object from that instead.
211        $status = $this->speechoidConnector->updateLexiconEntry( $json );
212        if ( !$status->isOK() ) {
213            throw new RuntimeException( "Failed to update lexicon entry item: $status" );
214        }
215
216        // SpeechoidConnector::updateLexiconEntry does not return dbRef,
217        // So we need to request the entry again from Speechoid.
218        // @todo Ask STTS to return complete result at update.
219        $speechoidEntry = $this->getEntry( $language, $key );
220        if ( $speechoidEntry === null ) {
221            throw new LogicException( "Expected the updated lexicon entry to exist." );
222        }
223        $speechoidEntryItem = $speechoidEntry->findItemBySpeechoidIdentity( $speechoidIdentity );
224        if ( $speechoidEntryItem === null ) {
225            throw new LogicException( 'Expected the updated lexicon entry item to exist.' );
226        }
227        $item->copyFrom( $speechoidEntryItem );
228    }
229
230    /**
231     * @since 0.1.8
232     * @param string $language
233     * @param string $key
234     * @param LexiconEntryItem $item
235     * @throws InvalidArgumentException If $item->item is null.
236     *  If Speechoid identity is not set.
237     * @throws RuntimeException
238     *  If no lexicon is available for language.
239     *  If failed to delete the lexicon entry item.
240     */
241    public function deleteEntryItem(
242        string $language,
243        string $key,
244        LexiconEntryItem $item
245    ): void {
246        if ( $item->getProperties() === null ) {
247            throw new InvalidArgumentException( '$item->item must not be null.' );
248        }
249        $itemSpeechoidIdentity = $item->getSpeechoidIdentity();
250        if ( $itemSpeechoidIdentity === null ) {
251            throw new InvalidArgumentException( 'Speechoid identity not set.' );
252        }
253        $lexiconName = $this->findLexiconNameByLanguage( $language );
254        if ( $lexiconName === null ) {
255            throw new RuntimeException( "No lexicon available for language $language" );
256        }
257        $status = $this->speechoidConnector->deleteLexiconEntry(
258            $lexiconName,
259            $itemSpeechoidIdentity
260        );
261        if ( !$status->isOK() ) {
262            throw new RuntimeException( "Failed to delete lexicon entry item: $status" );
263        }
264    }
265
266}