mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-02 18:26:49 +01:00
Switched to using Jackson for json parsing.
Also changed how response is parsed, allowing base response has different models to parse. Signed-off-by: Yahor Berdnikau <egorr.berd@gmail.com>
This commit is contained in:
parent
0dd01d18ba
commit
cfa90e0a8d
@ -4,17 +4,18 @@ ext.versions = [
|
||||
compileSdk : 22,
|
||||
|
||||
buildTools : "25.0.2",
|
||||
androidTools : "2.2.3",
|
||||
androidTools : "2.3.0",
|
||||
|
||||
androidSupport : "22.2.1",
|
||||
|
||||
kotlin : "1.0.6",
|
||||
kotlin : "1.1.0",
|
||||
|
||||
retrofit : "2.1.0",
|
||||
jackson : "2.8.7",
|
||||
|
||||
junit : "4.12",
|
||||
mockitoKotlin : "1.3.0",
|
||||
kluent : "1.14",
|
||||
kluent : "1.15",
|
||||
okhttp : "3.6.0",
|
||||
]
|
||||
|
||||
@ -28,14 +29,12 @@ ext.androidSupport = [
|
||||
design : "com.android.support:design:$versions.androidSupport",
|
||||
]
|
||||
|
||||
ext.kotlin = [
|
||||
stdlib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin"
|
||||
]
|
||||
|
||||
ext.other = [
|
||||
kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib-common:$versions.kotlin",
|
||||
retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
|
||||
gsonConverter : "com.squareup.retrofit2:converter-gson:$versions.retrofit",
|
||||
simpleXmlConverter : "com.squareup.retrofit2:converter-simplexml:$versions.retrofit",
|
||||
jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit",
|
||||
jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson",
|
||||
]
|
||||
|
||||
ext.testing = [
|
||||
|
@ -8,10 +8,10 @@ sourceSets {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile kotlin.stdlib
|
||||
|
||||
compile other.kotlinStdlib
|
||||
compile other.retrofit
|
||||
compile other.gsonConverter
|
||||
compile other.jacksonConverter
|
||||
compile other.jacksonKotlin
|
||||
|
||||
testCompile testing.junit
|
||||
testCompile testing.kotlinJunit
|
||||
|
@ -3,14 +3,18 @@ package org.moire.ultrasonic.api.subsonic
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okio.Okio
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should equal`
|
||||
import org.amshove.kluent.`should not be`
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.moire.ultrasonic.api.subsonic.models.SubsonicResponse
|
||||
import org.moire.ultrasonic.api.subsonic.models.License
|
||||
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
||||
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
||||
import retrofit2.Response
|
||||
import java.nio.charset.Charset
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Integration test for [SubsonicAPI] class.
|
||||
@ -72,6 +76,21 @@ class SubsonicAPITest {
|
||||
with(response.body()) {
|
||||
status `should be` SubsonicResponse.Status.OK
|
||||
version `should be` SubsonicAPIVersions.V1_13_0
|
||||
license `should equal` License(true, parseDate("2016-11-23T20:17:15.206Z"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should parse error get license response`() {
|
||||
enqueueResponse("generic_error_response.json")
|
||||
|
||||
val response = api.getApi().getLicense().execute()
|
||||
|
||||
assertResponseSuccessful(response)
|
||||
with(response.body()) {
|
||||
status `should be` SubsonicResponse.Status.ERROR
|
||||
error `should be` SubsonicError.GENERIC
|
||||
license `should be` null
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,8 +104,16 @@ class SubsonicAPITest {
|
||||
return source.readString(Charset.forName("UTF-8"))
|
||||
}
|
||||
|
||||
private fun assertResponseSuccessful(response: Response<SubsonicResponse>) {
|
||||
private fun <T> assertResponseSuccessful(response: Response<T>) {
|
||||
response.isSuccessful `should be` true
|
||||
response.body() `should not be` null
|
||||
}
|
||||
|
||||
private fun parseDate(dateAsString: String): Calendar {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US)
|
||||
val result = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
result.time = dateFormat.parse(dateAsString.replace("Z$".toRegex(), "+0000"))
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import okhttp3.OkHttpClient
|
||||
import org.moire.ultrasonic.api.subsonic.models.SubsonicResponse
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.jackson.JacksonConverterFactory
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
@ -31,15 +32,14 @@ class SubsonicAPI(baseUrl: String,
|
||||
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
|
||||
}.build()
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SubsonicResponse::class.javaObjectType,
|
||||
SubsonicResponse.Companion.ClassTypeAdapter())
|
||||
.create()
|
||||
private val jacksonMapper = ObjectMapper()
|
||||
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
|
||||
.registerModule(KotlinModule())
|
||||
|
||||
private val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.addConverterFactory(JacksonConverterFactory.create(jacksonMapper))
|
||||
.build()
|
||||
|
||||
private val subsonicAPI = retrofit.create(SubsonicAPIDefinition::class.java)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import org.moire.ultrasonic.api.subsonic.models.SubsonicResponse
|
||||
import org.moire.ultrasonic.api.subsonic.response.LicenseResponse
|
||||
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
|
||||
@ -12,5 +13,5 @@ interface SubsonicAPIDefinition {
|
||||
fun ping(): Call<SubsonicResponse>
|
||||
|
||||
@GET("getLicense.view")
|
||||
fun getLicense(): Call<SubsonicResponse>
|
||||
fun getLicense(): Call<LicenseResponse>
|
||||
}
|
@ -1,8 +1,15 @@
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
|
||||
/**
|
||||
* Subsonic REST API versions.
|
||||
*/
|
||||
@JsonDeserialize(using = SubsonicAPIVersions.Companion.SubsonicAPIVersionsDeserializer::class)
|
||||
enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion: String) {
|
||||
V1_1_0("3.8", "1.1.0"),
|
||||
V1_1_1("3.9", "1.1.1"),
|
||||
@ -41,5 +48,14 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
||||
}
|
||||
throw IllegalArgumentException("Unknown api version $apiVersion")
|
||||
}
|
||||
|
||||
class SubsonicAPIVersionsDeserializer: JsonDeserializer<SubsonicAPIVersions>() {
|
||||
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): SubsonicAPIVersions {
|
||||
if (p!!.currentName != "version") {
|
||||
throw JsonParseException(p, "Not valid token for API version!")
|
||||
}
|
||||
return fromApiVersion(p.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,14 @@
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
|
||||
/**
|
||||
* Common API errors.
|
||||
*/
|
||||
@JsonDeserialize(using = SubsonicError.Companion.SubsonicErrorDeserializer::class)
|
||||
enum class SubsonicError(val code: Int) {
|
||||
GENERIC(0),
|
||||
REQUIRED_PARAM_MISSING(10),
|
||||
@ -18,5 +24,15 @@ enum class SubsonicError(val code: Int) {
|
||||
fun parseErrorFromJson(jsonErrorCode: Int) = SubsonicError.values()
|
||||
.filter { it.code == jsonErrorCode }.firstOrNull()
|
||||
?: throw IllegalArgumentException("Unknown code $jsonErrorCode")
|
||||
|
||||
class SubsonicErrorDeserializer: JsonDeserializer<SubsonicError>() {
|
||||
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): SubsonicError {
|
||||
p!!.nextToken() // "code"
|
||||
val error = parseErrorFromJson(p.valueAsInt)
|
||||
p.nextToken() // "message"
|
||||
p.nextToken() // end of error object
|
||||
return error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.moire.ultrasonic.api.subsonic.models
|
||||
|
||||
import java.util.*
|
||||
|
||||
data class License(val valid: Boolean, val trialExpires: Calendar)
|
@ -1,73 +0,0 @@
|
||||
package org.moire.ultrasonic.api.subsonic.models
|
||||
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicError
|
||||
|
||||
/**
|
||||
* Base Subsonic API response.
|
||||
*/
|
||||
data class SubsonicResponse(val status: Status,
|
||||
val version: SubsonicAPIVersions,
|
||||
val error: SubsonicError?) {
|
||||
enum class Status(val jsonValue: String) {
|
||||
OK("ok"), ERROR("failed");
|
||||
|
||||
companion object {
|
||||
fun getStatusFromJson(jsonValue: String) = Status.values()
|
||||
.filter { it.jsonValue == jsonValue }.firstOrNull()
|
||||
?: throw IllegalArgumentException("Unknown status value: $jsonValue")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
class ClassTypeAdapter: TypeAdapter<SubsonicResponse>() {
|
||||
override fun read(`in`: JsonReader?): SubsonicResponse {
|
||||
if (`in` == null) {
|
||||
throw NullPointerException("No json for parsing")
|
||||
}
|
||||
|
||||
var status: Status = Status.ERROR
|
||||
var version: SubsonicAPIVersions = SubsonicAPIVersions.V1_1_0
|
||||
var error: SubsonicError? = null
|
||||
`in`.beginObject()
|
||||
if ("subsonic-response" == `in`.nextName()) {
|
||||
`in`.beginObject()
|
||||
while (`in`.hasNext()) {
|
||||
when (`in`.nextName()) {
|
||||
"status" -> status = Status.getStatusFromJson(`in`.nextString())
|
||||
"version" -> version = SubsonicAPIVersions.fromApiVersion(`in`.nextString())
|
||||
"error" -> error = parseError(`in`)
|
||||
else -> `in`.skipValue()
|
||||
}
|
||||
}
|
||||
`in`.endObject()
|
||||
} else{
|
||||
throw IllegalArgumentException("Not a subsonic-response json!")
|
||||
}
|
||||
`in`.endObject()
|
||||
return SubsonicResponse(status, version, error)
|
||||
}
|
||||
|
||||
override fun write(out: JsonWriter?, value: SubsonicResponse?) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
private fun parseError(reader: JsonReader): SubsonicError? {
|
||||
var error: SubsonicError? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"code" -> error = SubsonicError.parseErrorFromJson(reader.nextInt())
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
return error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package org.moire.ultrasonic.api.subsonic.response
|
||||
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicError
|
||||
import org.moire.ultrasonic.api.subsonic.models.License
|
||||
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
||||
|
||||
class LicenseResponse(val license: License?,
|
||||
status: Status,
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?):
|
||||
SubsonicResponse(status, version, error)
|
@ -0,0 +1,39 @@
|
||||
package org.moire.ultrasonic.api.subsonic.response
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonRootName
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicError
|
||||
|
||||
/**
|
||||
* Base Subsonic API response.
|
||||
*/
|
||||
@JsonRootName(value = "subsonic-response")
|
||||
open class SubsonicResponse(val status: Status,
|
||||
val version: SubsonicAPIVersions,
|
||||
val error: SubsonicError?) {
|
||||
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
|
||||
enum class Status(val jsonValue: String) {
|
||||
OK("ok"), ERROR("failed");
|
||||
|
||||
companion object {
|
||||
fun getStatusFromJson(jsonValue: String) = values()
|
||||
.filter { it.jsonValue == jsonValue }.firstOrNull()
|
||||
?: throw IllegalArgumentException("Unknown status value: $jsonValue")
|
||||
|
||||
class StatusJsonDeserializer: JsonDeserializer<Status>() {
|
||||
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Status {
|
||||
if (p!!.currentName != "status") {
|
||||
throw JsonParseException(p,
|
||||
"Current token is not status. Current token name ${p.currentName}.")
|
||||
}
|
||||
return getStatusFromJson(p.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should throw`
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Unit test for [SubsonicAPIVersions.SubsonicAPIVersionsDeserializer] class.
|
||||
*/
|
||||
class SubsonicAPIVersionsDeserializerTest {
|
||||
private val jsonParser = mock<JsonParser>()
|
||||
private val context = mock<DeserializationContext>()
|
||||
|
||||
private lateinit var deserializer: SubsonicAPIVersions.Companion.SubsonicAPIVersionsDeserializer
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
deserializer = SubsonicAPIVersions.Companion.SubsonicAPIVersionsDeserializer()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should throw if current token name is not version`() {
|
||||
doReturn("asdasd").whenever(jsonParser).currentName
|
||||
|
||||
{ deserializer.deserialize(jsonParser, context) } `should throw` JsonParseException::class
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should return parsed version`() {
|
||||
doReturn("version").whenever(jsonParser).currentName
|
||||
doReturn(SubsonicAPIVersions.V1_13_0.restApiVersion).whenever(jsonParser).text
|
||||
|
||||
val parsedVersion = deserializer.deserialize(jsonParser, context)
|
||||
|
||||
parsedVersion `should be` SubsonicAPIVersions.V1_13_0
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import org.amshove.kluent.`should equal`
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
||||
|
||||
/**
|
||||
* Unit test for [SubsonicResponse.Status] class
|
||||
|
@ -32,10 +32,16 @@ dependencies {
|
||||
compile androidSupport.support
|
||||
compile androidSupport.design
|
||||
|
||||
compile kotlin.stdlib
|
||||
compile other.kotlinStdlib
|
||||
|
||||
testCompile testing.junit
|
||||
testCompile testing.kotlinJunit
|
||||
testCompile testing.mockitoKotlin
|
||||
testCompile testing.kluent
|
||||
testCompile(testing.kotlinJunit) {
|
||||
exclude module: "kotlin-stdlib"
|
||||
}
|
||||
testCompile(testing.mockitoKotlin) {
|
||||
exclude module: "kotlin-stdlib"
|
||||
}
|
||||
testCompile(testing.kluent) {
|
||||
exclude module: "kotlin-stdlib"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user