diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a62b37a..171895a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 @@ -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 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 1799e84..df89be3 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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) @@ -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) diff --git a/android/app/libs/simple-split-api.jar b/android/app/libs/simple-split-api.jar new file mode 100644 index 0000000..4967980 Binary files /dev/null and b/android/app/libs/simple-split-api.jar differ diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt index 4ec6fc3..4e9bdcf 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt @@ -61,7 +61,8 @@ class ApiClient( return httpClient.sendRequest( type = RequestType.GET, - url = "$baseUrl/group?ids=$idsStr&passwords=$passwordsStr" + url = "$baseUrl/group?ids=$idsStr&passwords=$passwordsStr", + jsonSerializer = jsonSerializer ) } @@ -69,7 +70,8 @@ class ApiClient( httpClient.sendRequest( type = RequestType.POST, url = "$baseUrl/group", - body = request + body = request, + jsonSerializer = jsonSerializer ) suspend fun postExpense( @@ -79,7 +81,8 @@ class ApiClient( httpClient.sendRequest( type = RequestType.POST, url = "$baseUrl/expense?password=$password", - body = request + body = request, + jsonSerializer = jsonSerializer ) suspend fun putExpense( @@ -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( @@ -99,7 +103,8 @@ class ApiClient( ): Either = httpClient.sendRequest( type = RequestType.DELETE, - url = "$baseUrl/expense/$expenseUid?password=$password" + url = "$baseUrl/expense/$expenseUid?password=$password", + jsonSerializer = jsonSerializer ) suspend fun putGroup( @@ -110,7 +115,8 @@ class ApiClient( httpClient.sendRequest( type = RequestType.PUT, url = "$baseUrl/group/$uid?password=$password", - body = request + body = request, + jsonSerializer = jsonSerializer ) suspend fun postMember( @@ -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( @@ -129,7 +136,8 @@ class ApiClient( ): Either = httpClient.sendRequest( type = RequestType.DELETE, - url = "$baseUrl/member/$memberUid?password=$password" + url = "$baseUrl/member/$memberUid?password=$password", + jsonSerializer = jsonSerializer ) suspend fun putMember( @@ -140,13 +148,15 @@ class ApiClient( httpClient.sendRequest( type = RequestType.PUT, url = "$baseUrl/member/$memberUid?password=$password", - body = request + body = request, + jsonSerializer = jsonSerializer ) suspend fun getCurrencies(): Either = httpClient.sendRequest( type = RequestType.GET, - url = "$baseUrl/currency" + url = "$baseUrl/currency", + jsonSerializer = jsonSerializer ) companion object { diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/HttpClientExtensions.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/HttpClientExtensions.kt index 15b4605..d6dfdc9 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/HttpClientExtensions.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/HttpClientExtensions.kt @@ -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 @@ -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 @@ -28,7 +31,8 @@ enum class RequestType { suspend inline fun HttpClient.sendRequest( type: RequestType, url: String, - body: Request? = null + body: Request? = null, + jsonSerializer: JsonSerializer ): Either { val client = this @@ -40,14 +44,14 @@ suspend inline fun 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)) } } @@ -59,21 +63,25 @@ suspend inline fun 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() } + val errorText = response.bodyAsText() + val error = jsonSerializer + .deserialize(errorText) .getOrNull() raise( InvalidResponseException( statusCode = status.value, - errorMessage = errorBody + errorMessage = error ) ) } - Either.catch { response.body() } + jsonSerializer.deserialize(response.bodyAsText()) .mapLeft { error -> ApiException(cause = error) } .bind() + + // Either.catch { response.body() } + // .mapLeft { error -> ApiException(cause = error) } + // .bind() } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/HttpClientFactory.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/HttpClientFactory.kt index 708ee26..4c52fbf 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/HttpClientFactory.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/HttpClientFactory.kt @@ -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 @@ -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) { diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/coverters/ApiDataConverters.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/coverters/ApiDataConverters.kt index f7db42e..2408a3e 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/coverters/ApiDataConverters.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/coverters/ApiDataConverters.kt @@ -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.toCurrencies(): List { return this.map { dto -> dto.toCurrency() } @@ -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 ) \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/database/model/CurrencyEntity.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/database/model/CurrencyEntity.kt index db12ea7..4534a18 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/data/database/model/CurrencyEntity.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/database/model/CurrencyEntity.kt @@ -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 diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/json/ArgumentSerializer.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/json/ArgumentSerializer.kt new file mode 100644 index 0000000..69b3469 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/json/ArgumentSerializer.kt @@ -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 deserialize(text: String): Either = + either { + try { + json.decodeFromString(text) + } catch (exception: SerializationException) { + raise(ParsingException(cause = exception)) + } + } + + inline fun serialize(data: T): String = json.encodeToString(data) +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/json/JsonSerializer.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/json/JsonSerializer.kt index f92f8fe..80b417e 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/data/json/JsonSerializer.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/json/JsonSerializer.kt @@ -3,27 +3,25 @@ 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 +import com.google.gson.GsonBuilder +import com.google.gson.JsonSyntaxException class JsonSerializer { - @OptIn(ExperimentalSerializationApi::class) - val json = Json { - explicitNulls = false - ignoreUnknownKeys = true - } + val gson = GsonBuilder() + .setPrettyPrinting() + .create() inline fun deserialize(text: String): Either = either { try { - json.decodeFromString(text) - } catch (exception: SerializationException) { + gson.fromJson(text, T::class.java) + } catch (exception: JsonSyntaxException) { raise(ParsingException(cause = exception)) } } - inline fun serialize(data: T): String = json.encodeToString(data) + inline fun serialize(data: T): String { + return gson.toJson(data) + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt index 56b9a2b..95ea358 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt @@ -2,6 +2,7 @@ package com.github.ai.simplesplit.android.di import com.github.ai.simplesplit.android.data.api.ApiClient import com.github.ai.simplesplit.android.data.database.AppDatabase +import com.github.ai.simplesplit.android.data.json.ArgumentSerializer import com.github.ai.simplesplit.android.data.json.JsonSerializer import com.github.ai.simplesplit.android.data.repository.CurrencyRepository import com.github.ai.simplesplit.android.data.repository.ExpenseRepository @@ -62,6 +63,7 @@ object AndroidAppModule { singleOf(::ResourceProviderImpl).bind(ResourceProvider::class) singleOf(::SettingsImpl).bind(Settings::class) singleOf(::TimestampFormatter) + singleOf(::ArgumentSerializer) // Database single { AppDatabase.buildDatabase(get()) } diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/model/Expense.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/model/Expense.kt new file mode 100644 index 0000000..c6f5f7f --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/model/Expense.kt @@ -0,0 +1,17 @@ +package com.github.ai.simplesplit.android.model + +import com.github.ai.simplesplit.android.data.database.model.CurrencyEntity +import kotlinx.serialization.Serializable + +@Serializable +data class Expense( + val uid: String, + val title: String, + val description: String, + val amount: Double, + val currency: CurrencyEntity, + val paidBy: List, + val splitBetween: List, + val createdSeconds: Long, + val modifiedSeconds: Long +) \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/model/Group.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/model/Group.kt index 45aa882..6e4fec2 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/model/Group.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/model/Group.kt @@ -1,7 +1,17 @@ package com.github.ai.simplesplit.android.model +import com.github.ai.simplesplit.android.data.database.model.CurrencyEntity +import kotlinx.serialization.Serializable + +@Serializable data class Group( val uid: String, val title: String, - val description: String + val description: String, + val currency: CurrencyEntity, + val members: List, + val expenses: List, + val paybackTransactions: List, + val createdSeconds: Long, + val modifiedSeconds: Long ) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/MemberDto.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/model/Member.kt similarity index 60% rename from android/backend-api/src/main/kotlin/com/github/ai/split/api/MemberDto.kt rename to android/app/src/main/java/com/github/ai/simplesplit/android/model/Member.kt index a2d5273..1f54386 100644 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/MemberDto.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/model/Member.kt @@ -1,9 +1,9 @@ -package com.github.ai.split.api +package com.github.ai.simplesplit.android.model import kotlinx.serialization.Serializable @Serializable -data class MemberDto( +data class Member( val uid: String, val name: String ) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/TransactionDto.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/model/Transaction.kt similarity index 65% rename from android/backend-api/src/main/kotlin/com/github/ai/split/api/TransactionDto.kt rename to android/app/src/main/java/com/github/ai/simplesplit/android/model/Transaction.kt index 737b71f..ac1e5da 100644 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/TransactionDto.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/model/Transaction.kt @@ -1,9 +1,9 @@ -package com.github.ai.split.api +package com.github.ai.simplesplit.android.model import kotlinx.serialization.Serializable @Serializable -data class TransactionDto( +data class Transaction( val creditorUid: String, val debtorUid: String, val amount: Double diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogCellFactory.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogCellFactory.kt index 841b842..484c73a 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogCellFactory.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogCellFactory.kt @@ -2,8 +2,8 @@ package com.github.ai.simplesplit.android.presentation.dialogs.expenseDetails import androidx.compose.ui.unit.dp import com.github.ai.simplesplit.android.R -import com.github.ai.simplesplit.android.data.api.coverters.toCurrency import com.github.ai.simplesplit.android.domain.TimestampFormatter +import com.github.ai.simplesplit.android.model.Expense import com.github.ai.simplesplit.android.presentation.core.ResourceProvider import com.github.ai.simplesplit.android.presentation.core.compose.TextColor import com.github.ai.simplesplit.android.presentation.core.compose.TextSize @@ -27,7 +27,6 @@ import com.github.ai.simplesplit.android.presentation.core.compose.theme.OneLine import com.github.ai.simplesplit.android.presentation.core.compose.theme.ThemeProvider import com.github.ai.simplesplit.android.presentation.core.compose.theme.TinyMargin import com.github.ai.simplesplit.android.utils.formatAsMoney -import com.github.ai.split.api.ExpenseDto class ExpenseDetailsDialogCellFactory( private val themeProvider: ThemeProvider, @@ -36,7 +35,7 @@ class ExpenseDetailsDialogCellFactory( ) { fun createCells( - expense: ExpenseDto, + expense: Expense, eventProvider: CellEventProvider ): List { val cells = mutableListOf() @@ -50,7 +49,7 @@ class ExpenseDetailsDialogCellFactory( } private fun createTitleSection( - expense: ExpenseDto, + expense: Expense, eventProvider: CellEventProvider ): List { val cells = mutableListOf() @@ -64,7 +63,7 @@ class ExpenseDetailsDialogCellFactory( ) ) - val currency = expense.currency.toCurrency() + val currency = expense.currency // Title cells.add( @@ -91,8 +90,8 @@ class ExpenseDetailsDialogCellFactory( ) ) - val created = expense.created.timestampSeconds * 1000L - val modified = expense.modified.timestampSeconds * 1000L + val created = expense.createdSeconds * 1000L + val modified = expense.modifiedSeconds * 1000L cells.add( TextCellViewModel( @@ -144,7 +143,7 @@ class ExpenseDetailsDialogCellFactory( return cells } - private fun createPayerSection(expense: ExpenseDto): List { + private fun createPayerSection(expense: Expense): List { val cells = mutableListOf() cells.add( @@ -156,14 +155,14 @@ class ExpenseDetailsDialogCellFactory( ) ) - val currency = expense.currency.toCurrency() - for ((index, payer) in expense.paidBy.withIndex()) { cells.add( TextCellViewModel( TextCellModel( id = "payer_$index", - text = payer.name + " paid " + expense.amount.formatAsMoney(currency), + text = payer.name + " paid " + expense.amount.formatAsMoney( + expense.currency + ), textSize = TextSize.BODY_LARGE, textColor = TextColor.PRIMARY ) @@ -174,7 +173,7 @@ class ExpenseDetailsDialogCellFactory( return cells } - private fun createSplitSection(expense: ExpenseDto): List { + private fun createSplitSection(expense: Expense): List { val cells = mutableListOf() cells.add( @@ -186,7 +185,7 @@ class ExpenseDetailsDialogCellFactory( ) ) - val currency = expense.currency.toCurrency() + val currency = expense.currency val payerUid = expense.paidBy.firstOrNull()?.uid.orEmpty() val debtors = expense.splitBetween.filter { splitMember -> splitMember.uid != payerUid } diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/model/ExpenseDetailsDialogArgs.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/model/ExpenseDetailsDialogArgs.kt index 3e82903..a95d9f9 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/model/ExpenseDetailsDialogArgs.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/model/ExpenseDetailsDialogArgs.kt @@ -1,9 +1,9 @@ package com.github.ai.simplesplit.android.presentation.dialogs.expenseDetails.model -import com.github.ai.split.api.ExpenseDto +import com.github.ai.simplesplit.android.model.Expense import kotlinx.serialization.Serializable @Serializable data class ExpenseDetailsDialogArgs( - val expense: ExpenseDto + val expense: Expense ) \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/root/BottomSheetRootDialog.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/root/BottomSheetRootDialog.kt index 31b7dc0..852e1f7 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/root/BottomSheetRootDialog.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/root/BottomSheetRootDialog.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import arrow.core.getOrElse import com.github.ai.simplesplit.android.R -import com.github.ai.simplesplit.android.data.json.JsonSerializer +import com.github.ai.simplesplit.android.data.json.ArgumentSerializer import com.github.ai.simplesplit.android.databinding.ComposeViewBinding import com.github.ai.simplesplit.android.di.GlobalInjector.inject import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme @@ -22,9 +22,9 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment class BottomSheetRootDialog : BottomSheetDialogFragment() { private val themeProvider: ThemeProvider by inject() + private val serializer: ArgumentSerializer by inject() private val dialogArgs: Dialog by lazy { - val serializer = JsonSerializer() val args = arguments?.getString(ARGS) serializer.deserialize(args ?: "").getOrElse { throw IllegalArgumentException("Dialog args not found") @@ -67,18 +67,17 @@ class BottomSheetRootDialog : BottomSheetDialogFragment() { private const val ARGS = "args" fun newInstance(dialog: Dialog): BottomSheetDialogFragment { - val serialized = JsonSerializer() + val fragment = BottomSheetRootDialog() val args = Bundle() .apply { // TODO: should be optimized - putString(ARGS, serialized.serialize(dialog)) + putString(ARGS, fragment.serializer.serialize(dialog)) } - return BottomSheetRootDialog() - .apply { - arguments = args - } + fragment.arguments = args + + return fragment } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/ExpenseEditorInteractor.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/ExpenseEditorInteractor.kt index 81f6e54..63c5303 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/ExpenseEditorInteractor.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/ExpenseEditorInteractor.kt @@ -25,12 +25,12 @@ class ExpenseEditorInteractor( ): Either = either { val request = PutExpenseRequest( - title = title, - description = null, - amount = amount, - paidBy = if (payerUid != null) listOf(UserUidDto(uid = payerUid)) else null, - isSplitBetweenAll = true, - splitBetween = null + title, + null, + amount, + if (payerUid != null) listOf(UserUidDto(payerUid)) else emptyList(), + true, + null ) expenseRepository.updateExpense( @@ -51,13 +51,13 @@ class ExpenseEditorInteractor( ?: raise(AppException(message = "Failed to load credentials for group")) val request = PostExpenseRequest( - groupUid = groupUid, - title = title, - amount = amount, - description = "", - paidBy = listOf(UserUidDto(uid = payerUid)), - isSplitBetweenAll = true, - splitBetween = null + groupUid, + title, + "", + amount, + listOf(UserUidDto(payerUid)), + true, + null ) expenseRepository.createExpense( diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/ExpenseEditorViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/ExpenseEditorViewModel.kt index be305a6..44048bc 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/ExpenseEditorViewModel.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/ExpenseEditorViewModel.kt @@ -1,7 +1,6 @@ package com.github.ai.simplesplit.android.presentation.screens.expenseEditor import com.github.ai.simplesplit.android.R -import com.github.ai.simplesplit.android.data.api.coverters.toCurrency import com.github.ai.simplesplit.android.presentation.core.ResourceProvider import com.github.ai.simplesplit.android.presentation.core.compose.navigation.Router import com.github.ai.simplesplit.android.presentation.core.mvi.MviViewModel @@ -49,7 +48,7 @@ class ExpenseEditorViewModel( emit(ExpenseEditorState.Loading) val memberNames = args.group.members.map { member -> member.name } - val currency = args.group.currency.toCurrency() + val currency = args.group.currency when (args.mode) { ExpenseEditorMode.NewExpense -> { diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/model/ExpenseEditorArgs.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/model/ExpenseEditorArgs.kt index 83cca3c..1ee6b87 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/model/ExpenseEditorArgs.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/expenseEditor/model/ExpenseEditorArgs.kt @@ -1,12 +1,12 @@ package com.github.ai.simplesplit.android.presentation.screens.expenseEditor.model import com.github.ai.simplesplit.android.data.database.model.GroupCredentials -import com.github.ai.split.api.GroupDto +import com.github.ai.simplesplit.android.model.Group import kotlinx.serialization.Serializable @Serializable data class ExpenseEditorArgs( val mode: ExpenseEditorMode, - val group: GroupDto, + val group: Group, val credentials: GroupCredentials ) \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsViewModel.kt index cef5f4f..8d58320 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsViewModel.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsViewModel.kt @@ -2,6 +2,7 @@ package com.github.ai.simplesplit.android.presentation.screens.groupDetails import androidx.annotation.StringRes import com.github.ai.simplesplit.android.R +import com.github.ai.simplesplit.android.data.api.coverters.toGroup import com.github.ai.simplesplit.android.data.database.model.GroupCredentials import com.github.ai.simplesplit.android.presentation.core.ResourceProvider import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellEvent @@ -187,9 +188,9 @@ class GroupDetailsViewModel( ifLeft = { error -> emit(GroupDetailsState.Error(error.toErrorMessage(resources))) }, - ifRight = { group -> - data = group - emit(createScreenState(group)) + ifRight = { groupDto -> + data = groupDto.toGroup() + emit(createScreenState(groupDto)) } ) }.flowOn(Dispatchers.IO) @@ -215,7 +216,7 @@ class GroupDetailsViewModel( emit(newState) }, ifRight = { group -> - data = group + data = group.toGroup() emit(createScreenState(group)) } ) diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsArgs.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsArgs.kt index f42a9b6..4279eca 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsArgs.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsArgs.kt @@ -1,10 +1,10 @@ package com.github.ai.simplesplit.android.presentation.screens.groupDetails.model -import com.github.ai.split.api.GroupDto +import com.github.ai.simplesplit.android.model.Group import kotlinx.serialization.Serializable @Serializable data class GroupDetailsArgs( - val group: GroupDto, + val group: Group, val password: String ) \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupEditor/GroupEditorInteractor.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupEditor/GroupEditorInteractor.kt index 8866c80..2f3c1c7 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupEditor/GroupEditorInteractor.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupEditor/GroupEditorInteractor.kt @@ -57,8 +57,8 @@ class GroupEditorInteractor( memberRepository.createMember( password = credentials.password, request = PostMemberRequest( - groupUid = credentials.groupUid, - name = memberName + credentials.groupUid, + memberName ) ).bind() } @@ -75,7 +75,7 @@ class GroupEditorInteractor( memberUid = memberUid, password = credentials.password, request = PutMemberRequest( - name = memberName + memberName ) ).bind() } @@ -87,11 +87,11 @@ class GroupEditorInteractor( password = credentials.password, uid = credentials.groupUid, request = PutGroupRequest( - title = newTitle, - password = newPassword, - description = null, - currencyIsoCode = newCurrencyIsoCode, - members = null + newTitle, + newPassword, + null, + newCurrencyIsoCode, + null ) ).bind().group } else { @@ -112,12 +112,12 @@ class GroupEditorInteractor( ): Either = either { val request = PostGroupRequest( - password = password, - title = title, - description = null, - currencyIsoCode = currencyIsoCode, - members = members.map { UserNameDto(name = it) }, - expenses = null + password, + title, + null, + currencyIsoCode, + members.map { UserNameDto(it) }, + null ) val response = groupRepository.createGroup(request).bind() diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt index 9724daf..4d52fc7 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt @@ -3,6 +3,7 @@ package com.github.ai.simplesplit.android.presentation.screens.groups import androidx.annotation.StringRes import androidx.lifecycle.viewModelScope import com.github.ai.simplesplit.android.R +import com.github.ai.simplesplit.android.data.api.coverters.toGroup import com.github.ai.simplesplit.android.data.database.model.GroupCredentials import com.github.ai.simplesplit.android.model.ErrorMessage import com.github.ai.simplesplit.android.presentation.core.ResourceProvider @@ -189,7 +190,7 @@ class GroupsViewModel( router.navigateTo( Screen.GroupDetails( GroupDetailsArgs( - group = groupAndCreds.first, + group = groupAndCreds.first.toGroup(), password = groupAndCreds.second.password ) ) diff --git a/android/backend-api/build.gradle.kts b/android/backend-api/build.gradle.kts deleted file mode 100644 index 25400b0..0000000 --- a/android/backend-api/build.gradle.kts +++ /dev/null @@ -1,37 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlinSerialization) - id("java-library") -} - -val appGroupId = "com.github.ai.split" - -group = appGroupId -version = libs.versions.appVersion - -tasks.withType { - kotlinOptions { - apiVersion = "1.9" - languageVersion = "1.9" - jvmTarget = JavaVersion.VERSION_17.toString() - } -} - -java { - withSourcesJar() - withJavadocJar() - - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -dependencies { - testImplementation(libs.junit.engine) - testImplementation(libs.kotest.runner) - testImplementation(libs.kotest.assertions) - testImplementation(libs.mockk) - - implementation(libs.kotlinx.json) -} \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/CurrencyDto.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/CurrencyDto.kt deleted file mode 100644 index 1f924c0..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/CurrencyDto.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.ai.split.api - -import kotlinx.serialization.Serializable - -@Serializable -data class CurrencyDto( - val isoCode: String, - val name: String, - val symbol: String -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/ErrorMessageDto.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/ErrorMessageDto.kt deleted file mode 100644 index bd69993..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/ErrorMessageDto.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.ai.split.api - -import kotlinx.serialization.Serializable - -@Serializable -data class ErrorMessageDto( - val message: String?, - val exception: String, - val stacktraceBase64: String, - val stacktraceLines: List -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/ExpenseDto.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/ExpenseDto.kt deleted file mode 100644 index ededd6e..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/ExpenseDto.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.ai.split.api - -import kotlinx.serialization.Serializable - -@Serializable -data class ExpenseDto( - val uid: String, - val title: String, - val description: String?, - val amount: Double, - val currency: CurrencyDto, - val paidBy: List, - val splitBetween: List, - val created: TimestampDto, - val modified: TimestampDto -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/GetGroupErrorDto.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/GetGroupErrorDto.kt deleted file mode 100644 index b084fee..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/GetGroupErrorDto.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api - -import kotlinx.serialization.Serializable - -@Serializable -data class GetGroupErrorDto( - val uid: String, - val message: String -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/GroupDto.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/GroupDto.kt deleted file mode 100644 index b684a50..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/GroupDto.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.ai.split.api - -import kotlinx.serialization.Serializable - -@Serializable -data class GroupDto( - val uid: String, - val title: String, - val description: String, - val currency: CurrencyDto, - val members: List, - val expenses: List, - val paybackTransactions: List, - val created: TimestampDto, - val modified: TimestampDto -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/NewExpenseDto.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/NewExpenseDto.kt deleted file mode 100644 index 9811fb7..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/NewExpenseDto.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.ai.split.api - -import kotlinx.serialization.Serializable - -@Serializable -data class NewExpenseDto( - val title: String, - val description: String?, - val amount: Double, - val paidBy: List, - val isSplitBetweenAll: Boolean?, - val splitBetween: List? -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/TimestampDto.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/TimestampDto.kt deleted file mode 100644 index 39a7ad0..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/TimestampDto.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api - -import kotlinx.serialization.Serializable - -@Serializable -data class TimestampDto( - val timestampSeconds: Long, - val formatted: String -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/UserNameDto.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/UserNameDto.kt deleted file mode 100644 index 1e8e963..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/UserNameDto.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.ai.split.api - -import kotlinx.serialization.Serializable - -@Serializable -data class UserNameDto( - val name: String -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/UserUidDto.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/UserUidDto.kt deleted file mode 100644 index 793122e..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/UserUidDto.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.ai.split.api - -import kotlinx.serialization.Serializable - -@Serializable -data class UserUidDto( - val uid: String -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PostExpenseRequest.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PostExpenseRequest.kt deleted file mode 100644 index 3e21c6c..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PostExpenseRequest.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.ai.split.api.request - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.UserUidDto - -@Serializable -data class PostExpenseRequest( - val groupUid: String, - val title: String, - val description: String?, - val amount: Double, - val paidBy: List, - val isSplitBetweenAll: Boolean?, - val splitBetween: List? -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PostGroupRequest.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PostGroupRequest.kt deleted file mode 100644 index 78611ef..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PostGroupRequest.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.ai.split.api.request - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.NewExpenseDto -import com.github.ai.split.api.UserNameDto - -@Serializable -data class PostGroupRequest( - val password: String, - val title: String, - val description: String?, - val currencyIsoCode: String, - val members: List?, - val expenses: List? -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PostMemberRequest.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PostMemberRequest.kt deleted file mode 100644 index dbe91a8..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PostMemberRequest.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api.request - -import kotlinx.serialization.Serializable - -@Serializable -data class PostMemberRequest( - val groupUid: String, - val name: String -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PutExpenseRequest.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PutExpenseRequest.kt deleted file mode 100644 index 0b8f451..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PutExpenseRequest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.ai.split.api.request - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.UserUidDto - -@Serializable -data class PutExpenseRequest( - val title: String?, - val description: String?, - val amount: Double?, - val paidBy: List?, - val isSplitBetweenAll: Boolean?, - val splitBetween: List? -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PutGroupRequest.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PutGroupRequest.kt deleted file mode 100644 index f822ec8..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PutGroupRequest.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.ai.split.api.request - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.UserUidDto - -@Serializable -data class PutGroupRequest( - val title: String?, - val password: String?, - val description: String?, - val currencyIsoCode: String?, - val members: List? -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PutMemberRequest.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PutMemberRequest.kt deleted file mode 100644 index a1e4f5b..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/request/PutMemberRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.ai.split.api.request - -import kotlinx.serialization.Serializable - -@Serializable -data class PutMemberRequest( - val name: String -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/DeleteExpenseResponse.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/DeleteExpenseResponse.kt deleted file mode 100644 index d9bf4df..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/DeleteExpenseResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api.response - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.GroupDto - -@Serializable -data class DeleteExpenseResponse( - val group: GroupDto -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/DeleteMemberResponse.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/DeleteMemberResponse.kt deleted file mode 100644 index 27aeba0..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/DeleteMemberResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api.response - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.GroupDto - -@Serializable -data class DeleteMemberResponse( - val group: GroupDto -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/GetCurrenciesResponse.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/GetCurrenciesResponse.kt deleted file mode 100644 index 646bc03..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/GetCurrenciesResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api.response - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.CurrencyDto - -@Serializable -data class GetCurrenciesResponse( - val currencies: List -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/GetGroupsResponse.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/GetGroupsResponse.kt deleted file mode 100644 index 3ec3195..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/GetGroupsResponse.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.ai.split.api.response - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.GetGroupErrorDto -import com.github.ai.split.api.GroupDto - -@Serializable -data class GetGroupsResponse( - val groups: List, - val errors: List -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PostExpenseResponse.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PostExpenseResponse.kt deleted file mode 100644 index 420472a..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PostExpenseResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api.response - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.ExpenseDto - -@Serializable -data class PostExpenseResponse( - val expense: ExpenseDto -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PostGroupResponse.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PostGroupResponse.kt deleted file mode 100644 index baaec87..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PostGroupResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api.response - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.GroupDto - -@Serializable -data class PostGroupResponse( - val group: GroupDto -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PostMemberResponse.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PostMemberResponse.kt deleted file mode 100644 index 1d5d1f9..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PostMemberResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api.response - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.GroupDto - -@Serializable -data class PostMemberResponse( - val group: GroupDto -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PutExpenseResponse.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PutExpenseResponse.kt deleted file mode 100644 index d8b5cf7..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PutExpenseResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api.response - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.ExpenseDto - -@Serializable -data class PutExpenseResponse( - val expense: ExpenseDto -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PutGroupResponse.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PutGroupResponse.kt deleted file mode 100644 index a4ed957..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PutGroupResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api.response - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.GroupDto - -@Serializable -data class PutGroupResponse( - val group: GroupDto -) \ No newline at end of file diff --git a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PutMemberResponse.kt b/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PutMemberResponse.kt deleted file mode 100644 index 44b6a7d..0000000 --- a/android/backend-api/src/main/kotlin/com/github/ai/split/api/response/PutMemberResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.api.response - -import kotlinx.serialization.Serializable -import com.github.ai.split.api.GroupDto - -@Serializable -data class PutMemberResponse( - val group: GroupDto -) \ No newline at end of file diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index cf03fb2..8ff3b63 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -41,6 +41,7 @@ truth = "1.4.4" jgit = "6.2.0.202206071550-r" ksprefs = "2.4.1" constraintLayoutCompose = "1.0.1" +gson = "2.13.1" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } @@ -119,4 +120,5 @@ dadb = { module = "dev.mobile:dadb", version.ref = "dadb" } mordant = { module = "com.github.ajalt.mordant:mordant", version.ref = "mordant" } truth = { module = "com.google.truth:truth", version.ref = "truth" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } -ksprefs = { module = "com.github.cioccarellia:ksprefs", version.ref = "ksprefs" } \ No newline at end of file +ksprefs = { module = "com.github.cioccarellia:ksprefs", version.ref = "ksprefs" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } \ No newline at end of file diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 22a8651..babc761 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -8,7 +8,4 @@ pluginManagement { rootProject.name = "simple-split-android" -include( - ":app", - ":backend-api" -) \ No newline at end of file +include(":app") \ No newline at end of file diff --git a/android/setup-api.sh b/android/setup-api.sh new file mode 100755 index 0000000..be8a857 --- /dev/null +++ b/android/setup-api.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +JAR_NAME="simple-split-api.jar" +TARGET_DIR="api/target" + +# Function to find the api jar file +find_jar() { + find "$TARGET_DIR" -name "$JAR_NAME" -type f 2>/dev/null | head -1 +} + +set -e + +cd ../backend +sbt "project api" clean compile package -warn + +JAR_PATH=$(find_jar) + +if [[ ! -e "$JAR_PATH" ]]; then + echo "Unable to find $JAR_NAME file" + exit 1 +fi + +# Copy JAR to Android libs +mkdir -p ../android/app/libs +cp "$JAR_PATH" "../android/app/libs/$JAR_NAME" + +echo "$JAR_NAME built and copied successfully" diff --git a/backend/api-client/src/main/scala/com/github/ai/split/client/ApiClient.scala b/backend/api-client/src/main/scala/com/github/ai/split/client/ApiClient.scala index a569b33..df98e39 100644 --- a/backend/api-client/src/main/scala/com/github/ai/split/client/ApiClient.scala +++ b/backend/api-client/src/main/scala/com/github/ai/split/client/ApiClient.scala @@ -2,16 +2,19 @@ package com.github.ai.split.client import com.github.ai.split.api.{NewExpenseDto, UserNameDto, UserUidDto} import com.github.ai.split.api.request.{PostExpenseRequest, PostGroupRequest, PostMemberRequest, PutMemberRequest} +import com.google.gson.GsonBuilder import zio.* -import zio.json.* import zio.http.* +import scala.jdk.CollectionConverters.* + class ApiClient( private val client: Client ) { type ApiResponse = ZIO[Scope, Throwable, Response] + private val gson = GsonBuilder().setPrettyPrinting().create() private val DefaultPassword = "abc123" private val baseUrl = "https://127.0.0.1:8443" @@ -35,49 +38,47 @@ class ApiClient( } def postGroup(): ApiResponse = { + val body = PostGroupRequest( + DefaultPassword, + "Oktoberfest", + "Amazing party", + "USD", + List("Bob", "Alan").map(UserNameDto(_)).asJava, + List( + NewExpenseDto( + "Traditional Beer & Pretzels", + "Authentic Bavarian beer and pretzels at Oktoberfest", + 45.50, + List(UserNameDto("Bob")).asJava, + true, + List.empty.asJava + ), + // Option 2: Entry tickets + NewExpenseDto( + "Oktoberfest Entry Tickets", + "Entry tickets for the beer festival", + 24.00, + List(UserNameDto("Alan")).asJava, + true, + List.empty.asJava + ), + + // Option 3: Traditional food + NewExpenseDto( + "Bratwurst and Sauerkraut", + "Traditional Bavarian sausages and sauerkraut", + 32.75, + List(UserNameDto("Bob")).asJava, + true, + List.empty.asJava + ) + ).asJava + ) + client.request( Request.post( path = s"$baseUrl/group", - body = Body.fromString( - PostGroupRequest( - password = DefaultPassword, - title = "Oktoberfest", - description = Some("Amazing party"), - currencyIsoCode = "USD", - members = Some(List("Bob", "Alan").map(UserNameDto(_))), - expenses = Some( - List( - NewExpenseDto( - title = "Traditional Beer & Pretzels", - description = Some("Authentic Bavarian beer and pretzels at Oktoberfest"), - amount = 45.50, - paidBy = List(UserNameDto("Bob")), - isSplitBetweenAll = Some(true), - splitBetween = None - ), - // Option 2: Entry tickets - NewExpenseDto( - title = "Oktoberfest Entry Tickets", - description = Some("Entry tickets for the beer festival"), - amount = 24.00, - paidBy = List(UserNameDto("Alan")), - isSplitBetweenAll = Some(true), - splitBetween = None - ), - - // Option 3: Traditional food - NewExpenseDto( - title = "Bratwurst and Sauerkraut", - description = Some("Traditional Bavarian sausages and sauerkraut"), - amount = 32.75, - paidBy = List(UserNameDto("Bob")), - isSplitBetweenAll = Some(true), - splitBetween = None - ) - ) - ) - ).toJsonPretty - ) + body = Body.fromString(gson.toJson(body)) ) ) } @@ -86,20 +87,20 @@ class ApiClient( password: String = DefaultPassword, title: String = "Beer" ): ApiResponse = { + val body = PostExpenseRequest( + Groups.TripToDisneyLand, + title, + "", + 18.0, + List(UserUidDto(Users.Mickey)).asJava, + true, + List.empty.asJava + ) + client.request( Request.post( path = s"$baseUrl/expense?password=$password", - body = Body.fromString( - PostExpenseRequest( - groupUid = Groups.TripToDisneyLand, - title = title, - description = None, - amount = 18.0, - paidBy = List(UserUidDto(Users.Mickey)), - isSplitBetweenAll = Some(true), - splitBetween = None - ).toJsonPretty - ) + body = Body.fromString(gson.toJson(body)) ) ) } @@ -109,15 +110,15 @@ class ApiClient( groupUid: String = Groups.TripToDisneyLand, userName: String = "Bob" ): ApiResponse = { + val body = PostMemberRequest( + groupUid, + userName + ) + client.request( Request.post( path = s"$baseUrl/member?password=$password", - body = Body.fromString( - PostMemberRequest( - groupUid = groupUid, - name = userName - ).toJsonPretty - ) + body = Body.fromString(gson.toJson(body)) ) ) } @@ -142,9 +143,9 @@ class ApiClient( Request.put( path = s"$baseUrl/member/$memberUid?password=$password", body = Body.fromString( - PutMemberRequest( - name = newName - ).toJsonPretty + gson.toJson( + PutMemberRequest(newName) + ) ) ) ) diff --git a/backend/api-client/src/main/scala/com/github/ai/split/client/Data.scala b/backend/api-client/src/main/scala/com/github/ai/split/client/Data.scala index 9a84ed8..fead110 100644 --- a/backend/api-client/src/main/scala/com/github/ai/split/client/Data.scala +++ b/backend/api-client/src/main/scala/com/github/ai/split/client/Data.scala @@ -1,6 +1,5 @@ package com.github.ai.split.client -import scala.util.Random import java.util.Date object Data { diff --git a/backend/api-client/src/main/scala/com/github/ai/split/client/utils/JsonUtils.scala b/backend/api-client/src/main/scala/com/github/ai/split/client/utils/JsonUtils.scala deleted file mode 100644 index 974f3a3..0000000 --- a/backend/api-client/src/main/scala/com/github/ai/split/client/utils/JsonUtils.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.ai.split.client.utils - -import zio.http.* -import zio.json.* -import zio.* -import zio.direct.* - -extension (response: Response) { - def parseBody[T](implicit decoder: JsonDecoder[T]): IO[Throwable, T] = { - defer { - val jsonBody = response.body.asString.run - - ZIO - .fromEither( - jsonBody.fromJson[T](using decoder).left.map(errorMessage => Exception(errorMessage)) - ) - .run - } - } -} diff --git a/backend/api/src/main/java/com/github/ai/split/api/CurrencyDto.java b/backend/api/src/main/java/com/github/ai/split/api/CurrencyDto.java new file mode 100644 index 0000000..a50db8c --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/CurrencyDto.java @@ -0,0 +1,16 @@ +package com.github.ai.split.api; + +public class CurrencyDto { + + public String isoCode = ""; + public String name = ""; + public String symbol = ""; + + public CurrencyDto() {} + + public CurrencyDto(String isoCode, String name, String symbol) { + this.isoCode = isoCode; + this.name = name; + this.symbol = symbol; + } +} diff --git a/backend/api/src/main/java/com/github/ai/split/api/ErrorMessageDto.java b/backend/api/src/main/java/com/github/ai/split/api/ErrorMessageDto.java new file mode 100644 index 0000000..e134fae --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/ErrorMessageDto.java @@ -0,0 +1,22 @@ +package com.github.ai.split.api; + +import java.util.Collections; +import java.util.List; + +public class ErrorMessageDto { + + public String message = null; + public String exception = ""; + public String stacktraceBase64 = ""; + public List stacktraceLines = Collections.emptyList(); + + public ErrorMessageDto() { + } + + public ErrorMessageDto(String message, String exception, String stacktraceBase64, List stacktraceLines) { + this.message = message; + this.exception = exception; + this.stacktraceBase64 = stacktraceBase64; + this.stacktraceLines = stacktraceLines; + } +} diff --git a/backend/api/src/main/java/com/github/ai/split/api/ExpenseDto.java b/backend/api/src/main/java/com/github/ai/split/api/ExpenseDto.java new file mode 100644 index 0000000..7f0b838 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/ExpenseDto.java @@ -0,0 +1,37 @@ +package com.github.ai.split.api; + +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +public class ExpenseDto { + + public String uid = ""; + public String title = ""; + public String description = ""; + public double amount = 0.0; + public CurrencyDto currency = new CurrencyDto(); + public List paidBy = Collections.emptyList(); + public List splitBetween = Collections.emptyList(); + public TimestampDto created = new TimestampDto(); + public TimestampDto modified = new TimestampDto(); + + public ExpenseDto() { + this.paidBy = new ArrayList<>(); + this.splitBetween = new ArrayList<>(); + } + + public ExpenseDto(String uid, String title, String description, double amount, + CurrencyDto currency, List paidBy, List splitBetween, + TimestampDto created, TimestampDto modified) { + this.uid = uid; + this.title = title; + this.description = description; + this.amount = amount; + this.currency = currency != null ? currency : new CurrencyDto(); + this.paidBy = paidBy != null ? paidBy : new ArrayList<>(); + this.splitBetween = splitBetween != null ? splitBetween : new ArrayList<>(); + this.created = created != null ? created : new TimestampDto(); + this.modified = modified != null ? modified : new TimestampDto(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/GetGroupErrorDto.java b/backend/api/src/main/java/com/github/ai/split/api/GetGroupErrorDto.java new file mode 100644 index 0000000..055e677 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/GetGroupErrorDto.java @@ -0,0 +1,14 @@ +package com.github.ai.split.api; + +public class GetGroupErrorDto { + + public String uid = ""; + public String message = ""; + + public GetGroupErrorDto() {} + + public GetGroupErrorDto(String uid, String message) { + this.uid = uid; + this.message = message; + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/GroupDto.java b/backend/api/src/main/java/com/github/ai/split/api/GroupDto.java new file mode 100644 index 0000000..32bad4d --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/GroupDto.java @@ -0,0 +1,38 @@ +package com.github.ai.split.api; + +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +public class GroupDto { + + public String uid = ""; + public String title = ""; + public String description = ""; + public CurrencyDto currency = new CurrencyDto(); + public List members = Collections.emptyList(); + public List expenses = Collections.emptyList(); + public List paybackTransactions = Collections.emptyList(); + public TimestampDto created = new TimestampDto(); + public TimestampDto modified = new TimestampDto(); + + public GroupDto() { + this.members = new ArrayList<>(); + this.expenses = new ArrayList<>(); + this.paybackTransactions = new ArrayList<>(); + } + + public GroupDto(String uid, String title, String description, CurrencyDto currency, + List members, List expenses, List paybackTransactions, + TimestampDto created, TimestampDto modified) { + this.uid = uid; + this.title = title; + this.description = description; + this.currency = currency != null ? currency : new CurrencyDto(); + this.members = members != null ? members : new ArrayList<>(); + this.expenses = expenses != null ? expenses : new ArrayList<>(); + this.paybackTransactions = paybackTransactions != null ? paybackTransactions : new ArrayList<>(); + this.created = created != null ? created : new TimestampDto(); + this.modified = modified != null ? modified : new TimestampDto(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/MemberDto.java b/backend/api/src/main/java/com/github/ai/split/api/MemberDto.java new file mode 100644 index 0000000..d1577e5 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/MemberDto.java @@ -0,0 +1,14 @@ +package com.github.ai.split.api; + +public class MemberDto { + + public String uid = ""; + public String name = ""; + + public MemberDto() {} + + public MemberDto(String uid, String name) { + this.uid = uid; + this.name = name; + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/NewExpenseDto.java b/backend/api/src/main/java/com/github/ai/split/api/NewExpenseDto.java new file mode 100644 index 0000000..8fbe159 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/NewExpenseDto.java @@ -0,0 +1,32 @@ +package com.github.ai.split.api; + +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +public class NewExpenseDto { + + public String title = ""; + public String description = ""; + public double amount = 0.0; + public List paidBy = Collections.emptyList(); + public Boolean isSplitBetweenAll = null; + public List splitBetween = Collections.emptyList(); + + public NewExpenseDto() { + this.paidBy = new ArrayList<>(); + this.splitBetween = new ArrayList<>(); + } + + public NewExpenseDto(String title, String description, double amount, + List paidBy, Boolean isSplitBetweenAll, + List splitBetween) { + this.title = title; + this.description = description; + this.amount = amount; + this.paidBy = paidBy != null ? paidBy : new ArrayList<>(); + this.isSplitBetweenAll = isSplitBetweenAll; + this.splitBetween = splitBetween != null ? splitBetween : new ArrayList<>(); + } +} + diff --git a/backend/api/src/main/java/com/github/ai/split/api/TimestampDto.java b/backend/api/src/main/java/com/github/ai/split/api/TimestampDto.java new file mode 100644 index 0000000..843e579 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/TimestampDto.java @@ -0,0 +1,14 @@ +package com.github.ai.split.api; + +public class TimestampDto { + + public long timestampSeconds = 0L; + public String formatted = ""; + + public TimestampDto() {} + + public TimestampDto(long timestampSeconds, String formatted) { + this.timestampSeconds = timestampSeconds; + this.formatted = formatted; + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/TransactionDto.java b/backend/api/src/main/java/com/github/ai/split/api/TransactionDto.java new file mode 100644 index 0000000..76542db --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/TransactionDto.java @@ -0,0 +1,16 @@ +package com.github.ai.split.api; + +public class TransactionDto { + + public String creditorUid = ""; + public String debtorUid = ""; + public double amount = 0.0; + + public TransactionDto() {} + + public TransactionDto(String creditorUid, String debtorUid, double amount) { + this.creditorUid = creditorUid; + this.debtorUid = debtorUid; + this.amount = amount; + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/UserNameDto.java b/backend/api/src/main/java/com/github/ai/split/api/UserNameDto.java new file mode 100644 index 0000000..b101523 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/UserNameDto.java @@ -0,0 +1,13 @@ +package com.github.ai.split.api; + +public class UserNameDto { + + public String name = ""; + + public UserNameDto() {} + + public UserNameDto(String name) { + this.name = name; + } +} + diff --git a/backend/api/src/main/java/com/github/ai/split/api/UserUidDto.java b/backend/api/src/main/java/com/github/ai/split/api/UserUidDto.java new file mode 100644 index 0000000..2678eb1 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/UserUidDto.java @@ -0,0 +1,12 @@ +package com.github.ai.split.api; + +public class UserUidDto { + + public String uid = ""; + + public UserUidDto() {} + + public UserUidDto(String uid) { + this.uid = uid; + } +} diff --git a/backend/api/src/main/java/com/github/ai/split/api/request/PostExpenseRequest.java b/backend/api/src/main/java/com/github/ai/split/api/request/PostExpenseRequest.java new file mode 100644 index 0000000..bdc0a7a --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/request/PostExpenseRequest.java @@ -0,0 +1,35 @@ +package com.github.ai.split.api.request; + +import com.github.ai.split.api.UserUidDto; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +public class PostExpenseRequest { + + public String groupUid = ""; + public String title = ""; + public String description = ""; + public double amount = 0.0; + public List paidBy = Collections.emptyList(); + public Boolean isSplitBetweenAll = null; + public List splitBetween = Collections.emptyList(); + + public PostExpenseRequest() { + this.paidBy = new ArrayList<>(); + this.splitBetween = new ArrayList<>(); + } + + public PostExpenseRequest(String groupUid, String title, String description, double amount, + List paidBy, Boolean isSplitBetweenAll, + List splitBetween) { + this.groupUid = groupUid; + this.title = title; + this.description = description; + this.amount = amount; + this.paidBy = paidBy != null ? paidBy : new ArrayList<>(); + this.isSplitBetweenAll = isSplitBetweenAll; + this.splitBetween = splitBetween != null ? splitBetween : new ArrayList<>(); + } +} + diff --git a/backend/api/src/main/java/com/github/ai/split/api/request/PostGroupRequest.java b/backend/api/src/main/java/com/github/ai/split/api/request/PostGroupRequest.java new file mode 100644 index 0000000..f359934 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/request/PostGroupRequest.java @@ -0,0 +1,33 @@ +package com.github.ai.split.api.request; + +import com.github.ai.split.api.NewExpenseDto; +import com.github.ai.split.api.UserNameDto; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +public class PostGroupRequest { + + public String password = ""; + public String title = ""; + public String description = ""; + public String currencyIsoCode = ""; + public List members = Collections.emptyList(); + public List expenses = Collections.emptyList(); + + public PostGroupRequest() { + this.members = new ArrayList<>(); + this.expenses = new ArrayList<>(); + } + + public PostGroupRequest(String password, String title, String description, String currencyIsoCode, + List members, List expenses) { + this.password = password; + this.title = title; + this.description = description; + this.currencyIsoCode = currencyIsoCode; + this.members = members != null ? members : new ArrayList<>(); + this.expenses = expenses != null ? expenses : new ArrayList<>(); + } +} + diff --git a/backend/api/src/main/java/com/github/ai/split/api/request/PostMemberRequest.java b/backend/api/src/main/java/com/github/ai/split/api/request/PostMemberRequest.java new file mode 100644 index 0000000..d00640f --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/request/PostMemberRequest.java @@ -0,0 +1,15 @@ +package com.github.ai.split.api.request; + +public class PostMemberRequest { + + public String groupUid = ""; + public String name = ""; + + public PostMemberRequest() { + } + + public PostMemberRequest(String groupUid, String name) { + this.groupUid = groupUid; + this.name = name; + } +} diff --git a/backend/api/src/main/java/com/github/ai/split/api/request/PutExpenseRequest.java b/backend/api/src/main/java/com/github/ai/split/api/request/PutExpenseRequest.java new file mode 100644 index 0000000..4826e3c --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/request/PutExpenseRequest.java @@ -0,0 +1,33 @@ +package com.github.ai.split.api.request; + +import com.github.ai.split.api.UserUidDto; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +public class PutExpenseRequest { + + public String title = ""; + public String description = ""; + public Double amount = null; + public List paidBy = Collections.emptyList(); + public Boolean isSplitBetweenAll = null; + public List splitBetween = Collections.emptyList(); + + public PutExpenseRequest() { + this.paidBy = new ArrayList<>(); + this.splitBetween = new ArrayList<>(); + } + + public PutExpenseRequest(String title, String description, Double amount, + List paidBy, Boolean isSplitBetweenAll, + List splitBetween) { + this.title = title; + this.description = description; + this.amount = amount; + this.paidBy = paidBy != null ? paidBy : new ArrayList<>(); + this.isSplitBetweenAll = isSplitBetweenAll; + this.splitBetween = splitBetween != null ? splitBetween : new ArrayList<>(); + } +} + diff --git a/backend/api/src/main/java/com/github/ai/split/api/request/PutGroupRequest.java b/backend/api/src/main/java/com/github/ai/split/api/request/PutGroupRequest.java new file mode 100644 index 0000000..179d3b5 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/request/PutGroupRequest.java @@ -0,0 +1,29 @@ +package com.github.ai.split.api.request; + +import com.github.ai.split.api.UserUidDto; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +public class PutGroupRequest { + + public String title = null; + public String password = null; + public String description = null; + public String currencyIsoCode = null; + public List members = Collections.emptyList(); + + public PutGroupRequest() { + this.members = new ArrayList<>(); + } + + public PutGroupRequest(String title, String password, String description, String currencyIsoCode, + List members) { + this.title = title; + this.password = password; + this.description = description; + this.currencyIsoCode = currencyIsoCode; + this.members = members != null ? members : new ArrayList<>(); + } +} + diff --git a/backend/api/src/main/java/com/github/ai/split/api/request/PutMemberRequest.java b/backend/api/src/main/java/com/github/ai/split/api/request/PutMemberRequest.java new file mode 100644 index 0000000..5d0306f --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/request/PutMemberRequest.java @@ -0,0 +1,12 @@ +package com.github.ai.split.api.request; + +public class PutMemberRequest { + + public String name = ""; + + public PutMemberRequest() {} + + public PutMemberRequest(String name) { + this.name = name; + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/response/DeleteExpenseResponse.java b/backend/api/src/main/java/com/github/ai/split/api/response/DeleteExpenseResponse.java new file mode 100644 index 0000000..1e5ed0d --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/response/DeleteExpenseResponse.java @@ -0,0 +1,13 @@ +package com.github.ai.split.api.response; + +import com.github.ai.split.api.GroupDto; +public class DeleteExpenseResponse { + + public GroupDto group = new GroupDto(); + + public DeleteExpenseResponse() {} + + public DeleteExpenseResponse(GroupDto group) { + this.group = group != null ? group : new GroupDto(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/response/DeleteMemberResponse.java b/backend/api/src/main/java/com/github/ai/split/api/response/DeleteMemberResponse.java new file mode 100644 index 0000000..7827268 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/response/DeleteMemberResponse.java @@ -0,0 +1,13 @@ +package com.github.ai.split.api.response; + +import com.github.ai.split.api.GroupDto; +public class DeleteMemberResponse { + + public GroupDto group = new GroupDto(); + + public DeleteMemberResponse() {} + + public DeleteMemberResponse(GroupDto group) { + this.group = group != null ? group : new GroupDto(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/response/GetCurrenciesResponse.java b/backend/api/src/main/java/com/github/ai/split/api/response/GetCurrenciesResponse.java new file mode 100644 index 0000000..a01c855 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/response/GetCurrenciesResponse.java @@ -0,0 +1,21 @@ +package com.github.ai.split.api.response; + +import com.github.ai.split.api.CurrencyDto; + +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + + +public class GetCurrenciesResponse { + + public List currencies = Collections.emptyList(); + + public GetCurrenciesResponse() { + this.currencies = new ArrayList<>(); + } + + public GetCurrenciesResponse(List currencies) { + this.currencies = currencies != null ? currencies : new ArrayList<>(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/response/GetGroupsResponse.java b/backend/api/src/main/java/com/github/ai/split/api/response/GetGroupsResponse.java new file mode 100644 index 0000000..4120c23 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/response/GetGroupsResponse.java @@ -0,0 +1,23 @@ +package com.github.ai.split.api.response; + +import com.github.ai.split.api.GroupDto; +import com.github.ai.split.api.GetGroupErrorDto; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +public class GetGroupsResponse { + + public List groups = Collections.emptyList(); + public List errors = Collections.emptyList(); + + public GetGroupsResponse() { + this.groups = new ArrayList<>(); + this.errors = new ArrayList<>(); + } + + public GetGroupsResponse(List groups, List errors) { + this.groups = groups != null ? groups : new ArrayList<>(); + this.errors = errors != null ? errors : new ArrayList<>(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/response/PostExpenseResponse.java b/backend/api/src/main/java/com/github/ai/split/api/response/PostExpenseResponse.java new file mode 100644 index 0000000..b3bb400 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/response/PostExpenseResponse.java @@ -0,0 +1,14 @@ +package com.github.ai.split.api.response; + +import com.github.ai.split.api.ExpenseDto; + +public class PostExpenseResponse { + + public ExpenseDto expense = new ExpenseDto(); + + public PostExpenseResponse() {} + + public PostExpenseResponse(ExpenseDto expense) { + this.expense = expense != null ? expense : new ExpenseDto(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/response/PostGroupResponse.java b/backend/api/src/main/java/com/github/ai/split/api/response/PostGroupResponse.java new file mode 100644 index 0000000..0dacfdd --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/response/PostGroupResponse.java @@ -0,0 +1,13 @@ +package com.github.ai.split.api.response; + +import com.github.ai.split.api.GroupDto; +public class PostGroupResponse { + + public GroupDto group = new GroupDto(); + + public PostGroupResponse() {} + + public PostGroupResponse(GroupDto group) { + this.group = group != null ? group : new GroupDto(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/response/PostMemberResponse.java b/backend/api/src/main/java/com/github/ai/split/api/response/PostMemberResponse.java new file mode 100644 index 0000000..f336bef --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/response/PostMemberResponse.java @@ -0,0 +1,14 @@ +package com.github.ai.split.api.response; + +import com.github.ai.split.api.GroupDto; +public class PostMemberResponse { + + public GroupDto group = new GroupDto(); + + public PostMemberResponse() {} + + public PostMemberResponse(GroupDto group) { + this.group = group != null ? group : new GroupDto(); + } +} + diff --git a/backend/api/src/main/java/com/github/ai/split/api/response/PutExpenseResponse.java b/backend/api/src/main/java/com/github/ai/split/api/response/PutExpenseResponse.java new file mode 100644 index 0000000..1113525 --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/response/PutExpenseResponse.java @@ -0,0 +1,13 @@ +package com.github.ai.split.api.response; + +import com.github.ai.split.api.ExpenseDto; +public class PutExpenseResponse { + + public ExpenseDto expense = new ExpenseDto(); + + public PutExpenseResponse() {} + + public PutExpenseResponse(ExpenseDto expense) { + this.expense = expense != null ? expense : new ExpenseDto(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/response/PutGroupResponse.java b/backend/api/src/main/java/com/github/ai/split/api/response/PutGroupResponse.java new file mode 100644 index 0000000..bc078cf --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/response/PutGroupResponse.java @@ -0,0 +1,13 @@ +package com.github.ai.split.api.response; + +import com.github.ai.split.api.GroupDto; +public class PutGroupResponse { + + public GroupDto group = new GroupDto(); + + public PutGroupResponse() {} + + public PutGroupResponse(GroupDto group) { + this.group = group != null ? group : new GroupDto(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/java/com/github/ai/split/api/response/PutMemberResponse.java b/backend/api/src/main/java/com/github/ai/split/api/response/PutMemberResponse.java new file mode 100644 index 0000000..807507f --- /dev/null +++ b/backend/api/src/main/java/com/github/ai/split/api/response/PutMemberResponse.java @@ -0,0 +1,13 @@ +package com.github.ai.split.api.response; + +import com.github.ai.split.api.GroupDto; +public class PutMemberResponse { + + public GroupDto group = new GroupDto(); + + public PutMemberResponse() {} + + public PutMemberResponse(GroupDto group) { + this.group = group != null ? group : new GroupDto(); + } +} \ No newline at end of file diff --git a/backend/api/src/main/scala/com/github/ai/split/api/CurrencyDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/CurrencyDto.scala deleted file mode 100644 index 0f2f74f..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/CurrencyDto.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.ai.split.api - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class CurrencyDto( - isoCode: String, - name: String, - symbol: String -) - -object CurrencyDto { - implicit val encoder: JsonEncoder[CurrencyDto] = DeriveJsonEncoder.gen[CurrencyDto] - implicit val decoder: JsonDecoder[CurrencyDto] = DeriveJsonDecoder.gen[CurrencyDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/ErrorMessageDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/ErrorMessageDto.scala deleted file mode 100644 index 5f2a94f..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/ErrorMessageDto.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.ai.split.api - -import zio.json.{DeriveJsonEncoder, JsonEncoder} - -case class ErrorMessageDto( - message: Option[String], - exception: String, - stacktraceBase64: String, - stacktraceLines: List[String] -) - -object ErrorMessageDto { - implicit val encoder: JsonEncoder[ErrorMessageDto] = DeriveJsonEncoder.gen[ErrorMessageDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/ExpenseDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/ExpenseDto.scala deleted file mode 100644 index 5c4ad63..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/ExpenseDto.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.ai.split.api - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class ExpenseDto( - uid: String, - title: String, - description: Option[String], - amount: Double, - currency: CurrencyDto, - paidBy: List[MemberDto], - splitBetween: List[MemberDto], - created: TimestampDto, - modified: TimestampDto -) - -object ExpenseDto { - implicit val encoder: JsonEncoder[ExpenseDto] = DeriveJsonEncoder.gen[ExpenseDto] - implicit val decoder: JsonDecoder[ExpenseDto] = DeriveJsonDecoder.gen[ExpenseDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/GetGroupErrorDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/GetGroupErrorDto.scala deleted file mode 100644 index b7b746e..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/GetGroupErrorDto.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.ai.split.api - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class GetGroupErrorDto( - uid: String, - message: String -) - -object GetGroupErrorDto { - implicit val encoder: JsonEncoder[GetGroupErrorDto] = DeriveJsonEncoder.gen[GetGroupErrorDto] - implicit val decoder: JsonDecoder[GetGroupErrorDto] = DeriveJsonDecoder.gen[GetGroupErrorDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/GroupDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/GroupDto.scala deleted file mode 100644 index 73195eb..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/GroupDto.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.ai.split.api - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class GroupDto( - uid: String, - title: String, - description: String, - currency: CurrencyDto, - members: List[MemberDto], - expenses: List[ExpenseDto], - paybackTransactions: List[TransactionDto], - created: TimestampDto, - modified: TimestampDto -) - -object GroupDto { - implicit val encoder: JsonEncoder[GroupDto] = DeriveJsonEncoder.gen[GroupDto] - implicit val decoder: JsonDecoder[GroupDto] = DeriveJsonDecoder.gen[GroupDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/MemberDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/MemberDto.scala deleted file mode 100644 index 002f307..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/MemberDto.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.ai.split.api - -import zio.json._ - -case class MemberDto( - uid: String, - name: String -) - -object MemberDto { - implicit val encoder: JsonEncoder[MemberDto] = DeriveJsonEncoder.gen[MemberDto] - implicit val decoder: JsonDecoder[MemberDto] = DeriveJsonDecoder.gen[MemberDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/NewExpenseDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/NewExpenseDto.scala deleted file mode 100644 index ea95cb7..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/NewExpenseDto.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.ai.split.api - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class NewExpenseDto( - title: String, - description: Option[String], - amount: Double, - paidBy: List[UserNameDto], - isSplitBetweenAll: Option[Boolean], - splitBetween: Option[List[UserNameDto]] -) - -object NewExpenseDto { - implicit val encoder: JsonEncoder[NewExpenseDto] = DeriveJsonEncoder.gen[NewExpenseDto] - implicit val decoder: JsonDecoder[NewExpenseDto] = DeriveJsonDecoder.gen[NewExpenseDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/TimestampDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/TimestampDto.scala deleted file mode 100644 index b84801a..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/TimestampDto.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.ai.split.api - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class TimestampDto( - timestampSeconds: Long, - formatted: String -) - -object TimestampDto { - implicit val encoder: JsonEncoder[TimestampDto] = DeriveJsonEncoder.gen[TimestampDto] - implicit val decoder: JsonDecoder[TimestampDto] = DeriveJsonDecoder.gen[TimestampDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/TransactionDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/TransactionDto.scala deleted file mode 100644 index 2099fae..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/TransactionDto.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.ai.split.api - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class TransactionDto( - creditorUid: String, - debtorUid: String, - amount: Double -) - -object TransactionDto { - implicit val encoder: JsonEncoder[TransactionDto] = DeriveJsonEncoder.gen[TransactionDto] - implicit val decoder: JsonDecoder[TransactionDto] = DeriveJsonDecoder.gen[TransactionDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/UserNameDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/UserNameDto.scala deleted file mode 100644 index b65cb1b..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/UserNameDto.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class UserNameDto( - name: String -) - -object UserNameDto { - implicit val encoder: JsonEncoder[UserNameDto] = DeriveJsonEncoder.gen[UserNameDto] - implicit val decoder: JsonDecoder[UserNameDto] = DeriveJsonDecoder.gen[UserNameDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/UserUidDto.scala b/backend/api/src/main/scala/com/github/ai/split/api/UserUidDto.scala deleted file mode 100644 index 9f197c5..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/UserUidDto.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class UserUidDto( - uid: String -) - -object UserUidDto { - implicit val encoder: JsonEncoder[UserUidDto] = DeriveJsonEncoder.gen[UserUidDto] - implicit val decoder: JsonDecoder[UserUidDto] = DeriveJsonDecoder.gen[UserUidDto] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/request/PostExpenseRequest.scala b/backend/api/src/main/scala/com/github/ai/split/api/request/PostExpenseRequest.scala deleted file mode 100644 index 7dd39fa..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/request/PostExpenseRequest.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.github.ai.split.api.request - -import com.github.ai.split.api.UserUidDto -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class PostExpenseRequest( - groupUid: String, - title: String, - description: Option[String], - amount: Double, - paidBy: List[UserUidDto], - isSplitBetweenAll: Option[Boolean], - splitBetween: Option[List[UserUidDto]] -) - -object PostExpenseRequest { - implicit val encoder: JsonEncoder[PostExpenseRequest] = DeriveJsonEncoder.gen[PostExpenseRequest] - implicit val decoder: JsonDecoder[PostExpenseRequest] = DeriveJsonDecoder.gen[PostExpenseRequest] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/request/PostGroupRequest.scala b/backend/api/src/main/scala/com/github/ai/split/api/request/PostGroupRequest.scala deleted file mode 100644 index c78ee25..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/request/PostGroupRequest.scala +++ /dev/null @@ -1,18 +0,0 @@ -package com.github.ai.split.api.request - -import com.github.ai.split.api.{NewExpenseDto, UserNameDto} -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class PostGroupRequest( - password: String, - title: String, - description: Option[String], - currencyIsoCode: String, - members: Option[List[UserNameDto]], - expenses: Option[List[NewExpenseDto]] -) - -object PostGroupRequest { - implicit val encoder: JsonEncoder[PostGroupRequest] = DeriveJsonEncoder.gen[PostGroupRequest] - implicit val decoder: JsonDecoder[PostGroupRequest] = DeriveJsonDecoder.gen[PostGroupRequest] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/request/PostMemberRequest.scala b/backend/api/src/main/scala/com/github/ai/split/api/request/PostMemberRequest.scala deleted file mode 100644 index bfb3890..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/request/PostMemberRequest.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.ai.split.api.request - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class PostMemberRequest( - groupUid: String, - name: String -) - -object PostMemberRequest { - implicit val encoder: JsonEncoder[PostMemberRequest] = DeriveJsonEncoder.gen[PostMemberRequest] - implicit val decoder: JsonDecoder[PostMemberRequest] = DeriveJsonDecoder.gen[PostMemberRequest] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/request/PutExpenseRequest.scala b/backend/api/src/main/scala/com/github/ai/split/api/request/PutExpenseRequest.scala deleted file mode 100644 index 27315a6..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/request/PutExpenseRequest.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.ai.split.api.request - -import com.github.ai.split.api.UserUidDto -import zio.json.{DeriveJsonDecoder, JsonDecoder} - -case class PutExpenseRequest( - title: Option[String], - description: Option[String], - amount: Option[Double], - paidBy: Option[List[UserUidDto]], - isSplitBetweenAll: Option[Boolean], - splitBetween: Option[List[UserUidDto]] -) - -object PutExpenseRequest { - implicit val decoder: JsonDecoder[PutExpenseRequest] = DeriveJsonDecoder.gen[PutExpenseRequest] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/request/PutGroupRequest.scala b/backend/api/src/main/scala/com/github/ai/split/api/request/PutGroupRequest.scala deleted file mode 100644 index d33c29b..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/request/PutGroupRequest.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.ai.split.api.request - -import com.github.ai.split.api.UserUidDto -import zio.json.{DeriveJsonDecoder, JsonDecoder} - -case class PutGroupRequest( - title: Option[String], - password: Option[String], - description: Option[String], - currencyIsoCode: Option[String], - members: Option[List[UserUidDto]] -) - -object PutGroupRequest { - implicit val decoder: JsonDecoder[PutGroupRequest] = DeriveJsonDecoder.gen[PutGroupRequest] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/request/PutMemberRequest.scala b/backend/api/src/main/scala/com/github/ai/split/api/request/PutMemberRequest.scala deleted file mode 100644 index 4c00bef..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/request/PutMemberRequest.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api.request - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class PutMemberRequest( - name: String -) - -object PutMemberRequest { - implicit val encoder: JsonEncoder[PutMemberRequest] = DeriveJsonEncoder.gen[PutMemberRequest] - implicit val decoder: JsonDecoder[PutMemberRequest] = DeriveJsonDecoder.gen[PutMemberRequest] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/response/DeleteExpenseResponse.scala b/backend/api/src/main/scala/com/github/ai/split/api/response/DeleteExpenseResponse.scala deleted file mode 100644 index cf73c46..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/response/DeleteExpenseResponse.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api.response - -import com.github.ai.split.api.GroupDto -import zio.json.{DeriveJsonEncoder, JsonEncoder} - -case class DeleteExpenseResponse( - group: GroupDto -) - -object DeleteExpenseResponse { - implicit val encoder: JsonEncoder[DeleteExpenseResponse] = DeriveJsonEncoder.gen[DeleteExpenseResponse] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/response/DeleteMemberResponse.scala b/backend/api/src/main/scala/com/github/ai/split/api/response/DeleteMemberResponse.scala deleted file mode 100644 index 2f1133e..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/response/DeleteMemberResponse.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api.response - -import com.github.ai.split.api.GroupDto -import zio.json.{DeriveJsonEncoder, JsonEncoder} - -case class DeleteMemberResponse( - group: GroupDto -) - -object DeleteMemberResponse { - implicit val encoder: JsonEncoder[DeleteMemberResponse] = DeriveJsonEncoder.gen[DeleteMemberResponse] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/response/GetCurrenciesResponse.scala b/backend/api/src/main/scala/com/github/ai/split/api/response/GetCurrenciesResponse.scala deleted file mode 100644 index ea9735b..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/response/GetCurrenciesResponse.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api.response - -import com.github.ai.split.api.CurrencyDto -import zio.json.{DeriveJsonEncoder, JsonEncoder} - -case class GetCurrenciesResponse( - currencies: List[CurrencyDto] -) - -object GetCurrenciesResponse { - implicit val encoder: JsonEncoder[GetCurrenciesResponse] = DeriveJsonEncoder.gen[GetCurrenciesResponse] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/response/GetGroupsResponse.scala b/backend/api/src/main/scala/com/github/ai/split/api/response/GetGroupsResponse.scala deleted file mode 100644 index 4312548..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/response/GetGroupsResponse.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.ai.split.api.response - -import com.github.ai.split.api.{GetGroupErrorDto, GroupDto} -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class GetGroupsResponse( - groups: List[GroupDto], - errors: List[GetGroupErrorDto] -) - -object GetGroupsResponse { - implicit val encoder: JsonEncoder[GetGroupsResponse] = DeriveJsonEncoder.gen[GetGroupsResponse] - implicit val decoder: JsonDecoder[GetGroupsResponse] = DeriveJsonDecoder.gen[GetGroupsResponse] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/response/PostExpenseResponse.scala b/backend/api/src/main/scala/com/github/ai/split/api/response/PostExpenseResponse.scala deleted file mode 100644 index 3b3e4e1..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/response/PostExpenseResponse.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api.response - -import com.github.ai.split.api.ExpenseDto -import zio.json.{DeriveJsonEncoder, JsonEncoder} - -case class PostExpenseResponse( - expense: ExpenseDto -) - -object PostExpenseResponse { - implicit val encoder: JsonEncoder[PostExpenseResponse] = DeriveJsonEncoder.gen[PostExpenseResponse] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/response/PostGroupResponse.scala b/backend/api/src/main/scala/com/github/ai/split/api/response/PostGroupResponse.scala deleted file mode 100644 index 513799b..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/response/PostGroupResponse.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api.response - -import com.github.ai.split.api.GroupDto -import zio.json.{DeriveJsonEncoder, JsonEncoder} - -case class PostGroupResponse( - group: GroupDto -) - -object PostGroupResponse { - implicit val encoder: JsonEncoder[PostGroupResponse] = DeriveJsonEncoder.gen[PostGroupResponse] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/response/PostMemberResponse.scala b/backend/api/src/main/scala/com/github/ai/split/api/response/PostMemberResponse.scala deleted file mode 100644 index f885d2c..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/response/PostMemberResponse.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api.response - -import com.github.ai.split.api.GroupDto -import zio.json.{DeriveJsonEncoder, JsonEncoder} - -case class PostMemberResponse( - group: GroupDto -) - -object PostMemberResponse { - implicit val encoder: JsonEncoder[PostMemberResponse] = DeriveJsonEncoder.gen[PostMemberResponse] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/response/PutExpenseResponse.scala b/backend/api/src/main/scala/com/github/ai/split/api/response/PutExpenseResponse.scala deleted file mode 100644 index a4d6359..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/response/PutExpenseResponse.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api.response - -import com.github.ai.split.api.ExpenseDto -import zio.json.{DeriveJsonEncoder, JsonEncoder} - -case class PutExpenseResponse( - expense: ExpenseDto -) - -object PutExpenseResponse { - implicit val encoder: JsonEncoder[PutExpenseResponse] = DeriveJsonEncoder.gen[PutExpenseResponse] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/response/PutGroupResponse.scala b/backend/api/src/main/scala/com/github/ai/split/api/response/PutGroupResponse.scala deleted file mode 100644 index 0705681..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/response/PutGroupResponse.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.ai.split.api.response - -import com.github.ai.split.api.GroupDto -import zio.json.{DeriveJsonEncoder, JsonEncoder} - -case class PutGroupResponse( - group: GroupDto -) - -object PutGroupResponse { - implicit val encoder: JsonEncoder[PutGroupResponse] = DeriveJsonEncoder.gen[PutGroupResponse] -} diff --git a/backend/api/src/main/scala/com/github/ai/split/api/response/PutMemberResponse.scala b/backend/api/src/main/scala/com/github/ai/split/api/response/PutMemberResponse.scala deleted file mode 100644 index 23c8e3e..0000000 --- a/backend/api/src/main/scala/com/github/ai/split/api/response/PutMemberResponse.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.ai.split.api.response - -import com.github.ai.split.api.GroupDto -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -case class PutMemberResponse( - group: GroupDto -) - -object PutMemberResponse { - implicit val encoder: JsonEncoder[PutMemberResponse] = DeriveJsonEncoder.gen[PutMemberResponse] - implicit val decoder: JsonDecoder[PutMemberResponse] = DeriveJsonDecoder.gen[PutMemberResponse] -} diff --git a/backend/app/src/main/scala/com/github/ai/split/Layers.scala b/backend/app/src/main/scala/com/github/ai/split/Layers.scala index 842f4fe..bea31de 100644 --- a/backend/app/src/main/scala/com/github/ai/split/Layers.scala +++ b/backend/app/src/main/scala/com/github/ai/split/Layers.scala @@ -1,5 +1,6 @@ package com.github.ai.split +import com.github.ai.split.data.JsonSerializer import com.github.ai.split.data.currency.CurrencyParser import com.github.ai.split.data.db.dao.{ CurrencyEntityDao, @@ -95,11 +96,12 @@ object Layers { val assembleExpenseUseCase = ZLayer.fromFunction(AssembleExpenseUseCase(_, _, _, _)) // Controllers - val groupController = ZLayer.fromFunction(GroupController(_, _, _, _, _, _, _, _, _, _)) - val memberController = ZLayer.fromFunction(MemberController(_, _, _, _, _, _, _, _)) - val expenseController = ZLayer.fromFunction(ExpenseController(_, _, _, _, _, _, _)) - val currencyController = ZLayer.fromFunction(CurrencyController(_)) + val groupController = ZLayer.fromFunction(GroupController(_, _, _, _, _, _, _, _, _, _, _)) + val memberController = ZLayer.fromFunction(MemberController(_, _, _, _, _, _, _, _, _)) + val expenseController = ZLayer.fromFunction(ExpenseController(_, _, _, _, _, _, _, _)) + val currencyController = ZLayer.fromFunction(CurrencyController(_, _)) // Other val currencyParser = ZLayer.succeed(CurrencyParser()) + val jsonSerialized = ZLayer.succeed(JsonSerializer()) } diff --git a/backend/app/src/main/scala/com/github/ai/split/Main.scala b/backend/app/src/main/scala/com/github/ai/split/Main.scala index 0c4f760..e718251 100644 --- a/backend/app/src/main/scala/com/github/ai/split/Main.scala +++ b/backend/app/src/main/scala/com/github/ai/split/Main.scala @@ -118,6 +118,7 @@ object Main extends ZIOAppDefault { // Others Layers.currencyParser, + Layers.jsonSerialized, Server.live, ZLayer.succeed(serverConfig), Quill.H2.fromNamingStrategy(SnakeCase), diff --git a/backend/app/src/main/scala/com/github/ai/split/data/JsonSerializer.scala b/backend/app/src/main/scala/com/github/ai/split/data/JsonSerializer.scala new file mode 100644 index 0000000..c77215e --- /dev/null +++ b/backend/app/src/main/scala/com/github/ai/split/data/JsonSerializer.scala @@ -0,0 +1,30 @@ +package com.github.ai.split.data + +import com.github.ai.split.entity.exception.{DomainError, JsonDeserializationError, ParsingError} +import com.github.ai.split.utils.some +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken +import zio.{IO, Task, ZIO} + +class JsonSerializer { + + private val gson = GsonBuilder().setPrettyPrinting().create() + + def serialize(data: Any): String = { + gson.toJson(data) + } + + def deserializer[T]( + data: Task[String], + typeOf: Class[T] + ): IO[ParsingError, T] = { + for { + json <- data.mapError(error => ParsingError(message = "Unable to read json data")) + result <- ZIO + .attempt { + gson.fromJson(json, TypeToken.get(typeOf)) + } + .mapError(error => JsonDeserializationError(typeOf = typeOf, cause = error.some)) + } yield result + } +} diff --git a/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleExpenseUseCase.scala b/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleExpenseUseCase.scala index f6f5cfc..5d6f171 100644 --- a/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleExpenseUseCase.scala +++ b/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleExpenseUseCase.scala @@ -1,15 +1,13 @@ package com.github.ai.split.domain.usecases -import com.github.ai.split.data.db.dao.GroupMemberEntityDao import com.github.ai.split.api.ExpenseDto +import com.github.ai.split.data.db.dao.GroupMemberEntityDao import com.github.ai.split.data.db.repository.{CurrencyRepository, ExpenseRepository} import com.github.ai.split.entity.db.ExpenseUid import com.github.ai.split.utils.toExpenseDto import com.github.ai.split.entity.exception.DomainError import zio.* -import java.util.UUID - class AssembleExpenseUseCase( private val expenseRepository: ExpenseRepository, private val currencyRepository: CurrencyRepository, diff --git a/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleGroupResponseUseCase.scala b/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleGroupResponseUseCase.scala index e046b58..e8c465a 100644 --- a/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleGroupResponseUseCase.scala +++ b/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleGroupResponseUseCase.scala @@ -1,6 +1,5 @@ package com.github.ai.split.domain.usecases -import com.github.ai.split.data.db.dao.{GroupEntityDao, GroupMemberEntityDao} import com.github.ai.split.api.GroupDto import com.github.ai.split.entity.db.GroupUid import com.github.ai.split.data.db.repository.{CurrencyRepository, ExpenseRepository, GroupRepository} @@ -8,8 +7,6 @@ import com.github.ai.split.entity.exception.DomainError import zio.* import com.github.ai.split.utils.* -import java.util.UUID - class AssembleGroupResponseUseCase( private val expenseRepository: ExpenseRepository, private val currencyRepository: CurrencyRepository, diff --git a/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleGroupsResponseUseCase.scala b/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleGroupsResponseUseCase.scala index 466df36..e8299da 100644 --- a/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleGroupsResponseUseCase.scala +++ b/backend/app/src/main/scala/com/github/ai/split/domain/usecases/AssembleGroupsResponseUseCase.scala @@ -1,15 +1,13 @@ package com.github.ai.split.domain.usecases -import com.github.ai.split.data.db.dao.{GroupEntityDao, GroupMemberEntityDao, PaidByEntityDao, SplitBetweenEntityDao} import com.github.ai.split.api.GroupDto +import com.github.ai.split.data.db.dao.{GroupEntityDao, GroupMemberEntityDao, PaidByEntityDao, SplitBetweenEntityDao} import com.github.ai.split.data.db.repository.{ExpenseRepository, GroupRepository} import com.github.ai.split.entity.db.GroupUid import com.github.ai.split.entity.exception.DomainError import com.github.ai.split.utils.toGroupDto import zio.* -import java.util.UUID - class AssembleGroupsResponseUseCase( private val groupRepository: GroupRepository, private val expenseRepository: ExpenseRepository, diff --git a/backend/app/src/main/scala/com/github/ai/split/entity/exception/DomainError.scala b/backend/app/src/main/scala/com/github/ai/split/entity/exception/DomainError.scala index 548e1c6..21731e8 100644 --- a/backend/app/src/main/scala/com/github/ai/split/entity/exception/DomainError.scala +++ b/backend/app/src/main/scala/com/github/ai/split/entity/exception/DomainError.scala @@ -9,5 +9,14 @@ class DomainError( ) class ParsingError( - message: String -) extends DomainError(message = Some(message), cause = None) + message: String, + cause: Option[Throwable] = None +) extends DomainError(message = Some(message), cause = cause) + +class JsonDeserializationError( + typeOf: Class[?], + cause: Option[Throwable] = None +) extends ParsingError( + message = s"Unable to deserialize type: ${typeOf.getTypeName}", + cause = cause + ) diff --git a/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/CurrencyController.scala b/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/CurrencyController.scala index a66f310..d7550ff 100644 --- a/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/CurrencyController.scala +++ b/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/CurrencyController.scala @@ -2,32 +2,38 @@ package com.github.ai.split.presentation.controllers import com.github.ai.split.api.CurrencyDto import com.github.ai.split.api.response.GetCurrenciesResponse +import com.github.ai.split.data.JsonSerializer +import com.github.ai.split.entity.db.CurrencyEntity import com.github.ai.split.data.db.repository.CurrencyRepository +import com.github.ai.split.utils.toJavaList import com.github.ai.split.entity.exception.DomainError import zio.* import zio.direct.* import zio.http.Response -import zio.json.* class CurrencyController( - private val currencyRepository: CurrencyRepository + private val currencyRepository: CurrencyRepository, + private val jsonSerializer: JsonSerializer ) { - def getCurrencies(): IO[DomainError, Response] = { defer { val currencies = currencyRepository.getAll().run - val response = GetCurrenciesResponse( - currencies.map { currency => - CurrencyDto( - isoCode = currency.isoCode, - name = currency.name, - symbol = currency.symbol + Response.json(jsonSerializer.serialize(createResponse(currencies))) + } + } + + private def createResponse(currencies: List[CurrencyEntity]): GetCurrenciesResponse = { + new GetCurrenciesResponse( + currencies + .map { currency => + new CurrencyDto( + currency.isoCode, + currency.name, + currency.symbol ) } - ) - - Response.json(response.toJsonPretty) - } + .toJavaList() + ) } } diff --git a/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/ExpenseController.scala b/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/ExpenseController.scala index 1961535..49d6276 100644 --- a/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/ExpenseController.scala +++ b/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/ExpenseController.scala @@ -10,6 +10,7 @@ import com.github.ai.split.domain.usecases.{ } import com.github.ai.split.api.request.{PostExpenseRequest, PutExpenseRequest} import com.github.ai.split.api.response.{DeleteExpenseResponse, PostExpenseResponse, PutExpenseResponse} +import com.github.ai.split.data.JsonSerializer import com.github.ai.split.data.db.repository.ExpenseRepository import com.github.ai.split.entity.exception.DomainError import com.github.ai.split.domain.AccessResolverService @@ -25,11 +26,8 @@ import com.github.ai.split.entity.{ } import zio.* import zio.http.* -import zio.json.* import zio.direct.* -import java.util.UUID - class ExpenseController( private val expenseRepository: ExpenseRepository, private val accessResolver: AccessResolverService, @@ -37,36 +35,37 @@ class ExpenseController( private val assembleExpenseUseCase: AssembleExpenseUseCase, private val assembleGroupUseCase: AssembleGroupResponseUseCase, private val updateExpenseUseCase: UpdateExpenseUseCase, - private val removeExpenseUseCase: RemoveExpenseUseCase + private val removeExpenseUseCase: RemoveExpenseUseCase, + private val jsonSerializer: JsonSerializer ) { def createExpense( request: Request ): IO[DomainError, Response] = { for { - body <- request.body.parse[PostExpenseRequest] + body <- jsonSerializer.deserializer(request.body.asString, classOf[PostExpenseRequest]) groupUid <- body.groupUid.parseUid().map(uid => GroupUid(uid)) password <- parsePasswordParam(request) _ <- accessResolver.canAccessToGroup(groupUid = groupUid, password = password) - paidBy <- parsePaidBy(paidByUids = body.paidBy.map(_.uid)) + paidBy <- parsePaidBy(paidByUids = body.paidBy.toScalaList().map(_.uid)) split <- parseSplit( - isSplitEqually = body.isSplitBetweenAll.getOrElse(false), - splitUids = body.splitBetween.getOrElse(List.empty).map(_.uid) + isSplitEqually = Option(body.isSplitBetweenAll).exists(_.booleanValue()), + splitUids = body.splitBetween.toScalaList().map(_.uid) ) expense <- addExpenseUseCase.addExpenseToGroup( groupUid = groupUid, newExpense = NewExpense( title = body.title.trim, - description = body.description.map(_.trim).getOrElse(""), + description = body.description, amount = body.amount, paidBy = paidBy, split = split ) ) expenseDto <- assembleExpenseUseCase.assembleExpenseDto(expenseUid = expense.uid) - } yield Response.json(PostExpenseResponse(expenseDto).toJsonPretty) + } yield Response.json(jsonSerializer.serialize(PostExpenseResponse(expenseDto))) } def updateExpense( @@ -77,22 +76,27 @@ class ExpenseController( password <- parsePasswordParam(request) _ <- accessResolver.canAccessToExpense(expenseUid = expenseUid, password = password) - data <- request.body.parse[PutExpenseRequest] + data <- jsonSerializer.deserializer(request.body.asString, classOf[PutExpenseRequest]) - newPaidBy <- - if (data.paidBy.isDefined) { + newPaidBy <- { + val paidBy = data.paidBy.toScalaList() + if (paidBy.nonEmpty) { parsePaidBy( - paidByUids = data.paidBy.getOrElse(List.empty).map(_.uid) + paidByUids = paidBy.map(_.uid) ).map(paidBy => Some(paidBy)) } else { ZIO.succeed(None) } + } newSplit <- - if (data.splitBetween.isDefined || data.isSplitBetweenAll.isDefined) { + val splitBetween = data.splitBetween.toScalaList() + val isSplitBetweenAll = Option(data.isSplitBetweenAll).map(_.booleanValue()) + + if (splitBetween.nonEmpty || isSplitBetweenAll.isDefined) { parseSplit( - isSplitEqually = data.isSplitBetweenAll.getOrElse(false), - splitUids = data.splitBetween.getOrElse(List.empty).map(_.uid) + isSplitEqually = isSplitBetweenAll.getOrElse(false), + splitUids = splitBetween.map(_.uid) ) .map(split => Some(split)) } else { @@ -101,15 +105,15 @@ class ExpenseController( _ <- updateExpenseUseCase.updateExpense( expenseUid = expenseUid, - newTitle = data.title.map(_.trim).filter(_.nonEmpty), - newDescription = data.description.map(_.trim).filter(_.nonEmpty), - newAmount = data.amount, + newTitle = Option(data.title).map(_.trim).filter(_.nonEmpty), + newDescription = Option(data.description).map(_.trim).filter(_.nonEmpty), + newAmount = Option(data.amount).map(_.doubleValue()), newPaidBy = newPaidBy, newSplit = newSplit ) expenseDto <- assembleExpenseUseCase.assembleExpenseDto(expenseUid = expenseUid) - } yield Response.json(PutExpenseResponse(expenseDto).toJsonPretty) + } yield Response.json(jsonSerializer.serialize(PutExpenseResponse(expenseDto))) } def removeExpense( @@ -125,7 +129,7 @@ class ExpenseController( val groupDto = assembleGroupUseCase.assembleGroupDto(expense.groupUid).run - Response.json(DeleteExpenseResponse(groupDto).toJsonPretty) + Response.json(jsonSerializer.serialize(DeleteExpenseResponse(groupDto))) } } diff --git a/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/GroupController.scala b/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/GroupController.scala index bfd8ab0..0b62a2e 100644 --- a/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/GroupController.scala +++ b/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/GroupController.scala @@ -20,19 +20,26 @@ import com.github.ai.split.entity.{ NewGroup, NewUser, SplitBetweenAll, - SplitBetweenMembers, - UserReference + SplitBetweenMembers } import com.github.ai.split.api.request.{PostGroupRequest, PutGroupRequest} import com.github.ai.split.api.response.{GetGroupsResponse, PostGroupResponse, PutGroupResponse} +import com.github.ai.split.data.JsonSerializer import com.github.ai.split.entity.Access.{DENIED, GRANTED} import com.github.ai.split.entity.FileExtension.{CSV, HTML} -import com.github.ai.split.entity.db.{ExpenseEntity, GroupMemberEntity, GroupUid, UserEntity, UserUid} +import com.github.ai.split.entity.db.{GroupUid, UserUid} import com.github.ai.split.entity.exception.DomainError -import com.github.ai.split.utils.{getLastUrlParameter, parse, parsePasswordParam, parseUid, parseUidFromUrl, some} +import com.github.ai.split.utils.{ + toJavaList, + toScalaList, + getLastUrlParameter, + parsePasswordParam, + parseUid, + parseUidFromUrl, + some +} import zio.{IO, ZIO} import zio.http.{Body, Charsets, Header, Headers, MediaType, Request, Response, Status} -import zio.json.* import zio.direct.* import java.util.UUID @@ -47,7 +54,8 @@ class GroupController( private val assembleGroupUseCase: AssembleGroupResponseUseCase, private val assembleGroupsUseCase: AssembleGroupsResponseUseCase, private val updateGroupUseCase: UpdateGroupUseCase, - private val exportDataUseCase: ExportGroupDataUseCase + private val exportDataUseCase: ExportGroupDataUseCase, + private val jsonSerializer: JsonSerializer ) { def getGroups( @@ -68,13 +76,15 @@ class GroupController( groups <- assembleGroupsUseCase.assembleGroupDtos(uids = grantedGroupsUids) errors <- ZIO - .succeed(deniedGroupUids.map { uid => - GetGroupErrorDto( - uid = uid.toString, - message = "Not found" - ) - }) - } yield Response.json(GetGroupsResponse(groups, errors).toJsonPretty) + .succeed( + deniedGroupUids.map { uid => + GetGroupErrorDto( + uid.toString, + "Not found" + ) + } + ) + } yield Response.json(jsonSerializer.serialize(GetGroupsResponse(groups.toJavaList(), errors.toJavaList()))) } def updateGroup( @@ -85,9 +95,9 @@ class GroupController( password <- parsePasswordParam(request) _ <- accessResolver.canAccessToGroup(groupUid = groupUid, password = password) - data <- request.body.parse[PutGroupRequest] + data <- jsonSerializer.deserializer(request.body.asString, classOf[PutGroupRequest]) newMembers <- { - val newMembers = data.members.getOrElse(List.empty) + val newMembers = data.members.toScalaList() if (newMembers.nonEmpty) { ZIO .collectAll( @@ -101,37 +111,37 @@ class GroupController( _ <- updateGroupUseCase.updateGroup( groupUid = groupUid, - newPassword = data.password.map(_.trim).filter(_.nonEmpty), - newTitle = data.title.map(_.trim).filter(_.nonEmpty), - newDescription = data.description.map(_.trim).filter(_.nonEmpty), - newCurrencyIsoCode = data.currencyIsoCode.map(_.trim).filter(_.nonEmpty), + newPassword = Option(data.password).map(_.trim).filter(_.nonEmpty), + newTitle = Option(data.title).map(_.trim).filter(_.nonEmpty), + newDescription = Option(data.description).map(_.trim).filter(_.nonEmpty), + newCurrencyIsoCode = Option(data.currencyIsoCode).map(_.trim).filter(_.nonEmpty), newMemberUids = newMembers ) groupDto <- assembleGroupUseCase.assembleGroupDto(groupUid = groupUid) - } yield Response.json(PutGroupResponse(groupDto).toJsonPretty) + } yield Response.json(jsonSerializer.serialize(PutGroupResponse(groupDto))) } def createGroup( request: Request ): IO[DomainError, Response] = { for { - data <- request.body.parse[PostGroupRequest] + data <- jsonSerializer.deserializer(request.body.asString, classOf[PostGroupRequest]) newExpenses <- parseNewExpenses( - expenses = data.expenses.getOrElse(List.empty) + expenses = data.expenses.toScalaList() ) newGroup <- { val newUsers = data.members - .getOrElse(List.empty) + .toScalaList() .map(member => NewUser(name = member.name)) addGroupUseCase.addGroup( NewGroup( password = data.password.trim, title = data.title.trim, - description = data.description.getOrElse(""), + description = data.description.trim, currencyIsoCode = data.currencyIsoCode.trim, members = newUsers, expenses = newExpenses @@ -140,7 +150,7 @@ class GroupController( } groupDto <- assembleGroupUseCase.assembleGroupDto(groupUid = newGroup.uid) - } yield Response.json(PostGroupResponse(groupDto).toJsonPretty) + } yield Response.json(jsonSerializer.serialize(PostGroupResponse(groupDto))) } def exportGroup( @@ -179,16 +189,16 @@ class GroupController( expenses: List[NewExpenseDto] ): IO[DomainError, List[NewExpense]] = { val newExpenses = expenses.map { expense => - val isSplitBetweenAll = expense.isSplitBetweenAll.getOrElse(true) + val isSplitBetweenAll = Some(expense.isSplitBetweenAll).map(_.booleanValue()).getOrElse(true) val splitMembers = expense.splitBetween - .getOrElse(List.empty) + .toScalaList() .map(splitMember => NameReference(name = splitMember.name)) NewExpense( title = expense.title, - description = expense.description.getOrElse(""), + description = expense.description, amount = expense.amount, - paidBy = expense.paidBy.map(payer => NameReference(name = payer.name)), + paidBy = expense.paidBy.toScalaList().map(payer => NameReference(name = payer.name)), split = if (isSplitBetweenAll) SplitBetweenAll else SplitBetweenMembers(splitMembers) ) } diff --git a/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/MemberController.scala b/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/MemberController.scala index fd15df6..73eab68 100644 --- a/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/MemberController.scala +++ b/backend/app/src/main/scala/com/github/ai/split/presentation/controllers/MemberController.scala @@ -10,13 +10,13 @@ import com.github.ai.split.domain.usecases.{ } import com.github.ai.split.api.request.{PostMemberRequest, PutMemberRequest} import com.github.ai.split.api.response.{DeleteMemberResponse, PostMemberResponse, PutMemberResponse} +import com.github.ai.split.data.JsonSerializer import com.github.ai.split.domain.AccessResolverService import com.github.ai.split.entity.db.{GroupUid, MemberUid} -import com.github.ai.split.utils.{parse, parsePasswordParam, parseUid, parseUidFromUrl} +import com.github.ai.split.utils.{parsePasswordParam, parseUid, parseUidFromUrl} import com.github.ai.split.entity.exception.DomainError import zio.* import zio.http.{Request, Response} -import zio.json.* import zio.direct.* class MemberController( @@ -27,7 +27,8 @@ class MemberController( private val addMemberUseCase: AddMembersUseCase, private val removeMembersUseCase: RemoveMembersUseCase, private val updateMemberUseCase: UpdateMemberUseCase, - private val assembleGroupUseCase: AssembleGroupResponseUseCase + private val assembleGroupUseCase: AssembleGroupResponseUseCase, + private val jsonSerializer: JsonSerializer ) { def createMember( @@ -35,7 +36,7 @@ class MemberController( ): IO[DomainError, Response] = { for { password <- parsePasswordParam(request) - body <- request.body.parse[PostMemberRequest] + body <- jsonSerializer.deserializer(request.body.asString, classOf[PostMemberRequest]) groupUid <- body.groupUid.parseUid().map(uid => GroupUid(uid)) _ <- accessResolverService.canAccessToGroup(groupUid = groupUid, password = password) @@ -44,7 +45,7 @@ class MemberController( name = body.name ) groupDto <- assembleGroupUseCase.assembleGroupDto(groupUid) - } yield Response.json(PostMemberResponse(groupDto).toJsonPretty) + } yield Response.json(jsonSerializer.serialize(PostMemberResponse(groupDto))) } def updateMember( @@ -53,13 +54,14 @@ class MemberController( defer { val password = parsePasswordParam(request).run val memberUid = parseUidFromUrl(request).map(uid => MemberUid(uid)).run - val body = request.body.parse[PutMemberRequest].run + val body = jsonSerializer.deserializer(request.body.asString, classOf[PutMemberRequest]).run + accessResolver.canAccessToMember(memberUid = memberUid, password = password).run val member = updateMemberUseCase.updateMember(memberUid = memberUid, newName = body.name).run val groupDto = assembleGroupUseCase.assembleGroupDto(groupUid = member.groupUid).run - Response.json(PutMemberResponse(groupDto).toJsonPretty) + Response.json(jsonSerializer.serialize(PutMemberResponse(groupDto))) } } @@ -76,6 +78,6 @@ class MemberController( _ <- removeMembersUseCase.removeMemberByUids(memberUids = List(memberUid)) groupDto <- assembleGroupUseCase.assembleGroupDto(groupUid = group.uid) - } yield Response.json(DeleteMemberResponse(groupDto).toJsonPretty) + } yield Response.json(jsonSerializer.serialize(DeleteMemberResponse(groupDto))) } } diff --git a/backend/app/src/main/scala/com/github/ai/split/utils/CollectionExtensions.scala b/backend/app/src/main/scala/com/github/ai/split/utils/CollectionExtensions.scala new file mode 100644 index 0000000..78b7180 --- /dev/null +++ b/backend/app/src/main/scala/com/github/ai/split/utils/CollectionExtensions.scala @@ -0,0 +1,11 @@ +package com.github.ai.split.utils + +import scala.jdk.CollectionConverters.* + +extension [T](list: List[T]) { + def toJavaList(): java.util.List[T] = list.asJava +} + +extension [T](javaList: java.util.List[T]) { + def toScalaList(): List[T] = javaList.asScala.toList +} diff --git a/backend/app/src/main/scala/com/github/ai/split/utils/DataConverters.scala b/backend/app/src/main/scala/com/github/ai/split/utils/DataConverters.scala index 3c6e6d1..449b239 100644 --- a/backend/app/src/main/scala/com/github/ai/split/utils/DataConverters.scala +++ b/backend/app/src/main/scala/com/github/ai/split/utils/DataConverters.scala @@ -68,15 +68,15 @@ def toExpenseDto( } } } yield ExpenseDto( - uid = expense.uid.toString, - title = expense.title, - description = expense.description.some, - amount = expense.amount, - currency = toCurrencyDto(currency), - paidBy = paidByUsers, - splitBetween = splitBetweenUsers, - created = toTimestampDto(expense.created), - modified = toTimestampDto(expense.modified) + expense.uid.toString, + expense.title, + expense.description, + expense.amount, + toCurrencyDto(currency), + paidByUsers.toJavaList(), + splitBetweenUsers.toJavaList(), + toTimestampDto(expense.created), + toTimestampDto(expense.modified) ) } @@ -95,8 +95,8 @@ def toMemberDtos( .fromOption(userOption) .map(user => MemberDto( - uid = memberUid.toString, - name = user.name + memberUid.toString, + user.name ) ) .mapError(_ => DomainError(message = "User not found".some)) @@ -158,15 +158,15 @@ def toGroupDto( ) ) } yield GroupDto( - uid = group.uid.toString, - title = group.title, - description = group.description, - currency = toCurrencyDto(currency), - members = memberDtos, - expenses = transformedExpenses, - paybackTransactions = paybackTransactions.map(transaction => toTransactionDto(transaction)), - created = toTimestampDto(group.created), - modified = toTimestampDto(group.modified) + group.uid.toString, + group.title, + group.description, + toCurrencyDto(currency), + memberDtos.toJavaList(), + transformedExpenses.toJavaList(), + paybackTransactions.map(transaction => toTransactionDto(transaction)).toJavaList(), + toTimestampDto(group.created), + toTimestampDto(group.modified) ) } @@ -174,26 +174,26 @@ def toTransactionDto( transaction: Transaction ): TransactionDto = TransactionDto( - creditorUid = transaction.creditor.toString, - debtorUid = transaction.debtor.toString, - amount = transaction.amount + transaction.creditor.toString, + transaction.debtor.toString, + transaction.amount ) def toCurrencyDto( currency: CurrencyEntity ): CurrencyDto = CurrencyDto( - isoCode = currency.isoCode, - name = currency.name, - symbol = currency.symbol + currency.isoCode, + currency.name, + currency.symbol ) def toTimestampDto( dateTime: LocalDateTime ): TimestampDto = TimestampDto( - timestampSeconds = dateTime.toEpochSecond(ZoneOffset.UTC), - formatted = dateTime.format(TIMESTAMP_FORMAT) + dateTime.toEpochSecond(ZoneOffset.UTC), + dateTime.format(TIMESTAMP_FORMAT) ) private val TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") diff --git a/backend/app/src/main/scala/com/github/ai/split/utils/RequestExtensions.scala b/backend/app/src/main/scala/com/github/ai/split/utils/RequestExtensions.scala index 0b9ae73..de9c83c 100644 --- a/backend/app/src/main/scala/com/github/ai/split/utils/RequestExtensions.scala +++ b/backend/app/src/main/scala/com/github/ai/split/utils/RequestExtensions.scala @@ -3,26 +3,10 @@ package com.github.ai.split.utils import com.github.ai.split.entity.exception.DomainError import zio.IO import zio.ZIO -import zio.http.{Body, Request} -import zio.json.* -import zio.direct.* +import zio.http.Request import java.util.UUID -extension (body: Body) { - - def parse[T](implicit decoder: JsonDecoder[T]): IO[DomainError, T] = { - defer { - val text = body.asString.mapError(error => new DomainError(cause = error.some)).run - - ZIO - .fromEither(text.fromJson[T](using decoder)) - .mapError(message => DomainError(message = s"Invalid request format: $message".some)) - .run - } - } -} - extension (request: Request) { def getLastUrlParameter(): ZIO[Any, DomainError, String] = { val parameter = request.url.toString diff --git a/backend/app/src/main/scala/com/github/ai/split/utils/ResponseExtensions.scala b/backend/app/src/main/scala/com/github/ai/split/utils/ResponseExtensions.scala index 11d94cc..e427b84 100644 --- a/backend/app/src/main/scala/com/github/ai/split/utils/ResponseExtensions.scala +++ b/backend/app/src/main/scala/com/github/ai/split/utils/ResponseExtensions.scala @@ -3,11 +3,10 @@ package com.github.ai.split.utils import com.github.ai.split.api.ErrorMessageDto import com.github.ai.split.entity.exception.DomainError import com.github.ai.split.utils.* +import com.google.gson.GsonBuilder import zio.http.{Body, Response, Status} -import zio.json.* import java.nio.charset.StandardCharsets.UTF_8 -import java.nio.charset.{Charset, StandardCharsets} import java.util.Base64 import scala.annotation.tailrec @@ -31,15 +30,18 @@ extension (exception: DomainError) { .toList val response = ErrorMessageDto( - message = if (hasMessage) exception.message.map(_.trim) else None, - exception = exceptionToPrint.toString.trim, - stacktraceBase64 = encodedStacktrace, - stacktraceLines = stacktraceLines + if (hasMessage) exception.message.map(_.trim).getOrElse("") else null, + exceptionToPrint.toString.trim, + encodedStacktrace, + stacktraceLines.toJavaList() ) + // TODO: use JsonSerializable + val gson = GsonBuilder().setPrettyPrinting().create() + Response.error( status = Status.BadRequest, - body = Body.fromString(response.toJsonPretty, UTF_8) + body = Body.fromString(gson.toJson(gson), UTF_8) ) } diff --git a/backend/build.sbt b/backend/build.sbt index 109f6f0..3ef889f 100644 --- a/backend/build.sbt +++ b/backend/build.sbt @@ -4,16 +4,20 @@ val zioJsonVersion = "0.6.2" val circeVersion = "0.14.10" val zioDirect = "1.0.0-RC7" val zioHttp = "3.0.1" +val gsonVersion = "2.11.0" ThisBuild / scalaVersion := scala3Version -ThisBuild / version := "0.1.0-SNAPSHOT" +ThisBuild / version := "0.1.0" lazy val api = project .in(file("api")) .settings( name := "simple-split-api", + artifactName := { (sv: ScalaVersion, module: ModuleID, artifact: Artifact) => + artifact.name + "." + artifact.extension + }, libraryDependencies ++= Seq( - "dev.zio" %% "zio-json" % zioJsonVersion + "com.google.code.gson" % "gson" % gsonVersion ) ) @@ -61,23 +65,6 @@ lazy val app = project ) ) -lazy val generateKotlinClasses = taskKey[Unit]("Generate Kotlin API classes") - -lazy val codegen = project - .in(file("codegen")) - .dependsOn(api) - .settings( - name := "simple-split-codegen", - libraryDependencies ++= Seq( - "dev.zio" %% "zio" % zioVersion, - "dev.zio" %% "zio-json" % zioJsonVersion, - "dev.zio" %% "zio-direct" % zioDirect - ), - generateKotlinClasses := { - (Compile / runMain).toTask(" com.github.ai.split.codegen.TranspilerMain api/src/main/scala ./../android/backend-api/src/main/kotlin").value - }, - ) - lazy val apiClient = project .in(file("api-client")) .dependsOn(api) @@ -92,9 +79,7 @@ lazy val apiClient = project libraryDependencies ++= Seq( "dev.zio" %% "zio" % zioVersion, - "dev.zio" %% "zio-json" % zioJsonVersion, "dev.zio" %% "zio-direct" % zioDirect, - "dev.zio" %% "zio-http" % zioHttp, - "dev.zio" %% "zio-json" % zioJsonVersion + "dev.zio" %% "zio-http" % zioHttp ) ) \ No newline at end of file diff --git a/backend/codegen/src/main/scala/com/github/ai/split/codegen/ScalaToKotlinTranspiler.scala b/backend/codegen/src/main/scala/com/github/ai/split/codegen/ScalaToKotlinTranspiler.scala deleted file mode 100644 index ebca6d8..0000000 --- a/backend/codegen/src/main/scala/com/github/ai/split/codegen/ScalaToKotlinTranspiler.scala +++ /dev/null @@ -1,187 +0,0 @@ -package com.github.ai.split.codegen - -import com.github.ai.split.codegen.ScalaToKotlinTranspiler.{caseClassRegex, fieldRegex, importRegex, packageRegex} -import com.github.ai.split.codegen.model.{KotlinType, ScalaSyntaxError, AppError, Field} -import zio.* - -class ScalaToKotlinTranspiler { - - def transpile(input: String): IO[AppError, String] = { - for { - packageName <- parsePackageName(input) - imports <- parseImports(input) - className <- parseClassName(input) - fields <- parseFields(input) - - transpiledImports <- transpileImports(imports) - transpiledFields <- transpileFields(fields) - - result <- formatKotlinClass( - KotlinType( - packageName = packageName, - imports = transpiledImports, - typeName = className, - fields = transpiledFields - ) - ) - } yield result - } - - private def parsePackageName(input: String): IO[AppError, String] = { - val packageName = packageRegex - .findFirstMatchIn(input) - .map(m => m.group(1).trim) - - if (packageName.isDefined) ZIO.succeed(packageName.getOrElse("")) - else ZIO.fail(ScalaSyntaxError(message = "Failed to parse 'package' block")) - } - - private def parseImports(input: String): IO[AppError, List[String]] = { - val imports = importRegex.findAllIn(input).map { line => line }.toList - - ZIO.succeed(imports) - } - - private def parseClassName(input: String): IO[AppError, String] = { - val className = caseClassRegex - .findFirstMatchIn(input) - .map(m => m.group(1).trim) - - if (className.isDefined) ZIO.succeed(className.getOrElse("")) - else ZIO.fail(ScalaSyntaxError(message = "Failed to parse class name")) - } - - private def parseFields(input: String): IO[AppError, List[String]] = { - val allFields = caseClassRegex - .findFirstMatchIn(input) - .map(m => m.group(2).trim) - - if (allFields.isDefined) ZIO.succeed(allFields.getOrElse("").trim.split(",").toList) - else ZIO.fail(ScalaSyntaxError(message = "Failed to parse 'case class' fields")) - } - - private def transpileImports(imports: List[String]): IO[AppError, List[String]] = { - val transpiledImports = imports.map(line => transpileImport(line)) - - ZIO - .collectAll(transpiledImports) - .map(lines => List("import kotlinx.serialization.Serializable") ++ lines.flatten) - } - - private def transpileImport(line: String): IO[AppError, List[String]] = { - val trimmedLine = line.trim.replaceAll("_", "*") - if (trimmedLine.isEmpty || trimmedLine.startsWith("import zio")) { - return ZIO.succeed(List.empty) - } - - if (trimmedLine.contains("{") || trimmedLine.contains("}")) { - val bracketStartIdx = trimmedLine.indexOf("{") - val bracketEndIdx = trimmedLine.lastIndexOf("}") - if (bracketStartIdx < 0 || bracketEndIdx < 0 || bracketStartIdx >= bracketEndIdx) { - return ZIO.fail(ScalaSyntaxError(message = s"Unable to transpile '$line'")) - } - - val typeNames = trimmedLine - .substring(bracketStartIdx + 1, bracketEndIdx) - .split(" ") - .map(name => name.replaceAll(",", "")) - - val packageName = trimmedLine.substring(0, bracketStartIdx - 1) - val types = typeNames.map(name => packageName + "." + name).toList - - ZIO.succeed(types) - } else { - ZIO.succeed(List(trimmedLine)) - } - } - - private def transpileFields(fields: List[String]): IO[AppError, List[Field]] = { - ZIO.collectAll(fields.map(field => transpileField(field))) - } - - private def transpileField(field: String): IO[AppError, Field] = { - val cleanedField = field - .replaceAll("val", "") - .replaceAll("var", "") - .trim - - val nameAndType = fieldRegex - .findFirstMatchIn(cleanedField) - .map(m => (m.group(1), m.group(2))) - - if (nameAndType.isEmpty) { - return ZIO.fail(ScalaSyntaxError(message = s"Failed to transpile variable '$field'")) - } - - val (fieldName, fieldType) = nameAndType.get - - if (fieldType.contains("[") && fieldType.contains("]")) { - val bracketStartIdx = fieldType.indexOf("[") - val bracketEndIdx = fieldType.lastIndexOf("]") - - if (bracketStartIdx < 0 || bracketEndIdx < 0) { - return ZIO.fail(ScalaSyntaxError(message = s"Failed to transpile variable '$field'")) - } - - val genericType = fieldType.substring(0, bracketStartIdx) - val parameterType = fieldType - .substring(bracketStartIdx + 1, bracketEndIdx) - .replaceAll("\\[", "<") - .replaceAll("]", ">") - - if (genericType == "Option") { - ZIO.succeed( - Field( - name = fieldName, - fieldType = s"$parameterType?" - ) - ) - } else { - ZIO.succeed( - Field(name = fieldName, fieldType = fieldType.replaceAll("\\[", "<").replaceAll("]", ">")) - ) - } - } else { - ZIO.succeed( - Field( - name = fieldName, - fieldType = fieldType - ) - ) - } - } - - private def formatKotlinClass(tpe: KotlinType): IO[AppError, String] = { - val sb = StringBuilder() - - sb.append(s"package ${tpe.packageName}\n") - - if (tpe.imports.nonEmpty) { - sb.append("\n") - sb.append(tpe.imports.mkString("\n")) - sb.append("\n\n") - } else { - sb.append("\n") - } - - sb.append("@Serializable\n") - sb.append(s"data class ${tpe.typeName}(\n") - for ((field, index) <- tpe.fields.zipWithIndex) { - sb.append(s" val ${field.name}: ${field.fieldType}") - if (index != tpe.fields.size - 1) { - sb.append(",") - } - sb.append("\n") - } - sb.append(")") - - ZIO.succeed(sb.toString()) - } -} - -object ScalaToKotlinTranspiler { - private val importRegex = """(?m)^import.*""".r - private val packageRegex = """^\s*package\s+(\S+)""".r - private val caseClassRegex = """case class (\w+)\(\s*([\s\S]*?)\s*\)""".r - private val fieldRegex = """(\w+):\s+([\w\[\]]+)""".r -} diff --git a/backend/codegen/src/main/scala/com/github/ai/split/codegen/TranspilerMain.scala b/backend/codegen/src/main/scala/com/github/ai/split/codegen/TranspilerMain.scala deleted file mode 100644 index 26876c7..0000000 --- a/backend/codegen/src/main/scala/com/github/ai/split/codegen/TranspilerMain.scala +++ /dev/null @@ -1,136 +0,0 @@ -package com.github.ai.split.codegen - -import com.github.ai.split.codegen.model.{AppError, IOError, InvalidArgumentsError} -import zio.* - -import java.io.File -import java.io.PrintWriter -import scala.collection.mutable -import scala.collection.mutable.ListBuffer -import scala.io.Source - -object TranspilerMain extends ZIOAppDefault { - - def run: ZIO[ZIOAppArgs, Any, ExitCode] = { - val program = for { - arguments <- ZIOAppArgs.getArgs.flatMap(args => parseArguments(args.toList)) - scalaFiles <- findScalaFiles(arguments.sourceDir) - transpiledFiles <- transpile(scalaFiles, arguments.destinationDir) - } yield ExitCode.success - - program - .catchAll { error => - Console.printLineError(s"Error: $error") *> ZIO.succeed(ExitCode.failure) - } - } - - case class Arguments(sourceDir: String, destinationDir: String) - - private def parseArguments(args: List[String]): IO[AppError, Arguments] = { - args match { - case sourceDir :: destinationDir :: Nil => - ZIO.succeed(Arguments(sourceDir, destinationDir)) - case _ => - ZIO.fail(InvalidArgumentsError(message = s"Failed to parse arguments")) - } - } - - private def findScalaFiles( - rootPath: String - ): IO[AppError, List[FilePath]] = { - ZIO - .attempt { - val scalaFiles = ListBuffer[File]() - val queue = mutable.Queue[File]() - - queue.addOne(File(rootPath)) - - while (queue.nonEmpty) { - val dir = queue.remove(0) - if (dir.isDirectory) { - val childFiles = dir.listFiles().toList - - for (childFile <- childFiles) { - if (childFile.isDirectory) { - queue.addOne(childFile) - } else if (childFile.getName.endsWith(".scala")) { - scalaFiles.addOne(childFile) - } - } - } - } - - scalaFiles.toList - .map(file => FilePath(root = rootPath, path = file.getPath.stripPrefix(rootPath))) - } - .mapError(error => IOError(exception = error)) - } - - private def transpile( - files: List[FilePath], - outputDir: String - ): IO[AppError, List[FilePath]] = { - val transpiler = ScalaToKotlinTranspiler() - - val outputFiles = files.map { file => - for { - scalaContent <- readFile(file) - kotlinCode <- transpiler.transpile(scalaContent) - destinationFile <- createDestinationFile(inputFile = file, outputDir = outputDir) - - _ <- Console.printLine(s"Write file: ${destinationFile.toJavaFile().getPath}").mapError(IOError(_)) - - result <- writeFile(file = destinationFile, content = kotlinCode) - } yield result - } - - ZIO.collectAll(outputFiles) - } - - private def createDestinationFile( - inputFile: FilePath, - outputDir: String - ): IO[AppError, FilePath] = { - ZIO.succeed( - FilePath( - root = outputDir, - path = inputFile.path.replace(".scala", ".kt") - ) - ) - } - - private def readFile(file: FilePath): IO[AppError, String] = { - ZIO - .attempt { - val source = Source.fromFile(file.toJavaFile()) - try source.mkString - finally source.close() - } - .mapError(IOError) - } - - private def writeFile( - file: FilePath, - content: String - ): IO[AppError, FilePath] = { - ZIO - .attempt { - val parent = file.toJavaFile().getParentFile - if (!parent.exists()) { - parent.mkdirs() - } - - val pw = new PrintWriter(file.toJavaFile()) - try { - pw.write(content) - pw.flush() - } finally pw.close() - } - .map(_ => file) - .mapError(IOError) - } -} - -case class FilePath(root: String, path: String) { - def toJavaFile(): File = File(root, path) -} diff --git a/backend/codegen/src/main/scala/com/github/ai/split/codegen/model/AppError.scala b/backend/codegen/src/main/scala/com/github/ai/split/codegen/model/AppError.scala deleted file mode 100644 index 02561ef..0000000 --- a/backend/codegen/src/main/scala/com/github/ai/split/codegen/model/AppError.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.ai.split.codegen.model - -sealed class AppError(message: String) - -case class InvalidArgumentsError(message: String) extends AppError(message) - -case class IOError(exception: Throwable) extends AppError(exception.getMessage) - -case class ScalaSyntaxError(message: String) extends AppError(message) diff --git a/backend/codegen/src/main/scala/com/github/ai/split/codegen/model/Field.scala b/backend/codegen/src/main/scala/com/github/ai/split/codegen/model/Field.scala deleted file mode 100644 index cfb7ed6..0000000 --- a/backend/codegen/src/main/scala/com/github/ai/split/codegen/model/Field.scala +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.ai.split.codegen.model - -case class Field( - name: String, - fieldType: String -) diff --git a/backend/codegen/src/main/scala/com/github/ai/split/codegen/model/KotlinType.scala b/backend/codegen/src/main/scala/com/github/ai/split/codegen/model/KotlinType.scala deleted file mode 100644 index 77f1bef..0000000 --- a/backend/codegen/src/main/scala/com/github/ai/split/codegen/model/KotlinType.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.ai.split.codegen.model - -case class KotlinType( - packageName: String, - imports: List[String], - typeName: String, - fields: List[Field] -)