Added message and description to CallResponse
This commit is contained in:
parent
191fa0ef44
commit
a272599b09
4 changed files with 181 additions and 154 deletions
|
@ -31,6 +31,7 @@
|
|||
|
||||
package net.thauvin.erik.bitly
|
||||
|
||||
import net.thauvin.erik.bitly.Utils.toEndPoint
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
@ -107,13 +108,13 @@ open class Bitly() {
|
|||
/**
|
||||
* Executes an API call.
|
||||
*
|
||||
* @param endPoint The REST endpoint. (eg. `https://api-ssl.bitly.com/v4/shorten`)
|
||||
* @param endPoint The REST endpoint path. (eg. `shorten`, `expand`, etc.)
|
||||
* @param params The request parameters key/value map.
|
||||
* @param method The submission [Method][Methods].
|
||||
* @return A [CallResponse] object.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun call(endPoint: String, params: Map<String, Any> = emptyMap(), method: Methods = Methods.POST): CallResponse {
|
||||
return Utils.call(accessToken, endPoint, params, method)
|
||||
return Utils.call(accessToken, endPoint.toEndPoint(), params, method)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
package net.thauvin.erik.bitly
|
||||
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
@ -46,157 +46,146 @@ import java.util.logging.Level
|
|||
import java.util.logging.Logger
|
||||
|
||||
/** Provides useful generic functions. */
|
||||
open class Utils private constructor() {
|
||||
companion object {
|
||||
/** The logger instance. */
|
||||
val logger: Logger by lazy { Logger.getLogger(Utils::class.java.name) }
|
||||
object Utils {
|
||||
/** The logger instance. */
|
||||
@JvmStatic
|
||||
val logger: Logger by lazy { Logger.getLogger(Utils::class.java.name) }
|
||||
|
||||
/**
|
||||
* Executes an API call.
|
||||
*
|
||||
* @param accessToken The API access token.
|
||||
* @param endPoint The REST endpoint. (eg. `https://api-ssl.bitly.com/v4/shorten`)
|
||||
* @param params The request parameters key/value map.
|
||||
* @param method The submission [Method][Methods].
|
||||
* @return A [CallResponse] object.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun call(
|
||||
accessToken: String,
|
||||
endPoint: String,
|
||||
params: Map<String, Any> = emptyMap(),
|
||||
method: Methods = Methods.POST
|
||||
): CallResponse {
|
||||
val response = CallResponse()
|
||||
if (validateCall(accessToken, endPoint)) {
|
||||
endPoint.toHttpUrlOrNull()?.let { apiUrl ->
|
||||
val builder = when (method) {
|
||||
Methods.POST, Methods.PATCH -> {
|
||||
val formBody = JSONObject(params).toString()
|
||||
.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||
Request.Builder().apply {
|
||||
url(apiUrl.newBuilder().build())
|
||||
if (method == Methods.POST) {
|
||||
post(formBody)
|
||||
} else {
|
||||
patch(formBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Executes an API call.
|
||||
*
|
||||
* @param accessToken The API access token.
|
||||
* @param endPoint The REST endpoint URI. (eg. `https://api-ssl.bitly.com/v4/shorten`)
|
||||
* @param params The request parameters key/value map.
|
||||
* @param method The submission [Method][Methods].
|
||||
* @return A [CallResponse] object.
|
||||
*/
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun call(
|
||||
accessToken: String,
|
||||
endPoint: String,
|
||||
params: Map<String, Any> = emptyMap(),
|
||||
method: Methods = Methods.POST
|
||||
): CallResponse {
|
||||
require(endPoint.isNotBlank()) { "A valid API endpoint must be specified." }
|
||||
require(accessToken.isNotBlank()) { "A valid API access token must be provided." }
|
||||
|
||||
Methods.DELETE -> Request.Builder().url(apiUrl.newBuilder().build()).delete()
|
||||
else -> { // Methods.GET
|
||||
val httpUrl = apiUrl.newBuilder().apply {
|
||||
params.forEach {
|
||||
if (it.value is String) {
|
||||
addQueryParameter(it.key, it.value.toString())
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
Request.Builder().url(httpUrl)
|
||||
}
|
||||
}.addHeader("Authorization", "Bearer $accessToken")
|
||||
|
||||
val result = createHttpClient().newCall(builder.build()).execute()
|
||||
return CallResponse(parseBody(endPoint, result), result.code)
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
private fun createHttpClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder().apply {
|
||||
if (logger.isLoggable(Level.FINE)) {
|
||||
addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
redactHeader("Authorization")
|
||||
})
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun parseBody(endPoint: String, result: Response): String {
|
||||
result.body?.string()?.let { body ->
|
||||
if (!result.isSuccessful && body.isNotEmpty()) {
|
||||
try {
|
||||
with(JSONObject(body)) {
|
||||
if (logger.isSevereLoggable()) {
|
||||
if (has("message")) {
|
||||
logger.severe(getString("message") + " (${result.code})")
|
||||
}
|
||||
if (has("description")) {
|
||||
logger.severe(getString("description"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (jse: JSONException) {
|
||||
if (logger.isSevereLoggable()) {
|
||||
logger.log(
|
||||
Level.SEVERE,
|
||||
"An error occurred parsing the error response from Bitly. [$endPoint]",
|
||||
jse
|
||||
)
|
||||
endPoint.toHttpUrl().let { apiUrl ->
|
||||
val builder = when (method) {
|
||||
Methods.POST, Methods.PATCH -> {
|
||||
val formBody = JSONObject(params).toString()
|
||||
.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||
Request.Builder().apply {
|
||||
url(apiUrl.newBuilder().build())
|
||||
if (method == Methods.POST) {
|
||||
post(formBody)
|
||||
} else {
|
||||
patch(formBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
return body
|
||||
|
||||
Methods.DELETE -> Request.Builder().url(apiUrl.newBuilder().build()).delete()
|
||||
else -> { // Methods.GET
|
||||
val httpUrl = apiUrl.newBuilder().apply {
|
||||
params.forEach {
|
||||
if (it.value is String) {
|
||||
addQueryParameter(it.key, it.value.toString())
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
Request.Builder().url(httpUrl)
|
||||
}
|
||||
}.addHeader("Authorization", "Bearer $accessToken")
|
||||
|
||||
newHttpClient().newCall(builder.build()).execute().use {
|
||||
return parseResponse(it, endPoint)
|
||||
}
|
||||
return Constants.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is [Level.SEVERE] logging enabled.
|
||||
*/
|
||||
fun Logger.isSevereLoggable(): Boolean = this.isLoggable(Level.SEVERE)
|
||||
private fun newHttpClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder().apply {
|
||||
if (logger.isLoggable(Level.FINE)) {
|
||||
addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
redactHeader("Authorization")
|
||||
})
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a URL.
|
||||
*/
|
||||
fun String.isValidUrl(): Boolean {
|
||||
if (this.isNotBlank()) {
|
||||
private fun parseResponse(response: Response, endPoint: String): CallResponse {
|
||||
var message = response.message
|
||||
var description = ""
|
||||
var json = Constants.EMPTY_JSON
|
||||
response.body?.string()?.let { body ->
|
||||
json = body
|
||||
if (!response.isSuccessful && body.isNotEmpty()) {
|
||||
try {
|
||||
URL(this)
|
||||
return true
|
||||
} catch (e: MalformedURLException) {
|
||||
if (logger.isLoggable(Level.WARNING)) {
|
||||
logger.log(Level.WARNING, "Invalid URL: $this", e)
|
||||
with(JSONObject(body)) {
|
||||
if (has("message")) {
|
||||
message = getString("message")
|
||||
}
|
||||
if (has("description")) {
|
||||
description = getString("description")
|
||||
}
|
||||
}
|
||||
} catch (jse: JSONException) {
|
||||
if (logger.isSevereLoggable()) {
|
||||
logger.log(
|
||||
Level.SEVERE,
|
||||
"An error occurred parsing the error response from Bitly. [$endPoint]",
|
||||
jse
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return CallResponse(json, message, description, response.code)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes http(s) scheme from string.
|
||||
*/
|
||||
fun String.removeHttp(): String {
|
||||
return this.replaceFirst("^[Hh][Tt]{2}[Pp][Ss]?://".toRegex(), "")
|
||||
}
|
||||
/**
|
||||
* Determines if [Level.SEVERE] logging is enabled.
|
||||
*/
|
||||
fun Logger.isSevereLoggable(): Boolean = this.isLoggable(Level.SEVERE)
|
||||
|
||||
/**
|
||||
* Builds the full API endpoint URL using the [Constants.API_BASE_URL].
|
||||
*/
|
||||
fun String.toEndPoint(): String {
|
||||
return if (this.startsWith('/')) {
|
||||
"${Constants.API_BASE_URL}$this"
|
||||
} else {
|
||||
"${Constants.API_BASE_URL}/$this"
|
||||
/**
|
||||
* Validates a URL.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun String.isValidUrl(): Boolean {
|
||||
if (this.isNotBlank()) {
|
||||
try {
|
||||
URL(this)
|
||||
return true
|
||||
} catch (e: MalformedURLException) {
|
||||
if (logger.isLoggable(Level.WARNING)) {
|
||||
logger.log(Level.WARNING, "Invalid URL: $this", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun validateCall(accessToken: String, endPoint: String): Boolean {
|
||||
when {
|
||||
endPoint.isBlank() -> {
|
||||
if (logger.isSevereLoggable()) logger.severe("Please specify a valid API endpoint.")
|
||||
}
|
||||
/**
|
||||
* Removes http(s) scheme from string.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun String.removeHttp(): String {
|
||||
return this.replaceFirst("^[Hh][Tt]{2}[Pp][Ss]?://".toRegex(), "")
|
||||
}
|
||||
|
||||
accessToken.isBlank() -> {
|
||||
if (logger.isSevereLoggable()) logger.severe("Please specify a valid API access token.")
|
||||
}
|
||||
|
||||
else -> return true
|
||||
}
|
||||
return false
|
||||
/**
|
||||
* Builds the full API endpoint URL using the [Constants.API_BASE_URL].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun String.toEndPoint(): String {
|
||||
return if (this.isBlank() || this.startsWith("http", true)) {
|
||||
this
|
||||
} else {
|
||||
"${Constants.API_BASE_URL}/${this.removePrefix("/")}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,22 +34,27 @@ package net.thauvin.erik.bitly
|
|||
import assertk.all
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.contains
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNotEqualTo
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.matches
|
||||
import assertk.assertions.prop
|
||||
import net.thauvin.erik.bitly.Utils.Companion.isValidUrl
|
||||
import net.thauvin.erik.bitly.Utils.Companion.removeHttp
|
||||
import net.thauvin.erik.bitly.Utils.Companion.toEndPoint
|
||||
import assertk.assertions.startsWith
|
||||
import net.thauvin.erik.bitly.Utils.isValidUrl
|
||||
import net.thauvin.erik.bitly.Utils.removeHttp
|
||||
import net.thauvin.erik.bitly.Utils.toEndPoint
|
||||
import net.thauvin.erik.bitly.config.CreateConfig
|
||||
import net.thauvin.erik.bitly.config.UpdateConfig
|
||||
import org.json.JSONObject
|
||||
import org.junit.Before
|
||||
import java.io.File
|
||||
import java.util.logging.Level
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BitlyTest {
|
||||
|
@ -76,7 +81,12 @@ class BitlyTest {
|
|||
if (System.getenv("CI") == "true") {
|
||||
test.accessToken = Constants.EMPTY
|
||||
}
|
||||
assertEquals(longUrl, test.bitlinks().shorten(longUrl))
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
test.bitlinks().shorten(longUrl)
|
||||
}
|
||||
assertFailsWith(IllegalArgumentException::class, "Utils.call()") {
|
||||
Utils.call("", "foo")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -100,7 +110,19 @@ class BitlyTest {
|
|||
|
||||
@Test
|
||||
fun `endPoint should be specified`() {
|
||||
assertThat(bitly.call("")).prop(CallResponse::isSuccessful).isFalse()
|
||||
assertFailsWith(IllegalArgumentException::class, "bitly.call()") {
|
||||
bitly.call("")
|
||||
}
|
||||
assertFailsWith(IllegalArgumentException::class, "Utils.call()") {
|
||||
Utils.call("1234568", "")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `endPoint conversion`() {
|
||||
assertThat(Constants.API_BASE_URL.toEndPoint()).isEqualTo(Constants.API_BASE_URL)
|
||||
assertThat("path".toEndPoint()).isEqualTo("${Constants.API_BASE_URL}/path")
|
||||
assertThat("/path".toEndPoint()).isEqualTo("${Constants.API_BASE_URL}/path")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -110,16 +132,18 @@ class BitlyTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `as json`() {
|
||||
fun `shorten as json`() {
|
||||
assertTrue(bitly.bitlinks().shorten(longUrl, toJson = true).startsWith("{\"created_at\":"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get user`() {
|
||||
assertThat(bitly.call("user".toEndPoint(), method = Methods.GET), "call(user)")
|
||||
.prop(CallResponse::isSuccessful).isTrue()
|
||||
assertThat(Utils.call(bitly.accessToken, "/user".toEndPoint(), method = Methods.GET), "call(/user)")
|
||||
assertThat(bitly.call("user", method = Methods.GET), "call(user)")
|
||||
.prop(CallResponse::isSuccessful).isTrue()
|
||||
assertThat(Utils.call(bitly.accessToken, "user".toEndPoint(), method = Methods.GET), "call(/user)").all {
|
||||
prop(CallResponse::isSuccessful).isTrue()
|
||||
prop(CallResponse::body).contains("login")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -128,7 +152,7 @@ class BitlyTest {
|
|||
"ethauvin",
|
||||
JSONObject(
|
||||
bitly.call(
|
||||
"/bitlinks/${shortUrl.removeHttp()}".toEndPoint(),
|
||||
"/bitlinks/${shortUrl.removeHttp()}",
|
||||
method = Methods.GET
|
||||
).body
|
||||
).getString("created_by")
|
||||
|
@ -151,22 +175,31 @@ class BitlyTest {
|
|||
bl.shorten(longUrl, domain = "bit.ly")
|
||||
assertThat(bl.lastCallResponse, "shorten(longUrl)").all {
|
||||
prop(CallResponse::isSuccessful).isTrue()
|
||||
prop(CallResponse::resultCode).isEqualTo(200)
|
||||
prop(CallResponse::statusCode).isEqualTo(200)
|
||||
prop(CallResponse::body).contains("\"link\":\"$shortUrl\"")
|
||||
prop(CallResponse::message).isEmpty()
|
||||
}
|
||||
|
||||
bl.shorten(shortUrl)
|
||||
assertThat(bl.lastCallResponse, "shorten(shortUrl)").all {
|
||||
prop(CallResponse::isSuccessful).isFalse()
|
||||
prop(CallResponse::resultCode).isEqualTo(400)
|
||||
prop(CallResponse::statusCode).isEqualTo(400)
|
||||
prop(CallResponse::isBadRequest).isTrue()
|
||||
prop(CallResponse::body).contains("ALREADY_A_BITLY_LINK")
|
||||
prop(CallResponse::message).isEqualTo("ALREADY_A_BITLY_LINK")
|
||||
prop(CallResponse::description).isEqualTo("The value provided is invalid.")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicks summary`() {
|
||||
assertNotEquals(Constants.EMPTY, bitly.bitlinks().clicks(shortUrl))
|
||||
val bl = bitly.bitlinks()
|
||||
assertThat(bl.clicks(shortUrl)).isNotEqualTo(Constants.EMPTY)
|
||||
bl.clicks(shortUrl, unit = Units.MONTH, units = 6)
|
||||
assertThat(bl.lastCallResponse).all {
|
||||
prop(CallResponse::isUpgradeRequired)
|
||||
prop(CallResponse::statusCode).isEqualTo(402)
|
||||
prop(CallResponse::description).startsWith("Metrics")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -233,7 +266,9 @@ class BitlyTest {
|
|||
bl.update("bit.ly/407GjJU", id = "foo")
|
||||
assertThat(bl.lastCallResponse).all {
|
||||
prop(CallResponse::isForbidden).isTrue()
|
||||
prop(CallResponse::resultCode).isEqualTo(403)
|
||||
prop(CallResponse::statusCode).isEqualTo(403)
|
||||
prop(CallResponse::message).isEqualTo("FORBIDDEN")
|
||||
prop(CallResponse::description).contains("forbidden")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,7 +299,9 @@ class BitlyTest {
|
|||
|
||||
assertThat(bl.lastCallResponse).all {
|
||||
prop(CallResponse::isUnprocessableEntity).isTrue()
|
||||
prop(CallResponse::resultCode).isEqualTo(422)
|
||||
prop(CallResponse::statusCode).isEqualTo(422)
|
||||
prop(CallResponse::message).isEqualTo("UNPROCESSABLE_ENTITY")
|
||||
prop(CallResponse::description).contains("JSON")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue