Merge branch 'develop' of https://github.com/finocd2la/TSUDesk into feature/schedule
# Conflicts: # feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleApi.kt # feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/model/ScheduleEntity.kt
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
package ru.fincode.tsudesk.core.network
|
package ru.fincode.tsudesk.core.network
|
||||||
|
|
||||||
object NetworkConstants {
|
object Constants {
|
||||||
const val BASE_URL = "https://tulsu.ru/schedule/queries/"
|
const val BASE_URL = "https://tulsu.ru/schedule/queries/"
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,58 @@
|
|||||||
package ru.fincode.tsudesk.core.network
|
package ru.fincode.tsudesk.core.network
|
||||||
|
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
import retrofit2.Response
|
||||||
import ru.fincode.tsudesk.core.network.model.NetworkError
|
import ru.fincode.tsudesk.core.network.model.NetworkError
|
||||||
|
import ru.fincode.tsudesk.core.network.model.NetworkMeta
|
||||||
import ru.fincode.tsudesk.core.network.model.NetworkResult
|
import ru.fincode.tsudesk.core.network.model.NetworkResult
|
||||||
|
import ru.fincode.tsudesk.core.network.model.NetworkResult.Success
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
suspend inline fun <T> safeApiCall(
|
suspend inline fun <T> apiCall(
|
||||||
crossinline block: suspend () -> T
|
crossinline block: suspend () -> Response<T>
|
||||||
): NetworkResult<T> {
|
): NetworkResult<T> {
|
||||||
|
val startedAt = System.currentTimeMillis()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
NetworkResult.Success(block())
|
val response = block()
|
||||||
|
val finishedAt = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val raw = response.raw()
|
||||||
|
val meta = NetworkMeta(
|
||||||
|
startedAtMillis = startedAt,
|
||||||
|
finishedAtMillis = finishedAt,
|
||||||
|
sentAtMillis = raw.sentRequestAtMillis,
|
||||||
|
receivedAtMillis = raw.receivedResponseAtMillis
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body()
|
||||||
|
if (body != null) {
|
||||||
|
Success(data = body, meta = meta)
|
||||||
|
} else {
|
||||||
|
NetworkResult.Error(
|
||||||
|
NetworkError.Unknown(
|
||||||
|
throwable = NullPointerException("Response body is null"),
|
||||||
|
meta = meta
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NetworkResult.Error(NetworkError.Http(code = response.code(), meta = meta))
|
||||||
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
NetworkResult.Error(t.toNetworkError())
|
val meta = NetworkMeta(
|
||||||
|
startedAtMillis = startedAt,
|
||||||
|
finishedAtMillis = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
NetworkResult.Error(t.toNetworkError(meta))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Throwable.toNetworkError(): NetworkError = when (this) {
|
fun Throwable.toNetworkError(meta: NetworkMeta): NetworkError = when (this) {
|
||||||
is SocketTimeoutException -> NetworkError.Timeout
|
is SocketTimeoutException -> NetworkError.Timeout(meta)
|
||||||
is IOException -> NetworkError.NoInternet
|
is IOException -> NetworkError.NoInternet(meta)
|
||||||
is HttpException -> NetworkError.Http(code())
|
is HttpException -> NetworkError.Http(code(), meta)
|
||||||
else -> NetworkError.Unknown(this)
|
else -> NetworkError.Unknown(this, meta)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,30 @@
|
|||||||
package ru.fincode.tsudesk.core.network
|
package ru.fincode.tsudesk.core.network.di
|
||||||
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import ru.fincode.tsudesk.core.network.interceptor.DebugInterceptor
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import ru.fincode.tsudesk.core.network.interceptor.DebugInterceptor
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object NetworkModule {
|
object OkHttpModule {
|
||||||
|
|
||||||
private const val TIMEOUT = 30L
|
private const val TIMEOUT = 30L
|
||||||
val logging = HttpLoggingInterceptor().apply {
|
|
||||||
level = HttpLoggingInterceptor.Level.BODY
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideOkHttpClient(): OkHttpClient =
|
fun provideOkHttpClient(
|
||||||
OkHttpClient.Builder()
|
): OkHttpClient {
|
||||||
|
return OkHttpClient.Builder()
|
||||||
.connectTimeout(TIMEOUT, TimeUnit.SECONDS)
|
.connectTimeout(TIMEOUT, TimeUnit.SECONDS)
|
||||||
.readTimeout(TIMEOUT, TimeUnit.SECONDS)
|
.readTimeout(TIMEOUT, TimeUnit.SECONDS)
|
||||||
.writeTimeout(TIMEOUT, TimeUnit.SECONDS)
|
.writeTimeout(TIMEOUT, TimeUnit.SECONDS)
|
||||||
.retryOnConnectionFailure(true)
|
.retryOnConnectionFailure(true)
|
||||||
.addInterceptor(DebugInterceptor())
|
.addInterceptor(DebugInterceptor())
|
||||||
.build()
|
.build()
|
||||||
|
}
|
||||||
@Provides
|
|
||||||
fun provideRetrofit(
|
|
||||||
provider: RetrofitProvider,
|
|
||||||
client: OkHttpClient
|
|
||||||
): Retrofit = provider.process(NetworkConstants.BASE_URL, client)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package ru.fincode.tsudesk.core.network.di
|
||||||
|
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import ru.fincode.tsudesk.core.network.Constants
|
||||||
|
import ru.fincode.tsudesk.core.network.RetrofitProvider
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object RetrofitModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideRetrofit(
|
||||||
|
provider: RetrofitProvider,
|
||||||
|
client: OkHttpClient
|
||||||
|
): Retrofit =
|
||||||
|
provider.process(Constants.BASE_URL, client)
|
||||||
|
}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
package ru.fincode.tsudesk.core.network.model
|
package ru.fincode.tsudesk.core.network.model
|
||||||
|
|
||||||
sealed class NetworkError {
|
sealed class NetworkError(open val meta: NetworkMeta) {
|
||||||
object NoInternet : NetworkError()
|
|
||||||
object Timeout : NetworkError()
|
data class NoInternet(override val meta: NetworkMeta) : NetworkError(meta)
|
||||||
data class Http(val code: Int) : NetworkError()
|
data class Timeout(override val meta: NetworkMeta) : NetworkError(meta)
|
||||||
data class Unknown(val throwable: Throwable) : NetworkError()
|
|
||||||
|
data class Http(
|
||||||
|
val code: Int,
|
||||||
|
override val meta: NetworkMeta
|
||||||
|
) : NetworkError(meta)
|
||||||
|
|
||||||
|
data class Unknown(
|
||||||
|
val throwable: Throwable,
|
||||||
|
override val meta: NetworkMeta
|
||||||
|
) : NetworkError(meta)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ru.fincode.tsudesk.core.network.model
|
||||||
|
|
||||||
|
data class NetworkMeta(
|
||||||
|
val startedAtMillis: Long,
|
||||||
|
val finishedAtMillis: Long,
|
||||||
|
val sentAtMillis: Long? = null,
|
||||||
|
val receivedAtMillis: Long? = null
|
||||||
|
)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package ru.fincode.tsudesk.core.network.model
|
package ru.fincode.tsudesk.core.network.model
|
||||||
|
|
||||||
sealed class NetworkResult<out T> {
|
sealed class NetworkResult<out T> {
|
||||||
data class Success<T>(val data: T) : NetworkResult<T>()
|
data class Success<T>(val data: T, val meta: NetworkMeta) : NetworkResult<T>()
|
||||||
data class Error(val error: NetworkError) : NetworkResult<Nothing>()
|
data class Error(val error: NetworkError) : NetworkResult<Nothing>()
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
package ru.fincode.tsudesk.core.network.model
|
package ru.fincode.tsudesk.core.network.model
|
||||||
|
|
||||||
inline fun <T, R> NetworkResult<T>.map(
|
inline fun <T, R> NetworkResult<T>.map(
|
||||||
transform: (T) -> R
|
transform: (T, NetworkMeta) -> R
|
||||||
): NetworkResult<R> = when (this) {
|
): NetworkResult<R> =
|
||||||
is NetworkResult.Success -> NetworkResult.Success(transform(data))
|
when (val source = this) {
|
||||||
is NetworkResult.Error -> this
|
|
||||||
|
is NetworkResult.Success -> NetworkResult.Success(
|
||||||
|
data = transform(source.data, source.meta),
|
||||||
|
meta = source.meta
|
||||||
|
)
|
||||||
|
|
||||||
|
is NetworkResult.Error -> NetworkResult.Error(source.error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
package ru.fincode.tsudesk.feature.schedule.data.datasource
|
package ru.fincode.tsudesk.feature.schedule.data.datasource
|
||||||
|
|
||||||
import ru.fincode.tsudesk.core.network.model.NetworkResult
|
import ru.fincode.tsudesk.core.network.model.NetworkResult
|
||||||
import ru.fincode.tsudesk.core.network.safeApiCall
|
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleApi
|
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto.ScheduleSearchField.GROUP_P
|
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto.ScheduleSearchField.PREP
|
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto
|
import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class ScheduleRemoteDataSource @Inject constructor(
|
interface ScheduleRemoteDataSource {
|
||||||
private val api: ScheduleApi
|
suspend fun loadScheduleByGroup(number: String): NetworkResult<ScheduleDto>
|
||||||
) {
|
suspend fun loadScheduleByTeacher(name: String): NetworkResult<ScheduleDto>
|
||||||
suspend fun loadScheduleByGroup(number: String): NetworkResult<ScheduleDto> =
|
|
||||||
safeApiCall { api.getSchedule(GROUP_P, number) }
|
|
||||||
|
|
||||||
suspend fun loadScheduleByTeacher(name: String): NetworkResult<ScheduleDto> =
|
|
||||||
safeApiCall { api.getSchedule(PREP, name) }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
package ru.fincode.tsudesk.feature.schedule.data.datasource
|
|
||||||
|
|
||||||
import ru.fincode.tsudesk.core.network.model.NetworkResult
|
|
||||||
import ru.fincode.tsudesk.core.network.model.map
|
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.mapper.ScheduleDtoToDomainMapper
|
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
|
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.repository.ScheduleRepository
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class ScheduleRemoteDataSourceImpl @Inject constructor(
|
|
||||||
private val remote: ScheduleRemoteDataSource,
|
|
||||||
private val mapper: ScheduleDtoToDomainMapper
|
|
||||||
) : ScheduleRepository {
|
|
||||||
|
|
||||||
override suspend fun loadScheduleByGroup(number: String): NetworkResult<ScheduleEntity> =
|
|
||||||
remote.loadScheduleByGroup(number).map(mapper::invoke)
|
|
||||||
|
|
||||||
override suspend fun loadScheduleByTeacher(name: String): NetworkResult<ScheduleEntity> =
|
|
||||||
remote.loadScheduleByTeacher(name).map(mapper::invoke)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package ru.fincode.tsudesk.feature.schedule.data.mapper
|
package ru.fincode.tsudesk.feature.schedule.data.mapper
|
||||||
|
|
||||||
|
import ru.fincode.tsudesk.core.network.model.NetworkMeta
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto
|
import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto
|
import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.model.LessonEntity
|
import ru.fincode.tsudesk.feature.schedule.domain.model.LessonEntity
|
||||||
@@ -8,8 +9,11 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
class ScheduleDtoToDomainMapper @Inject constructor() {
|
class ScheduleDtoToDomainMapper @Inject constructor() {
|
||||||
|
|
||||||
operator fun invoke(dto: ScheduleDto): ScheduleEntity =
|
operator fun invoke(dto: ScheduleDto, meta: NetworkMeta): ScheduleEntity =
|
||||||
ScheduleEntity(lessons = dto.map(::mapLesson))
|
ScheduleEntity(
|
||||||
|
lessons = dto.map(::mapLesson),
|
||||||
|
timestamp = meta.receivedAtMillis ?: meta.finishedAtMillis
|
||||||
|
)
|
||||||
|
|
||||||
private fun mapLesson(item: LessonDto): LessonEntity =
|
private fun mapLesson(item: LessonDto): LessonEntity =
|
||||||
LessonEntity(
|
LessonEntity(
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
package ru.fincode.tsudesk.feature.schedule.data.remote
|
package ru.fincode.tsudesk.feature.schedule.data.remote
|
||||||
|
|
||||||
|
import retrofit2.Response
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto
|
import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleApiContract.Path.GET_SCHEDULE_METHOD
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto.*
|
import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleApiContract.Query.SEARCH_FIELD
|
||||||
|
import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleApiContract.Query.SEARCH_VALUE
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto
|
import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto
|
||||||
|
|
||||||
interface ScheduleApi {
|
interface ScheduleApi {
|
||||||
|
|
||||||
/**
|
@GET(GET_SCHEDULE_METHOD)
|
||||||
* Расписание по номеру группы.
|
|
||||||
* Пример: search_field=GROUP_P&search_value=220631
|
|
||||||
* https://tulsu.ru/schedule/queries/GetSchedule.php?search_field=GROUP_P&search_value=220631
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Расписание по ФИО преподавателя (строкой).
|
|
||||||
* Пример: search_field=PREP&search_value=Набродова Ирина Николаевна
|
|
||||||
*
|
|
||||||
* Важно: Retrofit сам URL-энкодит параметр search_value
|
|
||||||
* https://tulsu.ru/schedule/queries/GetDates.php?search_value=%D0%9D%D0%B0%D0%B1%D1%80%D0%BE%D0%B4%D0%BE%D0%B2%D0%B0%20%D0%98%D1%80%D0%B8%D0%BD%D0%B0%20%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%BD%D0%B0
|
|
||||||
*/
|
|
||||||
@GET("GetSchedule.php")
|
|
||||||
suspend fun getSchedule(
|
suspend fun getSchedule(
|
||||||
@Query("search_field") searchField: String, @Query("search_value") searchValue: String
|
@Query(SEARCH_FIELD) searchField: String,
|
||||||
): ScheduleDto
|
@Query(SEARCH_VALUE) searchValue: String
|
||||||
|
): Response<ScheduleDto>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package ru.fincode.tsudesk.feature.schedule.data.remote
|
||||||
|
|
||||||
|
object ScheduleApiContract {
|
||||||
|
|
||||||
|
object Path {
|
||||||
|
const val GET_SCHEDULE_METHOD = "GetSchedule.php"
|
||||||
|
}
|
||||||
|
|
||||||
|
object Query {
|
||||||
|
const val SEARCH_FIELD = "search_field"
|
||||||
|
const val SEARCH_VALUE = "search_value"
|
||||||
|
|
||||||
|
const val FIELD_GROUP = "GROUP_P"
|
||||||
|
const val FIELD_TEACHER = "PREP"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ru.fincode.tsudesk.feature.schedule.data.remote
|
||||||
|
|
||||||
|
import ru.fincode.tsudesk.core.network.model.NetworkResult
|
||||||
|
import ru.fincode.tsudesk.core.network.apiCall
|
||||||
|
import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleRemoteDataSource
|
||||||
|
import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ScheduleRemoteDataSourceImpl @Inject constructor(
|
||||||
|
private val api: ScheduleApi
|
||||||
|
) : ScheduleRemoteDataSource {
|
||||||
|
|
||||||
|
override suspend fun loadScheduleByGroup(number: String): NetworkResult<ScheduleDto> =
|
||||||
|
apiCall {
|
||||||
|
api.getSchedule(
|
||||||
|
searchField = ScheduleApiContract.Query.FIELD_GROUP,
|
||||||
|
searchValue = number
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadScheduleByTeacher(name: String): NetworkResult<ScheduleDto> =
|
||||||
|
apiCall {
|
||||||
|
api.getSchedule(
|
||||||
|
searchField = ScheduleApiContract.Query.FIELD_TEACHER,
|
||||||
|
searchValue = name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package ru.fincode.tsudesk.feature.schedule.di
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleRemoteDataSource
|
||||||
|
import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleRemoteDataSourceImpl
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
abstract class ScheduleRemoteDataSourceModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindScheduleRemoteDataSource(
|
||||||
|
impl: ScheduleRemoteDataSourceImpl
|
||||||
|
): ScheduleRemoteDataSource
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package ru.fincode.tsudesk.feature.schedule.domain.model
|
package ru.fincode.tsudesk.feature.schedule.domain.model
|
||||||
|
|
||||||
data class ScheduleEntity(
|
data class ScheduleEntity(
|
||||||
val lessons: List<LessonEntity>
|
val lessons: List<LessonEntity>,
|
||||||
|
val timestamp: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user