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