From f519b52a4018d85bd0d365a51ab0d17658287caf Mon Sep 17 00:00:00 2001 From: Alok Shukla Date: Tue, 21 Apr 2020 05:21:44 +0530 Subject: [PATCH] Added test classes #2 Covered all the publicly documented API of rev.ai #1 --- pom.xml | 12 +++ src/main/java/RevAi.kt | 6 +- src/main/java/SessionHandler.kt | 2 +- src/main/java/Utils.kt | 23 ++++- .../ai/rev/streaming/clients/AsyncClient.kt | 94 +++++++++++++++---- .../java/ai/rev/streaming/models/JobModels.kt | 27 ++++++ .../rev/streaming/models/StreamingModels.kt | 9 +- src/test/java/.gitkeep | 0 .../rev/api/test/clients/AsyncClientTest.kt | 77 +++++++++++++++ .../api/test/clients/StreamingClientTest.kt | 60 ++++++++++++ .../java/ai/rev/api/test/utils/UtilsTest.kt | 41 ++++++++ 11 files changed, 326 insertions(+), 25 deletions(-) delete mode 100644 src/test/java/.gitkeep create mode 100644 src/test/java/ai/rev/api/test/clients/AsyncClientTest.kt create mode 100644 src/test/java/ai/rev/api/test/clients/StreamingClientTest.kt create mode 100644 src/test/java/ai/rev/api/test/utils/UtilsTest.kt diff --git a/pom.xml b/pom.xml index a1c73b2..d4e514b 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,7 @@ spring-websocket 5.2.5.RELEASE + @@ -117,12 +118,23 @@ provided + + org.json + json + 20190722 + + org.jsoup jsoup 1.13.1 + + javax.ws.rs + javax.ws.rs-api + 2.1.1 + diff --git a/src/main/java/RevAi.kt b/src/main/java/RevAi.kt index b87db5f..d18c8c8 100644 --- a/src/main/java/RevAi.kt +++ b/src/main/java/RevAi.kt @@ -16,8 +16,8 @@ package ai.rev.streaming -import ai.rev.streaming.ai.rev.streaming.clients.AsyncClient -import ai.rev.streaming.ai.rev.streaming.clients.AsyncClientImpl +import ai.rev.streaming.clients.AsyncClient +import ai.rev.streaming.clients.AsyncClientImpl import ai.rev.streaming.clients.StreamingClient import ai.rev.streaming.clients.StreamingClientImpl import ai.rev.streaming.models.ClientConfig @@ -31,7 +31,7 @@ import ai.rev.streaming.models.ClientConfig * @author shuklaalok7 (alok@clay.fish) * @since v0.1.0 2020-03-29 18:20 IST */ -class RevAi constructor(private val clientConfig: ClientConfig) : AutoCloseable { +class RevAi constructor(clientConfig: ClientConfig) : AutoCloseable { val asyncClient: AsyncClient = AsyncClientImpl(clientConfig) val streamingClient: StreamingClient = StreamingClientImpl(clientConfig) diff --git a/src/main/java/SessionHandler.kt b/src/main/java/SessionHandler.kt index 645b6cd..5eeaa63 100644 --- a/src/main/java/SessionHandler.kt +++ b/src/main/java/SessionHandler.kt @@ -124,7 +124,7 @@ internal class SessionHandler(private val config: ClientConfig) : TextWebSocketH override fun handleTextMessage(session: WebSocketSession, message: TextMessage) { logger.info("Text message received\n$message") this.session = session - val data = AppUtils.convertToRevAiResponse(message) + val data = AppUtils.convertToStreamingResponse(message) if (state.get() == State.READY) config.callback.invoke(data) else if (data.type == "connected" && state.get() != State.CLOSING && state.get() != State.CLOSED) state.set(State.READY) } diff --git a/src/main/java/Utils.kt b/src/main/java/Utils.kt index dfbc0ba..7c991d1 100644 --- a/src/main/java/Utils.kt +++ b/src/main/java/Utils.kt @@ -18,7 +18,7 @@ package ai.rev.streaming import ai.rev.streaming.models.AudioContentType import ai.rev.streaming.models.ClientConfig -import ai.rev.streaming.models.RevAiResponse +import ai.rev.streaming.models.StreamingResponse import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -30,6 +30,10 @@ import org.springframework.web.socket.WebSocketHttpHeaders import org.springframework.web.socket.client.WebSocketClient import org.springframework.web.socket.client.standard.StandardWebSocketClient import java.net.URI +import java.util.* +import javax.ws.rs.client.Client +import javax.ws.rs.client.Invocation +import javax.ws.rs.core.MediaType /** @@ -91,9 +95,24 @@ internal object AppUtils { val gson: Gson = GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() + /** + * + */ + fun createInvocation(client: Client, uri: String, config: ClientConfig, vararg queryParam: Pair): Invocation.Builder = + client.target("https://${config.baseUrl}$uri").apply { + queryParam.forEach { this.queryParam(it.first, it.second) } + }.request(MediaType.APPLICATION_JSON_TYPE) + .accept(MediaType.APPLICATION_JSON_TYPE) + .acceptEncoding("gzip", "deflate", "sdch", "br") + .acceptLanguage(Locale.US) + .header("Authorization", "Bearer ${config.accessToken}") + .header("Accept-Language", "en-GB,en;q=0.8,en-US;q=0.6,hi;q=0.4") + .header("Connection", "keep-alive") + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36") + /** * @param message [TextMessage] received from rev.ai streaming API over the websocket * @return Response that this library generates */ - fun convertToRevAiResponse(message: TextMessage): RevAiResponse = gson.fromJson(message.payload, RevAiResponse::class.java) + fun convertToStreamingResponse(message: TextMessage): StreamingResponse = gson.fromJson(message.payload, StreamingResponse::class.java) } diff --git a/src/main/java/ai/rev/streaming/clients/AsyncClient.kt b/src/main/java/ai/rev/streaming/clients/AsyncClient.kt index bd23740..acb7c4b 100644 --- a/src/main/java/ai/rev/streaming/clients/AsyncClient.kt +++ b/src/main/java/ai/rev/streaming/clients/AsyncClient.kt @@ -13,9 +13,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -package ai.rev.streaming.ai.rev.streaming.clients +package ai.rev.streaming.clients +import ai.rev.streaming.AppUtils import ai.rev.streaming.models.* +import org.json.JSONObject +import java.util.concurrent.TimeUnit +import javax.ws.rs.client.Client +import javax.ws.rs.client.ClientBuilder +import javax.ws.rs.client.Entity +import javax.ws.rs.core.GenericType +import javax.ws.rs.core.MediaType /** * @author shuklaalok7 (alok@clay.fish) @@ -27,6 +35,8 @@ interface AsyncClient { * [Documentation](https://www.rev.ai/docs#tag/Account) * * Get the developer's account information. + * + * @return Details about the developer's account */ fun getAccount(): Account? @@ -44,8 +54,9 @@ interface AsyncClient { * @param speakerChannel Identifies which channel of the job output to caption. Default is `null` which works * only for jobs with no `speaker_channels_count` provided during job submission. * @param format MIME type specifying the caption output format + * @return String in the specified format */ - fun getCaptions(id: String, speakerChannel: Int? = null, format: CaptionFormat = CaptionFormat.VTT) + fun getCaptions(id: String, speakerChannel: Int? = null, format: CaptionFormat = CaptionFormat.VTT): String? /** * [Documentation](https://www.rev.ai/docs#tag/Transcript) @@ -60,8 +71,9 @@ interface AsyncClient { * * @param id Rev.ai API Job Id * @param format MIME type specifying the transcription output format + * @return The transcript // todo the transcript has JSON structure, try to put it in POJO */ - fun getTranscript(id: String, format: TranscriptFormat = TranscriptFormat.JSON) + fun getTranscript(id: String, format: TranscriptFormat = TranscriptFormat.JSON): JSONObject? /** * [Documentation](https://www.rev.ai/docs#operation/GetJobById) @@ -114,25 +126,54 @@ interface AsyncClient { * @author shuklaalok7 (alok@clay.fish) * @since v0.2.0 2020-04-19 08:46 PM IST */ -internal class AsyncClientImpl(private val clientConfig: ClientConfig) : AsyncClient { - override fun getAccount(): Account? { - TODO("Not yet implemented") +internal class AsyncClientImpl(private val config: ClientConfig) : AsyncClient { + + private val client: Client = ClientBuilder.newBuilder().connectTimeout(config.timeout, TimeUnit.SECONDS) + .readTimeout(config.timeout, TimeUnit.SECONDS).build() + + override fun getAccount(): Account? = try { + AppUtils.createInvocation(client, "/account", config).get(Account::class.java) + } catch (e: Exception) { + // todo inspect + logger.error("Error occurred in getting account", e) + null } - override fun getCaptions(id: String, speakerChannel: Int?, format: CaptionFormat) { - TODO("Not yet implemented") + override fun getCaptions(id: String, speakerChannel: Int?, format: CaptionFormat): String? = try { + val invocationBuilder = if (speakerChannel == null) AppUtils.createInvocation(client, "/jobs/$id/captions", config) + else AppUtils.createInvocation(client, "/jobs/$id/captions", config, Pair("speaker_channel", "$speakerChannel")) + + invocationBuilder.accept(format.mime).get().entity.toString() + } catch (e: Exception) { + // todo inspect + logger.error("Error occurred in getting captions for job $id", e) + null } - override fun getTranscript(id: String, format: TranscriptFormat) { - TODO("Not yet implemented") + override fun getTranscript(id: String, format: TranscriptFormat): JSONObject? = try { + JSONObject(AppUtils.createInvocation(client, "/jobs/$id/transcript", config).accept(format.mime) + .get().entity.toString()) + } catch (e: Exception) { + // todo inspect + logger.error("Error occurred in getting transcript for job $id", e) + null } - override fun get(id: String): Job? { - TODO("Not yet implemented") + override fun get(id: String): Job? = try { + AppUtils.createInvocation(client, "/jobs/$id", config).get(Job::class.java) + } catch (e: Exception) { + // todo inspect + logger.error("Error occurred in getting job with $id", e) + null } - override fun delete(id: String): Boolean { - TODO("Not yet implemented") + override fun delete(id: String): Boolean = try { + val status = AppUtils.createInvocation(client, "/jobs/$id", config).delete().status + status in 200..299 + } catch (e: Exception) { + // todo inspect + logger.error("Error occurred in deleting job with $id", e) + false } override fun getJobs(limit: Int, startingAfter: String?): List { @@ -141,11 +182,30 @@ internal class AsyncClientImpl(private val clientConfig: ClientConfig) : AsyncCl if (limit1 == 0) return emptyList() // todo implement - return emptyList() + return try { + val queryParams = arrayListOf(Pair("limit", "$limit1")) + if (startingAfter != null) queryParams.add(Pair("starting_after", startingAfter)) + + // May need to use Jackson instead of JaxB: https://stackoverflow.com/questions/9627170/cannot-unmarshal-a-json-array-of-objects-using-jersey-client + AppUtils.createInvocation(client, "/jobs", config, *queryParams.toTypedArray()).get(object : GenericType>() {}) + } catch (e: Exception) { + // todo inspect + logger.error("Error occurred in getting jobs", e) + emptyList() + } + } + + override fun submitJob(jobRequest: JobRequest): Job? = try { + AppUtils.createInvocation(client, "/jobs", config) + .post(Entity.entity(jobRequest, MediaType.APPLICATION_JSON_TYPE), Job::class.java) + } catch (e: Exception) { + // todo inspect + logger.error("Error occurred in submitting job", e) + null } - override fun submitJob(jobRequest: JobRequest): Job? { - TODO("Not yet implemented") + companion object { + private val logger = AppUtils.getLogger() } } diff --git a/src/main/java/ai/rev/streaming/models/JobModels.kt b/src/main/java/ai/rev/streaming/models/JobModels.kt index ab7672b..d596ce0 100644 --- a/src/main/java/ai/rev/streaming/models/JobModels.kt +++ b/src/main/java/ai/rev/streaming/models/JobModels.kt @@ -148,5 +148,32 @@ data class JobRequest( */ val customVocabularies: Array? ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as JobRequest + + if (mediaUrl != other.mediaUrl) return false + if (skipDiarization != other.skipDiarization) return false + if (skipPunctuation != other.skipPunctuation) return false + if (removeDisfluencies != other.removeDisfluencies) return false + if (filterProfanity != other.filterProfanity) return false + if (speakerChannelsCount != other.speakerChannelsCount) return false + if (callbackUrl != other.callbackUrl) return false + + return true + } + + override fun hashCode(): Int { + var result = mediaUrl.hashCode() + result = 31 * result + (skipDiarization?.hashCode() ?: 0) + result = 31 * result + (skipPunctuation?.hashCode() ?: 0) + result = 31 * result + (removeDisfluencies?.hashCode() ?: 0) + result = 31 * result + (filterProfanity?.hashCode() ?: 0) + result = 31 * result + (speakerChannelsCount ?: 0) + result = 31 * result + (callbackUrl?.hashCode() ?: 0) + return result + } } diff --git a/src/main/java/ai/rev/streaming/models/StreamingModels.kt b/src/main/java/ai/rev/streaming/models/StreamingModels.kt index 9989ca9..d583210 100644 --- a/src/main/java/ai/rev/streaming/models/StreamingModels.kt +++ b/src/main/java/ai/rev/streaming/models/StreamingModels.kt @@ -30,7 +30,7 @@ data class ClientConfig( /** * Handle the response obtained from rev.ai */ - val callback: (RevAiResponse) -> Unit, + val callback: (StreamingResponse) -> Unit, /** * In case the rev.ai API needs to be changed. Do not use `http`, `https`, `ws`, or `wss` in the beginning. The @@ -38,6 +38,11 @@ data class ClientConfig( */ val baseUrl: String = "api.rev.ai/speechtotext/v1", + /** + * Timeout when connecting with rev.ai API in seconds. + */ + val timeout: Long = 60, + /** * Size of the buffer in bytes to read from the input-stream passed to [RevAi.stream]. */ @@ -92,7 +97,7 @@ data class RawParameters( * @author shuklaalok7 (alok@clay.fish) * @since v0.1.0 2020-03-29 07:54 PM IST */ -data class RevAiResponse( +data class StreamingResponse( val id: String?, val type: String?, val elements: List?, diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/test/java/ai/rev/api/test/clients/AsyncClientTest.kt b/src/test/java/ai/rev/api/test/clients/AsyncClientTest.kt new file mode 100644 index 0000000..4454698 --- /dev/null +++ b/src/test/java/ai/rev/api/test/clients/AsyncClientTest.kt @@ -0,0 +1,77 @@ +/* + Copyright 2020 ClayFish Technologies + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package ai.rev.api.test.clients + +import ai.rev.streaming.RevAi +import ai.rev.streaming.clients.AsyncClient +import ai.rev.streaming.models.AudioContentType +import ai.rev.streaming.models.ClientConfig +import ai.rev.streaming.models.JobRequest +import org.junit.Test +import kotlin.test.BeforeTest +import kotlin.test.assertNotNull + +/** + * @author shuklaalok7 (alok@clay.fish) + * @since v0.2.0 2020-04-21 04:53 AM IST + */ +class AsyncClientTest { + + private lateinit var asyncClient: AsyncClient + + @BeforeTest + fun initialize() { + asyncClient = RevAi(ClientConfig("", AudioContentType.FLAC, {})).asyncClient + } + + @Test + fun testGetAccount() { + + } + + @Test + fun testGetCaptions() { + + } + + @Test + fun testGetTranscript() { + + } + + @Test + fun testGet() { + + } + + @Test + fun testDelete() { + + } + + @Test + fun testGetJobs() { + + } + + @Test + fun testSubmitJob() { + assertNotNull(asyncClient.submitJob(JobRequest("", speakerChannelsCount = null, callbackUrl = null, + customVocabularies = null))) + } + +} diff --git a/src/test/java/ai/rev/api/test/clients/StreamingClientTest.kt b/src/test/java/ai/rev/api/test/clients/StreamingClientTest.kt new file mode 100644 index 0000000..3b789e5 --- /dev/null +++ b/src/test/java/ai/rev/api/test/clients/StreamingClientTest.kt @@ -0,0 +1,60 @@ +/* + Copyright 2020 ClayFish Technologies + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package ai.rev.api.test.clients + +import ai.rev.streaming.RevAi +import ai.rev.streaming.clients.StreamingClient +import ai.rev.streaming.models.AudioContentType +import ai.rev.streaming.models.ClientConfig +import org.junit.Test +import kotlin.test.AfterTest +import kotlin.test.BeforeTest + +/** + * @author shuklaalok7 (alok@clay.fish) + * @since v0.2.0 2020-04-21 05:07 AM IST + */ +class StreamingClientTest { + + private lateinit var streamingClient: StreamingClient + + @BeforeTest + fun initialize() { + streamingClient = RevAi(ClientConfig("", AudioContentType.FLAC, {})).streamingClient + } + + @AfterTest + fun cleanup() { + streamingClient.close() + } + + @Test + fun testStream1() { + + } + + @Test + fun testStream2() { + + } + + @Test + fun testClose() { + + } + +} diff --git a/src/test/java/ai/rev/api/test/utils/UtilsTest.kt b/src/test/java/ai/rev/api/test/utils/UtilsTest.kt new file mode 100644 index 0000000..b58d40f --- /dev/null +++ b/src/test/java/ai/rev/api/test/utils/UtilsTest.kt @@ -0,0 +1,41 @@ +/* + Copyright 2020 ClayFish Technologies + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package ai.rev.api.test.utils + +import org.junit.Test + +/** + * @author shuklaalok7 (alok@clay.fish) + * @since v0.2.0 2020-04-21 05:13 AM IST + */ +class UtilsTest { + + @Test + fun testCreateInvocation() { + + } + + @Test + fun testConvertToStreamingResponse() { + + } + + @Test + fun handshake() { + } + +}