Skip to content

Commit

Permalink
[ECO-4943] feat: basic sending and receiving messages in the Chat
Browse files Browse the repository at this point in the history
In this commit, we introduce the basic implementation for sending and receiving messages. Corner cases and potential race conditions are not the focus at this stage and will be addressed later, once Room lifecycle management is implemented and a unified test suite is set up.
  • Loading branch information
ttypic committed Sep 13, 2024
1 parent 14aecaf commit 98158ac
Show file tree
Hide file tree
Showing 13 changed files with 666 additions and 207 deletions.
87 changes: 5 additions & 82 deletions chat-android/src/main/java/com/ably/chat/ChatApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package com.ably.chat

import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import io.ably.lib.http.HttpCore
import io.ably.lib.http.HttpUtils
import io.ably.lib.types.AblyException
import io.ably.lib.types.AsyncHttpPaginatedResponse
import io.ably.lib.types.ErrorInfo
Expand All @@ -17,19 +14,20 @@ private const val API_PROTOCOL_VERSION = 3
private const val PROTOCOL_VERSION_PARAM_NAME = "v"
private val apiProtocolParam = Param(PROTOCOL_VERSION_PARAM_NAME, API_PROTOCOL_VERSION.toString())

// TODO make this class internal
class ChatApi(private val realtimeClient: RealtimeClient, private val clientId: String) {
internal class ChatApi(private val realtimeClient: RealtimeClient, private val clientId: String) {

/**
* Get messages from the Chat Backend
*
* @return paginated result with messages
*/
suspend fun getMessages(roomId: String, params: QueryOptions): PaginatedResult<Message> {
suspend fun getMessages(roomId: String, options: QueryOptions, fromSerial: String? = null): PaginatedResult<Message> {
val baseParams = options.toParams()
val params = fromSerial?.let { baseParams + Param("fromSerial", it) } ?: baseParams
return makeAuthorizedPaginatedRequest(
url = "/chat/v1/rooms/$roomId/messages",
method = "GET",
params = params.toParams(),
params = params,
) {
Message(
timeserial = it.requireString("timeserial"),
Expand Down Expand Up @@ -137,17 +135,6 @@ class ChatApi(private val realtimeClient: RealtimeClient, private val clientId:
}
}

private fun JsonElement?.toRequestBody(useBinaryProtocol: Boolean = false): HttpCore.RequestBody =
HttpUtils.requestBodyFromGson(this, useBinaryProtocol)

private fun Map<String, String>.toJson() = JsonObject().apply {
forEach { (key, value) -> addProperty(key, value) }
}

private fun JsonElement.toMap() = buildMap<String, String> {
requireJsonObject().entrySet().filter { (_, value) -> value.isJsonPrimitive }.forEach { (key, value) -> put(key, value.asString) }
}

private fun QueryOptions.toParams() = buildList {
start?.let { add(Param("start", it)) }
end?.let { add(Param("end", it)) }
Expand All @@ -162,67 +149,3 @@ private fun QueryOptions.toParams() = buildList {
),
)
}

private fun JsonElement.requireJsonObject(): JsonObject {
if (!isJsonObject) {
throw AblyException.fromErrorInfo(
ErrorInfo("Response value expected to be JsonObject, got primitive instead", HttpStatusCodes.InternalServerError),
)
}
return asJsonObject
}

private fun JsonElement.requireString(memberName: String): String {
val memberElement = requireField(memberName)
if (!memberElement.isJsonPrimitive) {
throw AblyException.fromErrorInfo(
ErrorInfo(
"Value for \"$memberName\" field expected to be JsonPrimitive, got object instead",
HttpStatusCodes.InternalServerError,
),
)
}
return memberElement.asString
}

private fun JsonElement.requireLong(memberName: String): Long {
val memberElement = requireJsonPrimitive(memberName)
try {
return memberElement.asLong
} catch (formatException: NumberFormatException) {
throw AblyException.fromErrorInfo(
formatException,
ErrorInfo("Required numeric field \"$memberName\" is not a valid long", HttpStatusCodes.InternalServerError),
)
}
}

private fun JsonElement.requireInt(memberName: String): Int {
val memberElement = requireJsonPrimitive(memberName)
try {
return memberElement.asInt
} catch (formatException: NumberFormatException) {
throw AblyException.fromErrorInfo(
formatException,
ErrorInfo("Required numeric field \"$memberName\" is not a valid int", HttpStatusCodes.InternalServerError),
)
}
}

private fun JsonElement.requireJsonPrimitive(memberName: String): JsonPrimitive {
val memberElement = requireField(memberName)
if (!memberElement.isJsonPrimitive) {
throw AblyException.fromErrorInfo(
ErrorInfo(
"Value for \"$memberName\" field expected to be JsonPrimitive, got object instead",
HttpStatusCodes.InternalServerError,
),
)
}
return memberElement.asJsonPrimitive
}

private fun JsonElement.requireField(memberName: String): JsonElement = requireJsonObject().get(memberName)
?: throw AblyException.fromErrorInfo(
ErrorInfo("Required field \"$memberName\" is missing", HttpStatusCodes.InternalServerError),
)
3 changes: 2 additions & 1 deletion chat-android/src/main/java/com/ably/chat/ChatClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ interface ChatClient {
val clientOptions: ClientOptions
}

fun ChatClient(realtimeClient: RealtimeClient, clientOptions: ClientOptions): ChatClient = DefaultChatClient(realtimeClient, clientOptions)
fun ChatClient(realtimeClient: RealtimeClient, clientOptions: ClientOptions = ClientOptions()): ChatClient =
DefaultChatClient(realtimeClient, clientOptions)

internal class DefaultChatClient(
override val realtime: RealtimeClient,
Expand Down
84 changes: 84 additions & 0 deletions chat-android/src/main/java/com/ably/chat/JsonUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.ably.chat

import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import io.ably.lib.http.HttpCore
import io.ably.lib.http.HttpUtils
import io.ably.lib.types.AblyException
import io.ably.lib.types.ErrorInfo

internal fun JsonElement?.toRequestBody(useBinaryProtocol: Boolean = false): HttpCore.RequestBody =
HttpUtils.requestBodyFromGson(this, useBinaryProtocol)

internal fun Map<String, String>.toJson() = JsonObject().apply {
forEach { (key, value) -> addProperty(key, value) }
}

internal fun JsonElement.toMap() = buildMap<String, String> {
requireJsonObject().entrySet().filter { (_, value) -> value.isJsonPrimitive }.forEach { (key, value) -> put(key, value.asString) }
}

internal fun JsonElement.requireJsonObject(): JsonObject {
if (!isJsonObject) {
throw AblyException.fromErrorInfo(
ErrorInfo("Response value expected to be JsonObject, got primitive instead", HttpStatusCodes.InternalServerError),
)
}
return asJsonObject
}

internal fun JsonElement.requireString(memberName: String): String {
val memberElement = requireField(memberName)
if (!memberElement.isJsonPrimitive) {
throw AblyException.fromErrorInfo(
ErrorInfo(
"Value for \"$memberName\" field expected to be JsonPrimitive, got object instead",
HttpStatusCodes.InternalServerError,
),
)
}
return memberElement.asString
}

internal fun JsonElement.requireLong(memberName: String): Long {
val memberElement = requireJsonPrimitive(memberName)
try {
return memberElement.asLong
} catch (formatException: NumberFormatException) {
throw AblyException.fromErrorInfo(
formatException,
ErrorInfo("Required numeric field \"$memberName\" is not a valid long", HttpStatusCodes.InternalServerError),
)
}
}

internal fun JsonElement.requireInt(memberName: String): Int {
val memberElement = requireJsonPrimitive(memberName)
try {
return memberElement.asInt
} catch (formatException: NumberFormatException) {
throw AblyException.fromErrorInfo(
formatException,
ErrorInfo("Required numeric field \"$memberName\" is not a valid int", HttpStatusCodes.InternalServerError),
)
}
}

internal fun JsonElement.requireJsonPrimitive(memberName: String): JsonPrimitive {
val memberElement = requireField(memberName)
if (!memberElement.isJsonPrimitive) {
throw AblyException.fromErrorInfo(
ErrorInfo(
"Value for \"$memberName\" field expected to be JsonPrimitive, got object instead",
HttpStatusCodes.InternalServerError,
),
)
}
return memberElement.asJsonPrimitive
}

internal fun JsonElement.requireField(memberName: String): JsonElement = requireJsonObject().get(memberName)
?: throw AblyException.fromErrorInfo(
ErrorInfo("Required field \"$memberName\" is missing", HttpStatusCodes.InternalServerError),
)
Loading

0 comments on commit 98158ac

Please sign in to comment.