Skip to content

Commit

Permalink
Merge branch 'http' into 'master'
Browse files Browse the repository at this point in the history
Non-Volley HTTP Requests

See merge request 415-cradle/cradlemobile!49
  • Loading branch information
Jeremy Schwartz committed Aug 16, 2020
2 parents c4f3cde + 76260a2 commit c3da28c
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 1 deletion.
110 changes: 110 additions & 0 deletions app/src/main/java/com/cradle/neptune/net/Http.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.cradle.neptune.net

import com.cradle.neptune.model.Marshal
import java.lang.Exception
import java.net.HttpURLConnection
import java.net.URL

/**
* Contains functions for making generic HTTP requests.
*/
object Http {

/**
* Enumeration of common HTTP method request types.
*/
enum class Method { GET, POST, PUT, DELETE }

/**
* Performs a generic HTTP request.
*
* @param method the request method
* @param url where to send the request
* @param headers HTTP headers to include with the request
* @param body an optional body to send along with the request
* @return the result of the network request
* @throws java.net.MalformedURLException if [url] is malformed
*/
fun request(
method: Method,
url: String,
headers: Map<String, String>,
body: ByteArray?
): NetworkResult<ByteArray> =
with(URL(url).openConnection() as HttpURLConnection) {
requestMethod = method.toString()
headers.forEach { (k, v) -> addRequestProperty(k, v) }
doInput = true

try {
if (body != null) {
doOutput = true
outputStream.write(body)
}

@Suppress("MagicNumber")
if (responseCode in 200 until 300) {
val responseBody = inputStream.readBytes()
inputStream.close()
Success(responseBody, responseCode)
} else {
val responseBody = errorStream.readBytes()
errorStream.close()
Failure(responseBody, responseCode)
}
} catch (ex: Exception) {
NetworkException(ex)
}
}

/**
* Sends a generic HTTP request with a JSON body and expects a JSON
* response.
*
* The "Content-Type application/json" header is automatically included
* in requests sent using this function.
*
* @param method the request method
* @param url where to send the request
* @param headers HTTP headers to include with the request
* @param body an optional body to send along with the request
* @return the result of the network request
* @throws java.net.MalformedURLException if [url] is malformed
* @throws org.json.JSONException if the response body is not JSON
*/
fun jsonRequest(
method: Method,
url: String,
headers: Map<String, String>,
body: Json?
): NetworkResult<Json> =
request(
method,
url,
headers + ("Content-Type" to "application/json"),
body?.marshal()
).map(Json.Companion::unmarshal)

/**
* A generalized version of [jsonRequest] which accepts a generic instance
* for the [body] parameter.
*
* Useful for POST requests where you don't care about the response body.
*
* @param method the request method
* @param url where to send the request
* @param headers HTTP headers to include with the request
* @param body an optional body to send along with the request
* @return the result of the network request
* @throws java.net.MalformedURLException if [url] is malformed
* @throws org.json.JSONException if the response body is not JSON
*/
fun <Body> jsonRequest(
method: Method,
url: String,
headers: Map<String, String>,
body: Body?
): NetworkResult<Json>
where Body : Marshal<Json> =
jsonRequest(method, url, headers, body?.marshal())
}
110 changes: 110 additions & 0 deletions app/src/main/java/com/cradle/neptune/net/Json.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.cradle.neptune.net

import com.cradle.neptune.model.Marshal
import com.cradle.neptune.model.Unmarshal
import org.json.JSONArray
import org.json.JSONObject

/**
* Sum type which represents a blob of JSON data.
*/
sealed class Json : Marshal<ByteArray> {

/**
* Unwraps this JSON data as an object.
*
* Returns `null` if this is a [JsonArray] and not a [JsonObject].
*/
val obj: JSONObject?
get() = when (this) {
is JsonObject -> value
is JsonArray -> null
}

/**
* Unwraps this JSON data as an array.
*
* Returns `null` if this is a [JsonObject] and not a [JsonArray]
*/
val arr: JSONArray?
get() = when (this) {
is JsonObject -> null
is JsonArray -> value
}

/**
* Converts this JSON data into a string without any line breaks or
* indentations.
*
* @return a string representation of the JSON data
*/
abstract override fun toString(): String

/**
* Converts this JSON data into a string.
*
* @param indentFactor the number of spaces to used when indenting nested
* structures
* @return a string representation of the JSON data
*/
abstract fun toString(indentFactor: Int): String

/**
* Converts this JSON data into a byte array.
*
* @return the JSON data as a byte array
*/
final override fun marshal() = toString().toByteArray()

companion object : Unmarshal<Json, ByteArray> {
/**
* Converts a byte array into a [JsonObject] or [JsonArray].
*
* @param data the byte array to parse
* @return a [Json] variant
* @throws org.json.JSONException if unable to parse [data]
*/
override fun unmarshal(data: ByteArray): Json {
val str = String(data).trimStart()
return if (str.firstOrNull() == '{') {
JsonObject(str)
} else {
JsonArray(str)
}
}
}
}

/**
* Wraps a [JSONObject] in a [Json] variant allowing polymorphism between the
* two JSON types.
*
* @property value Underlying [JSONObject] value
*/
class JsonObject(val value: JSONObject) : Json() {

constructor() : this(JSONObject())

constructor(string: String) : this(JSONObject(string))

override fun toString() = value.toString()

override fun toString(indentFactor: Int): String = value.toString(indentFactor)
}

/**
* Wraps a [JSONArray] in a [Json] variant allowing polymorphism between the
* two JSON types.
*
* @property value Underlying [JSONArray] value
*/
class JsonArray(val value: JSONArray) : Json() {

constructor() : this(JSONArray())

constructor(string: String) : this(JSONArray(string))

override fun toString() = value.toString()

override fun toString(indentFactor: Int): String = value.toString(indentFactor)
}
110 changes: 110 additions & 0 deletions app/src/main/java/com/cradle/neptune/net/NetworkResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.cradle.neptune.net

import com.cradle.neptune.model.Unmarshal

/**
* Sum type representing the result of a network request.
*/
sealed class NetworkResult<T> {
/**
* Unwraps this network result into an optional value.
*
* @return the result value or null depending on whether the result is a
* [Success], [Failure], [NetworkException] variant
*/
val unwrapped: T?
get() = when (this) {
is Success -> value
is Failure -> null
is NetworkException -> null
}

/**
* Applies a closure [f] to transform the value field of a [Success] result.
*
* In the case of [Failure] and [NetworkException] variants, this method
* does nothing.
*
* @param f transformation to apply to the result value
* @return a new [NetworkResult] with the transformed value
*/
fun <U> map(f: (T) -> U): NetworkResult<U> = when (this) {
is Success -> Success(f(value), statusCode)
is Failure -> Failure(body, statusCode)
is NetworkException -> NetworkException(cause)
}
}

/**
* The result of a successful network request.
*
* A request is considered successful if the response has a status code in the
* 200..<300 range.
*
* @property value The result value
* @property statusCode Status code of the response which generated this result
*/
data class Success<T>(val value: T, val statusCode: Int) : NetworkResult<T>()

/**
* The result of a network request which made it to the server but the status
* code of the response indicated a failure (e.g., 404, 500, etc.).
*
* Contains the response status code along with the response body as a byte
* array. Note that the body is not of type [T] like in [Success] since the
* response for a failed request may not be the same type as the response for
* a successful request.
*
* @property body The body of the response
* @property statusCode The status code of the response
*/
data class Failure<T>(val body: ByteArray, val statusCode: Int) : NetworkResult<T>() {

/**
* Converts the response body of this failure result to some other type.
*
* @param unmarshaller an object used to unmarshall the byte array body
* into a different type
* @return a new object which was constructed from the response body
*/
fun <R, U> marshal(unmarshaller: U)
where U : Unmarshal<R, ByteArray> =
unmarshaller.unmarshal(body)

/**
* Converts the response body of this failure result to JSON.
*
* Whether a [JsonObject] or [JsonArray] is returned depends on the content
* of the response body.
*
* @return a [Json] object
* @throws org.json.JSONException if the response body cannot be converted
* into JSON.
*/
fun toJson() = marshal(Json.Companion)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as Failure<*>

if (!body.contentEquals(other.body)) return false
if (statusCode != other.statusCode) return false

return true
}

override fun hashCode(): Int {
var result = body.contentHashCode()
result = 31 * result + statusCode
return result
}
}

/**
* Represents an exception that occurred whilst making a network request.
*
* @property cause the exception which caused the failure
*/
data class NetworkException<T>(val cause: Exception) : NetworkResult<T>()
2 changes: 1 addition & 1 deletion app/src/main/java/com/cradle/neptune/network/Api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Api @Inject constructor(
* @return the response body or an error if one occurred
*/
suspend fun authenticate(username: String, password: String): NetworkResult<JsonObject> {
val obj = JsonObject(mapOf("username" to username, "password" to password))
val obj = JsonObject(mapOf("email" to username, "password" to password))
return requestObject(HttpMethod.POST, urlManager.authentication, obj)
}

Expand Down

0 comments on commit c3da28c

Please sign in to comment.