Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 9 additions & 53 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ jobs:
distribution: 'zulu'
java-version: 17

- name: Setup SBT
uses: sbt/setup-sbt@v1

- name: Cache Dependencies
uses: coursier/cache-action@v6

- name: Build API jar
run: cd android && ./setup-api.sh

- name: Build debug apk
run: cd android && ./gradlew app:assembleDebug

Expand Down Expand Up @@ -99,56 +108,3 @@ jobs:
with:
name: simple-split-backend.jar
path: ./backend/app/target/scala-3.7.1/simple-split-backend.jar

transpile-and-check-api:
name: Transpile API and check it is up to date
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17

- name: Setup SBT
uses: sbt/setup-sbt@v1

- name: Cache Dependencies
uses: coursier/cache-action@v6

- name: Generate new API classes
run: |
# Backup current generated files
cp -r android/backend-api/src/main/kotlin android/backend-api/src/main/kotlin-backup
# Remove current generated files
rm -r android/backend-api/src/main/kotlin/com
# Generate new API classes
cd backend && sbt codegen/generateKotlinClasses

- name: Check if API classes are up to date
run: |
# Compare generated files with committed files
if ! diff -r android/backend-api/src/main/kotlin android/backend-api/src/main/kotlin-backup > /dev/null 2>&1; then
echo "❌ API classes are NOT up to date!"
echo "Run the following commands to update API:"
echo ""
echo " cd backend"
echo " sbt codegen/generateKotlinClasses"
echo ""
echo "Then commit the updated files."
echo ""
echo "Differences found:"
diff -r android/backend-api/src/main/kotlin android/backend-api/src/main/kotlin-backup || true
exit 1
else
echo "✅ API classes are up to date!"
fi

# Remove backup-file
rm -r android/backend-api/src/main/kotlin-backup

- name: Compile transpiled API
run: cd android && ./gradlew backend-api:assemble
5 changes: 3 additions & 2 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,11 @@ dependencies {
implementation(libs.ktorClientOkhttp)
implementation(libs.ktorClientLogging)
implementation(libs.ktorClientNegotiation)
implementation(libs.ktorSerializationJson)
// implementation(libs.ktorSerializationJson)

// Json
implementation(libs.kotlinx.json)
implementation(libs.gson)

// Arrow
implementation(libs.arrow.core)
Expand All @@ -144,7 +145,7 @@ dependencies {
implementation(libs.decompose.extensions)

// Api
implementation(project(":backend-api"))
implementation(files("libs/simple-split-api.jar"))

// Preferences
implementation(libs.ksprefs)
Expand Down
Binary file added android/app/libs/simple-split-api.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,17 @@ class ApiClient(

return httpClient.sendRequest<Unit, GetGroupsResponse>(
type = RequestType.GET,
url = "$baseUrl/group?ids=$idsStr&passwords=$passwordsStr"
url = "$baseUrl/group?ids=$idsStr&passwords=$passwordsStr",
jsonSerializer = jsonSerializer
)
}

suspend fun postGroup(request: PostGroupRequest): Either<ApiException, PostGroupResponse> =
httpClient.sendRequest<PostGroupRequest, PostGroupResponse>(
type = RequestType.POST,
url = "$baseUrl/group",
body = request
body = request,
jsonSerializer = jsonSerializer
)

suspend fun postExpense(
Expand All @@ -79,7 +81,8 @@ class ApiClient(
httpClient.sendRequest<PostExpenseRequest, PostExpenseResponse>(
type = RequestType.POST,
url = "$baseUrl/expense?password=$password",
body = request
body = request,
jsonSerializer = jsonSerializer
)

suspend fun putExpense(
Expand All @@ -90,7 +93,8 @@ class ApiClient(
httpClient.sendRequest(
type = RequestType.PUT,
url = "$baseUrl/expense/$expenseUid?password=$password",
body = request
body = request,
jsonSerializer = jsonSerializer
)

suspend fun removeExpense(
Expand All @@ -99,7 +103,8 @@ class ApiClient(
): Either<ApiException, DeleteExpenseResponse> =
httpClient.sendRequest<Unit, DeleteExpenseResponse>(
type = RequestType.DELETE,
url = "$baseUrl/expense/$expenseUid?password=$password"
url = "$baseUrl/expense/$expenseUid?password=$password",
jsonSerializer = jsonSerializer
)

suspend fun putGroup(
Expand All @@ -110,7 +115,8 @@ class ApiClient(
httpClient.sendRequest<PutGroupRequest, PutGroupResponse>(
type = RequestType.PUT,
url = "$baseUrl/group/$uid?password=$password",
body = request
body = request,
jsonSerializer = jsonSerializer
)

suspend fun postMember(
Expand All @@ -120,7 +126,8 @@ class ApiClient(
httpClient.sendRequest(
type = RequestType.POST,
url = "$baseUrl/member?password=$password",
body = request
body = request,
jsonSerializer = jsonSerializer
)

suspend fun removeMember(
Expand All @@ -129,7 +136,8 @@ class ApiClient(
): Either<ApiException, DeleteMemberResponse> =
httpClient.sendRequest<Unit, DeleteMemberResponse>(
type = RequestType.DELETE,
url = "$baseUrl/member/$memberUid?password=$password"
url = "$baseUrl/member/$memberUid?password=$password",
jsonSerializer = jsonSerializer
)

suspend fun putMember(
Expand All @@ -140,13 +148,15 @@ class ApiClient(
httpClient.sendRequest<PutMemberRequest, PutMemberResponse>(
type = RequestType.PUT,
url = "$baseUrl/member/$memberUid?password=$password",
body = request
body = request,
jsonSerializer = jsonSerializer
)

suspend fun getCurrencies(): Either<ApiException, GetCurrenciesResponse> =
httpClient.sendRequest<Unit, GetCurrenciesResponse>(
type = RequestType.GET,
url = "$baseUrl/currency"
url = "$baseUrl/currency",
jsonSerializer = jsonSerializer
)

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.github.ai.simplesplit.android.data.api

import arrow.core.Either
import arrow.core.computations.ResultEffect.bind
import arrow.core.raise.either
import com.github.ai.simplesplit.android.data.json.JsonSerializer
import com.github.ai.simplesplit.android.model.exception.ApiException
import com.github.ai.simplesplit.android.model.exception.InvalidResponseException
import com.github.ai.simplesplit.android.model.exception.NetworkException
Expand All @@ -13,6 +15,7 @@ import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.put
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
Expand All @@ -28,7 +31,8 @@ enum class RequestType {
suspend inline fun <reified Request, reified Response> HttpClient.sendRequest(
type: RequestType,
url: String,
body: Request? = null
body: Request? = null,
jsonSerializer: JsonSerializer
): Either<ApiException, Response> {
val client = this

Expand All @@ -40,14 +44,14 @@ suspend inline fun <reified Request, reified Response> HttpClient.sendRequest(
RequestType.POST -> client.post(url) {
contentType(ContentType.Application.Json)
if (body != null) {
setBody(body)
setBody(jsonSerializer.serialize(body))
}
}

RequestType.PUT -> client.put(url) {
contentType(ContentType.Application.Json)
if (body != null) {
setBody(body)
setBody(jsonSerializer.serialize(body))
}
}

Expand All @@ -59,21 +63,25 @@ suspend inline fun <reified Request, reified Response> HttpClient.sendRequest(

val status = response.status
if (status != HttpStatusCode.OK) {
// TODO: Kotlin serializer cannot parse quoted json object from server
val errorBody = Either
.catch { response.body<ErrorMessageDto>() }
val errorText = response.bodyAsText()
val error = jsonSerializer
.deserialize<ErrorMessageDto>(errorText)
.getOrNull()

raise(
InvalidResponseException(
statusCode = status.value,
errorMessage = errorBody
errorMessage = error
)
)
}

Either.catch { response.body<Response>() }
jsonSerializer.deserialize<Response>(response.bodyAsText())
.mapLeft { error -> ApiException(cause = error) }
.bind()

// Either.catch { response.body<Response>() }
// .mapLeft { error -> ApiException(cause = error) }
// .bind()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.engine.okhttp.OkHttpConfig
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.serialization.kotlinx.json.json
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
Expand All @@ -26,9 +24,9 @@ object HttpClientFactory {
logLevel: LogLevel
): HttpClient {
return HttpClient(OkHttp) {
install(ContentNegotiation) {
json(jsonSerializer.json)
}
// install(ContentNegotiation) {
// json(jsonSerializer.json)
// }
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.github.ai.simplesplit.android.data.api.coverters

import com.github.ai.simplesplit.android.data.database.model.CurrencyEntity
import com.github.ai.simplesplit.android.model.Expense
import com.github.ai.simplesplit.android.model.Group
import com.github.ai.simplesplit.android.model.Member
import com.github.ai.simplesplit.android.model.Transaction
import com.github.ai.split.api.CurrencyDto
import com.github.ai.split.api.ExpenseDto
import com.github.ai.split.api.GroupDto
import com.github.ai.split.api.MemberDto
import com.github.ai.split.api.TransactionDto

fun List<CurrencyDto>.toCurrencies(): List<CurrencyEntity> {
return this.map { dto -> dto.toCurrency() }
Expand All @@ -12,4 +20,43 @@ fun CurrencyDto.toCurrency(): CurrencyEntity =
isoCode = isoCode,
name = name,
symbol = symbol
)

fun MemberDto.toMember(): Member =
Member(
uid = uid,
name = name
)

fun TransactionDto.toTransaction(): Transaction =
Transaction(
creditorUid = creditorUid,
debtorUid = debtorUid,
amount = amount
)

fun ExpenseDto.toExpense(): Expense =
Expense(
uid = uid,
title = title,
description = description,
amount = amount,
currency = currency.toCurrency(),
paidBy = paidBy.map { it.toMember() },
splitBetween = splitBetween.map { it.toMember() },
createdSeconds = created.timestampSeconds,
modifiedSeconds = modified.timestampSeconds
)

fun GroupDto.toGroup(): Group =
Group(
uid = uid,
title = title,
description = description,
currency = currency.toCurrency(),
members = members.map { it.toMember() },
expenses = expenses.map { it.toExpense() },
paybackTransactions = paybackTransactions.map { it.toTransaction() },
createdSeconds = created.timestampSeconds,
modifiedSeconds = modified.timestampSeconds
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.github.ai.simplesplit.android.data.database.model

import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable

@Serializable
@Entity("currencies")
data class CurrencyEntity(
@PrimaryKey
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.github.ai.simplesplit.android.data.json

import arrow.core.Either
import arrow.core.raise.either
import com.github.ai.simplesplit.android.model.exception.ParsingException
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class ArgumentSerializer {

@OptIn(ExperimentalSerializationApi::class)
val json = Json {
explicitNulls = false
ignoreUnknownKeys = true
}

inline fun <reified T> deserialize(text: String): Either<ParsingException, T> =
either {
try {
json.decodeFromString(text)
} catch (exception: SerializationException) {
raise(ParsingException(cause = exception))
}
}

inline fun <reified T> serialize(data: T): String = json.encodeToString(data)
}
Loading