Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.89% covered (warning)
58.89%
53 / 90
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LexiconSpeechoidStorage
58.89% covered (warning)
58.89%
53 / 90
16.67% covered (danger)
16.67%
1 / 6
97.77
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
66.67% covered (warning)
66.67%
14 / 21
0.00% covered (danger)
0.00%
0 / 1
10.37
 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 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        $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}