diff --git a/contract/build.gradle.kts b/contract/build.gradle.kts new file mode 100644 index 0000000000000000000000000000000000000000..6b866819e578d67741be745372f3ef46315af965 --- /dev/null +++ b/contract/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + id(Plugins.AndroidLib.plugin) + id(Plugins.Kotlin.plugin) + id(Plugins.Parcelize.plugin) +} + +android { + namespace = "band.effective.office.contract" + compileSdk = 33 + + defaultConfig { + minSdk = 26 + targetSdk = 33 + } + sourceSets["main"].apply { + manifest.srcFile("src/androidMain/AndroidManifest.xml") + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + +} + +kotlin { + android { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + sourceSets { + val commonMain by getting { + dependencies { + api(Dependencies.Ktor.Client.Core) + implementation(Dependencies.KotlinxSerialization.json) + implementation(Dependencies.KotlinxDatetime.kotlinxDatetime) + implementation(Dependencies.Ktor.Client.CIO) + + } + } + val androidMain by getting { + dependencies { + + } + } + } +} \ No newline at end of file diff --git a/contract/src/androidMain/AndroidManifest.xml b/contract/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..f750b51350c8100b9f6b5b206b41d419fbd85470 --- /dev/null +++ b/contract/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/api/Api.kt b/contract/src/commonMain/kotlin/band/effective/office/network/api/Api.kt new file mode 100644 index 0000000000000000000000000000000000000000..0a3de1d25529bc7c312c83638771292c4064d37c --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/api/Api.kt @@ -0,0 +1,58 @@ +package band.effective.office.network.api + +import band.effective.office.network.dto.BookingInfo +import band.effective.office.network.dto.SuccessResponse +import band.effective.office.network.dto.UserDTO +import band.effective.office.network.dto.WorkspaceDTO +import band.effective.office.network.model.Either +import band.effective.office.network.model.ErrorResponse +import kotlinx.coroutines.flow.Flow + +interface Api { + /**Get workspace by id + * @param id workspace id + * @return if response is success when return workspace info*/ + suspend fun getWorkspace(id: String): Either + + /**Get all workspace current type + * @param tag workspace type. Meeting or regular + * @return if response is success when return list of workspaces*/ + suspend fun getWorkspaces(tag: String): Either> + + /**Get user by id + * @param id user id + * @return if response is success when return user info*/ + suspend fun getUser(id: String): Either + + /**Get all users + * @return if response is success when return users list*/ + suspend fun getUsers(): Either> + + /**Get user's bookings*/ + suspend fun getBookingsByUser(userId: String): Either> + + /**Get bookings in workspace*/ + suspend fun getBookingsByWorkspaces(workspaceId: String): Either> + + /**Booking workspace*/ + suspend fun createBooking(bookingInfo: BookingInfo): Either + + /**Update booking info*/ + suspend fun updateBooking( + bookingInfo: BookingInfo + ): Either + + /**Delete booking*/ + suspend fun deleteBooking( + bookingId: String + ): Either + + /**Subscribe on workspace info updates*/ + suspend fun subscribeOnWorkspaceUpdates(id: String): Flow> + + /**Subscribe on organizers list updates*/ + suspend fun subscribeOnOrganizersList(): Flow>> + + /**Subscribe on bookings list updates*/ + suspend fun subscribeOnBookingsList(workspaceId: String): Flow>> +} \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/api/impl/ApiImpl.kt b/contract/src/commonMain/kotlin/band/effective/office/network/api/impl/ApiImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..c9b05126938211b8c6eefb5f40e210ec6e7dfe95 --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/api/impl/ApiImpl.kt @@ -0,0 +1,101 @@ +package band.effective.office.network.api.impl + +import band.effective.office.network.api.Api +import band.effective.office.network.dto.BookingInfo +import band.effective.office.network.dto.SuccessResponse +import band.effective.office.network.dto.UserDTO +import band.effective.office.network.dto.WorkspaceDTO +import band.effective.office.network.model.Either +import band.effective.office.network.model.ErrorResponse +import band.effective.office.utils.KtorEtherClient +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class ApiImpl : Api { + private val client = KtorEtherClient + private val baseUrl: String = "https://d5do2upft1rficrbubot.apigw.yandexcloud.net" + override suspend fun getWorkspace(id: String): Either = + client.securityResponse("$baseUrl/workspaces") { + url { + parameters.append("id", id) + } + } + + override suspend fun getWorkspaces(tag: String): Either> = + client.securityResponse("$baseUrl/workspaces") { + url { + parameters.append("tag", tag) + } + } + + override suspend fun getUser(id: String): Either = + client.securityResponse("$baseUrl/users") { + url { + parameters.append("id", id) + } + } + + override suspend fun getUsers(): Either> = + client.securityResponse("$baseUrl/users") + + //TODO(Maksim Mishenko): Request not exist in swagger + override suspend fun getBookingsByUser(userId: String): Either> = + Either.Error(ErrorResponse(code = 601, description = "Request not exist in swagger")) + + //TODO(Maksim Mishenko): Request not exist in swagger + override suspend fun getBookingsByWorkspaces(workspaceId: String): Either> = + Either.Error(ErrorResponse(code = 601, description = "Request not exist in swagger")) + + //TODO(Maksim Mishenko): Request not exist in swagger + override suspend fun createBooking(bookingInfo: BookingInfo): Either = + Either.Error(ErrorResponse(code = 601, description = "Request not exist in swagger")) + + //TODO(Maksim Mishenko): Request not exist in swagger + override suspend fun updateBooking( + bookingInfo: BookingInfo + ): Either = + Either.Error(ErrorResponse(code = 601, description = "Request not exist in swagger")) + + //TODO(Maksim Mishenko): Request not exist in swagger + override suspend fun deleteBooking(bookingId: String): Either = + Either.Error(ErrorResponse(code = 601, description = "Request not exist in swagger")) + + //TODO(Maksim Mishenko): Request not exist in swagger + override suspend fun subscribeOnWorkspaceUpdates(id: String): Flow> = + flow { + emit( + Either.Error( + ErrorResponse( + code = 601, + description = "Request not exist in swagger" + ) + ) + ) + } + + //TODO(Maksim Mishenko): Request not exist in swagger + override suspend fun subscribeOnOrganizersList(): Flow>> = + flow { + emit( + Either.Error( + ErrorResponse( + code = 601, + description = "Request not exist in swagger" + ) + ) + ) + } + + //TODO(Maksim Mрегьюлар воркспейсамishenko): Request not exist in swagger + override suspend fun subscribeOnBookingsList(workspaceId: String): Flow>> = + flow { + emit( + Either.Error( + ErrorResponse( + code = 601, + description = "Request not exist in swagger" + ) + ) + ) + } +} \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/api/impl/ApiMock.kt b/contract/src/commonMain/kotlin/band/effective/office/network/api/impl/ApiMock.kt new file mode 100644 index 0000000000000000000000000000000000000000..15b55f1e381e6e8aa872ed0b5f37d49cc98ba49b --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/api/impl/ApiMock.kt @@ -0,0 +1,117 @@ +package band.effective.office.network.api.impl + +import band.effective.office.network.api.Api +import band.effective.office.network.dto.BookingInfo +import band.effective.office.network.dto.SuccessResponse +import band.effective.office.network.dto.UserDTO +import band.effective.office.network.dto.WorkspaceDTO +import band.effective.office.network.model.Either +import band.effective.office.network.model.ErrorResponse +import band.effective.office.utils.MockFactory +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class ApiMock(private val realApi: Api, mockFactory: MockFactory) : Api { + var getRealResponse: Boolean = false + private val workspaces = mockFactory.workspaces() + private val meetingRooms = mockFactory.meetingRooms() + private val users = MutableStateFlow(mockFactory.users()) + private val bookings = MutableStateFlow(mockFactory.bookings()) + private val successResponse = mockFactory.success() + + private fun response(mock: T?, realResponse: Either) = + with(getRealResponse) { + when { + this && !(realResponse.requestNotExist()) -> realResponse + mock == null -> Either.Error(ErrorResponse.getResponse(404)) + realResponse.requestNotExist() -> Either.Success(mock) + else -> Either.Success(mock) + } + } + + private fun Either.requestNotExist() = + this is Either.Error && error.code in 600..699 + + override suspend fun getWorkspace(id: String): Either = response( + mock = (workspaces + meetingRooms).firstOrNull() { it.id == id }, + realResponse = realApi.getWorkspace(id) + ) + + override suspend fun getWorkspaces(tag: String): Either> = + response( + mock = if (tag == "meeting") meetingRooms else workspaces, + realResponse = realApi.getWorkspaces(tag = tag) + ) + + override suspend fun getUser(id: String): Either = response( + mock = users.value.firstOrNull { it.id == id }, + realResponse = realApi.getUser(id) + ) + + override suspend fun getUsers(): Either> = response( + mock = users.value, + realResponse = realApi.getUsers() + ) + + override suspend fun getBookingsByUser(userId: String): Either> = + response( + mock = bookings.value.filter { it.ownerId == userId }, + realResponse = realApi.getBookingsByUser(userId = userId) + ) + + override suspend fun getBookingsByWorkspaces(workspaceId: String): Either> = + response( + mock = bookings.value.filter { it.workspaceId == workspaceId }, + realResponse = realApi.getBookingsByWorkspaces(workspaceId = workspaceId) + ) + + override suspend fun createBooking(bookingInfo: BookingInfo): Either = + response( + mock = successResponse.apply { bookings.update { it + bookingInfo } }, + realResponse = realApi.createBooking(bookingInfo) + ) + + override suspend fun updateBooking( + bookingInfo: BookingInfo + ): Either = response( + mock = successResponse.apply { bookings.update { it.map { element -> if (element.id == bookingInfo.id) bookingInfo else element } } }, + realResponse = realApi.updateBooking(bookingInfo) + ) + + override suspend fun deleteBooking( + bookingId: String + ): Either = response( + mock = successResponse.apply { bookings.update { it.filter { element -> element.id != bookingId } } }, + realResponse = realApi.deleteBooking(bookingId) + ) + + override suspend fun subscribeOnWorkspaceUpdates(id: String): Flow> = + flow { + realApi.subscribeOnWorkspaceUpdates(id).collect { if (getRealResponse) emit(it) } + } + + override suspend fun subscribeOnOrganizersList(): Flow>> = + flow { + coroutineScope { + launch { users.collect { if (!getRealResponse) emit(Either.Success(it)) } } + launch { + realApi.subscribeOnOrganizersList().collect { if (getRealResponse) emit(it) } + } + } + } + + override suspend fun subscribeOnBookingsList(workspaceId: String): Flow>> = + flow { + coroutineScope { + launch { bookings.collect { if (!getRealResponse) emit(Either.Success(it.filter { item -> item.workspaceId == workspaceId })) } } + launch { + realApi.subscribeOnBookingsList(workspaceId) + .collect { if (getRealResponse) emit(it) } + } + } + } +} \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/dto/BookingInfo.kt b/contract/src/commonMain/kotlin/band/effective/office/network/dto/BookingInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8a9a78aea69e0112323b2a08ad49a7267375924 --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/dto/BookingInfo.kt @@ -0,0 +1,10 @@ +package band.effective.office.network.dto + +data class BookingInfo( + val id: String, + val begin: Long, + val end: Long, + val ownerId: String, + val participants: List, + val workspaceId: String +) \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/dto/IntegrationDTO.kt b/contract/src/commonMain/kotlin/band/effective/office/network/dto/IntegrationDTO.kt new file mode 100644 index 0000000000000000000000000000000000000000..32dbc9e4dbd427d71c17b27a25df6fc7f35df417 --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/dto/IntegrationDTO.kt @@ -0,0 +1,6 @@ +package band.effective.office.network.dto + +data class IntegrationDTO( + val name: String, + val value: String +) \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/dto/SuccessResponse.kt b/contract/src/commonMain/kotlin/band/effective/office/network/dto/SuccessResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..6637b077165be8615836f7f26e6f4ffd2c0ac3ef --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/dto/SuccessResponse.kt @@ -0,0 +1,5 @@ +package band.effective.office.network.dto + +data class SuccessResponse( + val status: String +) \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/dto/UserDTO.kt b/contract/src/commonMain/kotlin/band/effective/office/network/dto/UserDTO.kt new file mode 100644 index 0000000000000000000000000000000000000000..9b90c383e4afbbf4bed1d79c762ff78e1062c45c --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/dto/UserDTO.kt @@ -0,0 +1,10 @@ +package band.effective.office.network.dto + +data class UserDTO( + val active: Boolean, + val avatarUrl: String, + val fullName: String, + val id: String, + val integrations: List, + val role: String +) \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/dto/UtilityDTO.kt b/contract/src/commonMain/kotlin/band/effective/office/network/dto/UtilityDTO.kt new file mode 100644 index 0000000000000000000000000000000000000000..292c0f0e7ceeb4e33eed6466a1623165070a5a5d --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/dto/UtilityDTO.kt @@ -0,0 +1,8 @@ +package band.effective.office.network.dto + +data class UtilityDTO( + val count: Int, + val iconUrl: String, + val id: String, + val name: String +) \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/dto/WorkspaceDTO.kt b/contract/src/commonMain/kotlin/band/effective/office/network/dto/WorkspaceDTO.kt new file mode 100644 index 0000000000000000000000000000000000000000..9577d17d5f97566fae9ff16cfcc7b9f3d8e84cd8 --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/dto/WorkspaceDTO.kt @@ -0,0 +1,8 @@ +package band.effective.office.network.dto + +// TODO(Mishnko Maksim): tablet must get events list in workspace +data class WorkspaceDTO( + val id: String, + val name: String, + val utilities: List +) \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/model/Either.kt b/contract/src/commonMain/kotlin/band/effective/office/network/model/Either.kt new file mode 100644 index 0000000000000000000000000000000000000000..62ab0cc42363f0d145dae4ac0546f5ee72b70173 --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/model/Either.kt @@ -0,0 +1,6 @@ +package band.effective.office.network.model + +sealed interface Either { + data class Error(val error: ErrorType) : Either + data class Success(val data: DataType) : Either +} \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/network/model/ErrorResponse.kt b/contract/src/commonMain/kotlin/band/effective/office/network/model/ErrorResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a49037eaa4fd9928ddde16f0c9e213edc373fe6 --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/network/model/ErrorResponse.kt @@ -0,0 +1,15 @@ +package band.effective.office.network.model + +data class ErrorResponse(val code: Int, val description: String) { + companion object { + fun getResponse(code: Int): ErrorResponse { + val description = when (code) { + 404 -> "Not found" + in 400..499 -> "Client error" + in 500..599 -> "Server error" + else -> "Unknown error" + } + return ErrorResponse(code, description) + } + } +} \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEitherPlagin.kt b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEitherPlagin.kt new file mode 100644 index 0000000000000000000000000000000000000000..e1fbfed22fd8ab5b0070e42be0910f6c0abdf86f --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEitherPlagin.kt @@ -0,0 +1,23 @@ +package band.effective.office.utils + +import band.effective.office.network.model.Either +import band.effective.office.network.model.ErrorResponse +import io.ktor.client.plugins.api.createClientPlugin +import io.ktor.utils.io.readUTF8Line +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer + +val KtorEitherPlugin = createClientPlugin("KtorEitherPlugin") { + transformResponseBody { response, content, requestedType -> + if (response.status.value in 200..299) { + Either.Success( + Json.decodeFromString( + serializer(requestedType.kotlinType!!.arguments[1].type!!), + content.readUTF8Line()!! + ) + ) + } else { + Either.Error(ErrorResponse.getResponse(response.status.value)) + } + } +} \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..a6fae3175777506769b21c163a6da1d6b1d669bc --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt @@ -0,0 +1,36 @@ +package band.effective.office.utils + +import band.effective.office.network.model.Either +import band.effective.office.network.model.ErrorResponse +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.put + +object KtorEtherClient { + val httpClient = HttpClient(CIO) { + install(KtorEitherPlugin) + } + + enum class RestMethod { Get, Post, Delete, Put } + + suspend inline fun securityResponse( + urlString: String, + method: RestMethod = RestMethod.Get, + block: HttpRequestBuilder.() -> Unit = {}, + ): Either = + try { + when (method) { + RestMethod.Get -> httpClient.get(urlString, block) + RestMethod.Post -> httpClient.post(urlString, block) + RestMethod.Delete -> httpClient.delete(urlString, block) + RestMethod.Put -> httpClient.put(urlString, block) + }.body() + } catch (e: Exception) { + Either.Error(ErrorResponse(code = 0, description = e.message ?: "Error")) + } +} \ No newline at end of file diff --git a/contract/src/commonMain/kotlin/band/effective/office/utils/MockFactory.kt b/contract/src/commonMain/kotlin/band/effective/office/utils/MockFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..c0870a75c62087038b5becc9d504e719bb9ea416 --- /dev/null +++ b/contract/src/commonMain/kotlin/band/effective/office/utils/MockFactory.kt @@ -0,0 +1,107 @@ +package band.effective.office.utils + +import band.effective.office.network.dto.BookingInfo +import band.effective.office.network.dto.SuccessResponse +import band.effective.office.network.dto.UserDTO +import band.effective.office.network.dto.UtilityDTO +import band.effective.office.network.dto.WorkspaceDTO +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.todayIn +import kotlin.random.Random + +class MockFactory { + private fun getTime(hours: Int = 0, minutes: Int = 0) = + Clock.System.todayIn(TimeZone.currentSystemDefault()).run { + LocalDateTime( + year = year, + monthNumber = monthNumber, + dayOfMonth = dayOfMonth, + hour = hours, + minute = minutes + ).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() + } + + private fun lanUtility() = UtilityDTO( + count = Random.nextInt(0, 20), + iconUrl = "", + id = "", + name = "lan" + ) + + private fun placeUtility() = UtilityDTO( + count = Random.nextInt(0, 20), + iconUrl = "", + id = "", + name = "place" + ) + + private fun tvUtility() = UtilityDTO( + count = 1, + iconUrl = "", + id = "", + name = "place" + ) + + private fun user(name: String, role: String) = UserDTO( + active = Random.nextBoolean(), + avatarUrl = "", + fullName = name, + id = name, + integrations = listOf(), + role = role + ) + + private fun booking(owner: String, start: Pair, finish: Pair, workspace: String) = + BookingInfo( + id = "${Random.nextInt(10000)}", + begin = getTime(start.first, start.second), + end = getTime(finish.first, finish.second), + ownerId = owner, + participants = listOf(), + workspaceId = workspace + ) + + fun workspaces() = listOf() + + fun meetingRooms() = listOf( + WorkspaceDTO( + id = "Sirius", + name = "Sirius", + utilities = listOf(lanUtility(), placeUtility()) + ), + WorkspaceDTO( + id = "Pluto", + name = "Pluto", + utilities = listOf(lanUtility(), placeUtility()) + ), + WorkspaceDTO(id = "Moon", name = "Moon", utilities = listOf(lanUtility(), placeUtility())), + WorkspaceDTO( + id = "Antares", + name = "Antares", + utilities = listOf(lanUtility(), placeUtility()) + ), + WorkspaceDTO( + id = "Sun", + name = "Sun", + utilities = listOf(lanUtility(), placeUtility(), tvUtility()) + ) + ) + + private val names = listOf("Ольга Белозерова", "Матвей Авгуль", "Лилия Акентьева") + + fun users() = names.map { user(it, "ADMIN") } + + fun bookings() = names.mapIndexed { index, name -> + booking( + owner = name, + start = Pair(11 + index, (index % 2) * 30), + finish = Pair(12 + index, (index % 2) * 30), + workspace = "Sirius" + ) + } + + fun success() = SuccessResponse(status = "ok") +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b8f418f92370872667942458503486c9f21d8ee9..9e18d722783aff17ec89c9c4905e518bc5a32703 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,4 +10,5 @@ include(":tabletApp:features:network") include(":tabletApp:features:domain") include(":tabletApp:features:core") include(":tabletApp:features:freeNegotiationsScreen") -include("wheel-picker-compose") \ No newline at end of file +include("wheel-picker-compose") +include("contract") \ No newline at end of file