Add Rust helpers for resolving labels

ET-5091
This commit is contained in:
Niccolò Forlini
2025-10-23 12:57:25 +02:00
parent 4d3b08aa25
commit 7062fb7403
10 changed files with 260 additions and 3 deletions
@@ -22,17 +22,19 @@ import ch.protonmail.android.mailcommon.domain.coroutines.AppScope
import ch.protonmail.android.mailcommon.domain.coroutines.IODispatcher
import ch.protonmail.android.maillabel.data.MailLabelRustCoroutineScope
import ch.protonmail.android.maillabel.data.local.LabelDataSource
import ch.protonmail.android.maillabel.data.local.RustGetLabelIdBySystemLabel
import ch.protonmail.android.maillabel.data.local.RustGetSystemLabelById
import ch.protonmail.android.maillabel.data.local.RustLabelDataSource
import ch.protonmail.android.maillabel.data.repository.InMemorySelectedMailLabelIdRepositoryImpl
import ch.protonmail.android.maillabel.data.local.RustMailboxFactory
import ch.protonmail.android.maillabel.data.repository.InMemorySelectedMailLabelIdRepositoryImpl
import ch.protonmail.android.maillabel.data.repository.RustLabelRepository
import ch.protonmail.android.maillabel.data.repository.ViewModeRepositoryImpl
import ch.protonmail.android.maillabel.data.usecase.CreateRustSidebar
import ch.protonmail.android.maillabel.data.usecase.RustGetAllMailLabelId
import ch.protonmail.android.maillabel.domain.repository.LabelRepository
import ch.protonmail.android.maillabel.domain.repository.SelectedMailLabelIdRepository
import ch.protonmail.android.maillabel.domain.usecase.FindLocalSystemLabelId
import ch.protonmail.android.maillabel.domain.repository.ViewModeRepository
import ch.protonmail.android.maillabel.domain.usecase.FindLocalSystemLabelId
import ch.protonmail.android.mailsession.domain.repository.UserSessionRepository
import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserId
import dagger.Module
@@ -54,18 +56,23 @@ object MailLabelModule {
@MailLabelRustCoroutineScope
fun provideLabelRustCoroutineScope(): CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@Suppress("LongParameterList")
@Provides
@Singleton
fun provideRustLabelDataSource(
userSessionRepository: UserSessionRepository,
createRustSidebar: CreateRustSidebar,
rustGetAllMailLabelId: RustGetAllMailLabelId,
rustGetSystemLabelById: RustGetSystemLabelById,
rustGetLabelIdBySystemLabel: RustGetLabelIdBySystemLabel,
@MailLabelRustCoroutineScope coroutineScope: CoroutineScope,
@IODispatcher ioDispatcher: CoroutineDispatcher
): LabelDataSource = RustLabelDataSource(
userSessionRepository,
createRustSidebar,
rustGetAllMailLabelId,
rustGetSystemLabelById,
rustGetLabelIdBySystemLabel,
coroutineScope,
ioDispatcher
)
@@ -20,12 +20,14 @@ package ch.protonmail.android.maillabel.data.local
import arrow.core.Either
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalSystemLabel
import ch.protonmail.android.mailcommon.domain.model.DataError
import kotlinx.coroutines.flow.Flow
import me.proton.core.domain.entity.UserId
import uniffi.proton_mail_uniffi.SidebarCustomFolder
import uniffi.proton_mail_uniffi.SidebarCustomLabel
import uniffi.proton_mail_uniffi.SidebarSystemLabel
import uniffi.proton_mail_uniffi.SystemLabel
interface LabelDataSource {
@@ -37,4 +39,7 @@ interface LabelDataSource {
suspend fun getAllMailLabelId(userId: UserId): Either<DataError, LocalLabelId>
suspend fun resolveSystemLabelByLocalId(userId: UserId, labelId: LocalLabelId): Either<DataError, LocalSystemLabel>
suspend fun resolveLocalIdBySystemLabel(userId: UserId, systemLabel: SystemLabel): Either<DataError, LocalLabelId>
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.maillabel.data.local
import arrow.core.Either
import arrow.core.raise.either
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalSystemLabel
import ch.protonmail.android.mailcommon.data.mapper.toDataError
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailsession.domain.wrapper.MailUserSessionWrapper
import uniffi.proton_mail_uniffi.ResolveSystemLabelIdResult
import uniffi.proton_mail_uniffi.resolveSystemLabelId
import javax.inject.Inject
class RustGetLabelIdBySystemLabel @Inject constructor() {
suspend operator fun invoke(
mailUserSession: MailUserSessionWrapper,
labelId: LocalSystemLabel
): Either<DataError, LocalLabelId> = either {
when (val result = resolveSystemLabelId(mailUserSession.getRustUserSession(), labelId)) {
is ResolveSystemLabelIdResult.Error -> raise(result.v1.toDataError())
is ResolveSystemLabelIdResult.Ok -> result.v1 ?: raise(DataError.Local.NoDataCached)
}
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.maillabel.data.local
import arrow.core.Either
import arrow.core.raise.either
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalSystemLabel
import ch.protonmail.android.mailcommon.data.mapper.toDataError
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailsession.domain.wrapper.MailUserSessionWrapper
import uniffi.proton_mail_uniffi.ResolveSystemLabelByIdResult
import uniffi.proton_mail_uniffi.resolveSystemLabelById
import javax.inject.Inject
class RustGetSystemLabelById @Inject constructor() {
suspend operator fun invoke(
mailUserSession: MailUserSessionWrapper,
labelId: LocalLabelId
): Either<DataError, LocalSystemLabel> = either {
when (val result = resolveSystemLabelById(mailUserSession.getRustUserSession(), labelId)) {
is ResolveSystemLabelByIdResult.Error -> raise(result.v1.toDataError())
is ResolveSystemLabelByIdResult.Ok -> result.v1 ?: raise(DataError.Local.NoDataCached)
}
}
}
@@ -21,6 +21,7 @@ package ch.protonmail.android.maillabel.data.local
import arrow.core.Either
import arrow.core.left
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalSystemLabel
import ch.protonmail.android.mailcommon.domain.coroutines.IODispatcher
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.maillabel.data.MailLabelRustCoroutineScope
@@ -51,6 +52,8 @@ class RustLabelDataSource @Inject constructor(
private val userSessionRepository: UserSessionRepository,
private val createRustSidebar: CreateRustSidebar,
private val rustGetAllMailLabelId: RustGetAllMailLabelId,
private val rustGetSystemLabelById: RustGetSystemLabelById,
private val rustGetLabelIdBySystemLabel: RustGetLabelIdBySystemLabel,
@MailLabelRustCoroutineScope private val coroutineScope: CoroutineScope,
@IODispatcher private val ioDispatcher: CoroutineDispatcher
) : LabelDataSource {
@@ -152,4 +155,36 @@ class RustLabelDataSource @Inject constructor(
}
return@withContext rustGetAllMailLabelId(session)
}
override suspend fun resolveSystemLabelByLocalId(
userId: UserId,
labelId: LocalLabelId
): Either<DataError, LocalSystemLabel> {
return withContext(ioDispatcher) {
val session = userSessionRepository.getUserSession(userId)
if (session == null) {
Timber.e("rust-label: trying to resolve system label by local id with null session.")
return@withContext DataError.Local.NoUserSession.left()
}
rustGetSystemLabelById(session, labelId)
}
}
override suspend fun resolveLocalIdBySystemLabel(
userId: UserId,
systemLabel: LocalSystemLabel
): Either<DataError, LocalLabelId> {
return withContext(ioDispatcher) {
val session = userSessionRepository.getUserSession(userId)
if (session == null) {
Timber.e("rust-label: trying to resolve local id by system label with null session.")
return@withContext DataError.Local.NoUserSession.left()
}
rustGetLabelIdBySystemLabel(session, systemLabel)
}
}
}
@@ -125,6 +125,23 @@ fun LocalSystemLabel.toSystemLabel() = when (this) {
}
}
fun SystemLabelId.toLocalSystemLabel() = when (this) {
SystemLabelId.Inbox -> LocalSystemLabel.INBOX
SystemLabelId.AllDrafts -> LocalSystemLabel.ALL_DRAFTS
SystemLabelId.AllSent -> LocalSystemLabel.ALL_SENT
SystemLabelId.Trash -> LocalSystemLabel.TRASH
SystemLabelId.Spam -> LocalSystemLabel.SPAM
SystemLabelId.AllMail -> LocalSystemLabel.ALL_MAIL
SystemLabelId.Archive -> LocalSystemLabel.ARCHIVE
SystemLabelId.Sent -> LocalSystemLabel.SENT
SystemLabelId.Drafts -> LocalSystemLabel.DRAFTS
SystemLabelId.Outbox -> LocalSystemLabel.OUTBOX
SystemLabelId.Starred -> LocalSystemLabel.STARRED
SystemLabelId.AllScheduled -> LocalSystemLabel.SCHEDULED
SystemLabelId.AlmostAllMail -> LocalSystemLabel.ALMOST_ALL_MAIL
SystemLabelId.Snoozed -> LocalSystemLabel.SNOOZED
}
fun MovableSystemFolder.toSystemLabel() = when (this) {
MovableSystemFolder.INBOX -> SystemLabelId.Inbox
MovableSystemFolder.TRASH -> SystemLabelId.Trash
@@ -18,15 +18,21 @@
package ch.protonmail.android.maillabel.data.repository
import arrow.core.Either
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.maillabel.data.local.LabelDataSource
import ch.protonmail.android.maillabel.data.mapper.toLabel
import ch.protonmail.android.maillabel.data.mapper.toLabelId
import ch.protonmail.android.maillabel.data.mapper.toLabelWithSystemLabelId
import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId
import ch.protonmail.android.maillabel.data.mapper.toLocalSystemLabel
import ch.protonmail.android.maillabel.data.mapper.toSystemLabel
import ch.protonmail.android.maillabel.domain.model.Label
import ch.protonmail.android.maillabel.domain.model.LabelId
import ch.protonmail.android.maillabel.domain.model.LabelType
import ch.protonmail.android.maillabel.domain.model.LabelWithSystemLabelId
import ch.protonmail.android.maillabel.domain.model.NewLabel
import ch.protonmail.android.maillabel.domain.model.SystemLabelId
import ch.protonmail.android.maillabel.domain.repository.LabelRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
@@ -147,6 +153,20 @@ class RustLabelRepository @Inject constructor(
TODO("Not yet implemented")
}
override suspend fun resolveSystemLabel(userId: UserId, labelId: LabelId): Either<DataError, SystemLabelId> {
return labelDataSource.resolveSystemLabelByLocalId(userId = userId, labelId = labelId.toLocalLabelId()).map {
it.toSystemLabel()
}
}
override suspend fun resolveLocalIdBySystemLabel(
userId: UserId,
labelId: SystemLabelId
): Either<DataError, LabelId> {
return labelDataSource.resolveLocalIdBySystemLabel(userId = userId, systemLabel = labelId.toLocalSystemLabel())
.map { it.toLabelId() }
}
private fun Flow<List<Label>>.convertToDataResultFlow(): Flow<DataResult<List<Label>>> {
return this.map { labels ->
if (labels.isNotEmpty()) {
@@ -20,9 +20,13 @@ package ch.protonmail.android.maillabel.data.local
import app.cash.turbine.test
import arrow.core.right
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalSystemLabel
import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId
import ch.protonmail.android.maillabel.data.usecase.CreateRustSidebar
import ch.protonmail.android.maillabel.data.usecase.RustGetAllMailLabelId
import ch.protonmail.android.maillabel.data.wrapper.SidebarWrapper
import ch.protonmail.android.maillabel.domain.model.LabelId
import ch.protonmail.android.mailsession.domain.repository.UserSessionRepository
import ch.protonmail.android.mailsession.domain.wrapper.MailUserSessionWrapper
import ch.protonmail.android.test.utils.rule.LoggingTestRule
@@ -32,6 +36,7 @@ import ch.protonmail.android.testdata.user.UserIdTestData
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.confirmVerified
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@@ -44,7 +49,7 @@ import uniffi.proton_mail_uniffi.LiveQueryCallback
import uniffi.proton_mail_uniffi.WatchHandle
import kotlin.test.assertEquals
class RustLabelDataSourceTest {
internal class RustLabelDataSourceTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@@ -56,11 +61,15 @@ class RustLabelDataSourceTest {
private val testCoroutineScope = CoroutineScope(mainDispatcherRule.testDispatcher)
private val createRustSidebar = mockk<CreateRustSidebar>()
private val rustGetAllMailLabelId = mockk<RustGetAllMailLabelId>()
private val rustGetSystemLabelById = mockk<RustGetSystemLabelById>()
private val rustGetLabelIdBySystemLabel = mockk<RustGetLabelIdBySystemLabel>()
private val labelDataSource = RustLabelDataSource(
userSessionRepository,
createRustSidebar,
rustGetAllMailLabelId,
rustGetSystemLabelById,
rustGetLabelIdBySystemLabel,
testCoroutineScope,
mainDispatcherRule.testDispatcher
)
@@ -348,4 +357,45 @@ class RustLabelDataSourceTest {
coVerify { sidebarMock.destroy() }
}
}
@Test
fun `resolve system label by local id calls the UC with the user session and label`() = runTest {
// Given
val userId = UserIdTestData.userId
val userSessionMock = mockk<MailUserSessionWrapper>()
val labelId = LabelId("1").toLocalLabelId()
coEvery { userSessionRepository.getUserSession(userId) } returns userSessionMock
coEvery {
rustGetSystemLabelById(userSessionMock, labelId)
} returns LocalSystemLabel.INBOX.right()
// When
val actual = labelDataSource.resolveSystemLabelByLocalId(userId, labelId)
// Then
assertEquals(LocalSystemLabel.INBOX.right(), actual)
coVerify(exactly = 1) { rustGetSystemLabelById(userSessionMock, labelId) }
confirmVerified(rustGetLabelIdBySystemLabel)
}
@Test
fun `resolve local id by system label calls the UC with the user session and system label`() = runTest {
// Given
val userId = UserIdTestData.userId
val userSessionMock = mockk<MailUserSessionWrapper>()
val systemLabel = LocalSystemLabel.INBOX
val expectedLocalLabelId = LocalLabelId(1u)
coEvery { userSessionRepository.getUserSession(userId) } returns userSessionMock
coEvery {
rustGetLabelIdBySystemLabel(userSessionMock, systemLabel)
} returns expectedLocalLabelId.right()
// When
val actual = labelDataSource.resolveLocalIdBySystemLabel(userId, systemLabel)
// Then
assertEquals(expectedLocalLabelId.right(), actual)
coVerify(exactly = 1) { rustGetLabelIdBySystemLabel(userSessionMock, systemLabel) }
confirmVerified(rustGetLabelIdBySystemLabel)
}
}
@@ -18,11 +18,14 @@
package ch.protonmail.android.maillabel.domain.repository
import arrow.core.Either
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.maillabel.domain.model.Label
import ch.protonmail.android.maillabel.domain.model.LabelId
import ch.protonmail.android.maillabel.domain.model.LabelType
import ch.protonmail.android.maillabel.domain.model.LabelWithSystemLabelId
import ch.protonmail.android.maillabel.domain.model.NewLabel
import ch.protonmail.android.maillabel.domain.model.SystemLabelId
import kotlinx.coroutines.flow.Flow
import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.entity.UserId
@@ -103,6 +106,11 @@ interface LabelRepository {
labelId: LabelId
)
suspend fun resolveSystemLabel(userId: UserId, labelId: LabelId): Either<DataError, SystemLabelId>
suspend fun resolveLocalIdBySystemLabel(userId: UserId, labelId: SystemLabelId): Either<DataError, LabelId>
/**
* Mark local data as stale for [userId], by [type].
*
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.maillabel.domain.usecase
import ch.protonmail.android.maillabel.domain.model.LabelId
import ch.protonmail.android.maillabel.domain.repository.LabelRepository
import me.proton.core.domain.entity.UserId
import javax.inject.Inject
class ResolveSystemLabelId @Inject constructor(private val labelRepository: LabelRepository) {
suspend operator fun invoke(userId: UserId, labelId: LabelId) = labelRepository.resolveSystemLabel(userId, labelId)
}