Skip to content

Commit

Permalink
Clean up application and comment primary interfaces
Browse files Browse the repository at this point in the history
Signed-off-by: Yurii Surzhykov <yuriisurzhykov@gmail.com>
  • Loading branch information
yuriisurzhykov committed May 28, 2024
1 parent cab07cc commit 08f2b19
Show file tree
Hide file tree
Showing 20 changed files with 110 additions and 70 deletions.
3 changes: 1 addition & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
android:theme="@style/Theme.PursTest"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".ui.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.PursTest">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.yuriisurzhykov.purs
package com.github.yuriisurzhykov.purs.data

import androidx.room.Database
import androidx.room.RoomDatabase
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.github.yuriisurzhykov.purs
package com.github.yuriisurzhykov.purs.di

import android.content.Context
import androidx.room.Room
import com.github.yuriisurzhykov.purs.core.MergeStrategy
import com.github.yuriisurzhykov.purs.core.RequestResponseMergeStrategy
import com.github.yuriisurzhykov.purs.core.RequestResult
import com.github.yuriisurzhykov.purs.data.PursDatabase
import com.github.yuriisurzhykov.purs.data.cache.GetLocationId
import com.github.yuriisurzhykov.purs.data.cache.LocationCacheDataSource
import com.github.yuriisurzhykov.purs.data.cache.LocationDao
Expand All @@ -18,7 +19,7 @@ import com.github.yuriisurzhykov.purs.data.repository.LocationWorkingHoursCloudM
import com.github.yuriisurzhykov.purs.domain.chain.AddMissingDaysUseCase
import com.github.yuriisurzhykov.purs.domain.chain.MergeCrossDayTimeSlotsUseCase
import com.github.yuriisurzhykov.purs.domain.chain.MergeTimeSlotsUseCase
import com.github.yuriisurzhykov.purs.domain.chain.SortMissingDaysUseCase
import com.github.yuriisurzhykov.purs.domain.chain.SortWorkingDaysUseCase
import com.github.yuriisurzhykov.purs.domain.chain.WorkingHourChainProcessor
import com.github.yuriisurzhykov.purs.domain.mapper.LocationCacheToDomainMapper
import com.github.yuriisurzhykov.purs.domain.mapper.StringToDayOfWeekMapper
Expand Down Expand Up @@ -98,11 +99,11 @@ object PursAppModule {
fun provideWorkingHourChainProcessor(
mergeCrossDayTimeSlots: MergeCrossDayTimeSlotsUseCase,
addMissingDaysUseCase: AddMissingDaysUseCase,
sortMissingDaysUseCase: SortMissingDaysUseCase
sortWorkingDaysUseCase: SortWorkingDaysUseCase
): WorkingHourChainProcessor = WorkingHourChainProcessor.Base(
mergeCrossDayTimeSlots,
addMissingDaysUseCase,
sortMissingDaysUseCase
sortWorkingDaysUseCase
)

@Provides
Expand All @@ -115,12 +116,12 @@ object PursAppModule {

@Provides
@Singleton
fun provideMergeCrossDayTimeSlotsUseCase(sortMissingDaysUseCase: SortMissingDaysUseCase): MergeCrossDayTimeSlotsUseCase =
MergeCrossDayTimeSlotsUseCase.Base(sortMissingDaysUseCase)
fun provideMergeCrossDayTimeSlotsUseCase(sortWorkingDaysUseCase: SortWorkingDaysUseCase): MergeCrossDayTimeSlotsUseCase =
MergeCrossDayTimeSlotsUseCase.Base(sortWorkingDaysUseCase)

@Provides
@Singleton
fun provideSortMissingDaysUseCase(): SortMissingDaysUseCase = SortMissingDaysUseCase.Base()
fun provideSortMissingDaysUseCase(): SortWorkingDaysUseCase = SortWorkingDaysUseCase.Base()

@Provides
@Singleton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.yuriisurzhykov.purs
package com.github.yuriisurzhykov.purs.ui

import android.os.Bundle
import androidx.activity.ComponentActivity
Expand All @@ -7,9 +7,7 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.github.yuriisurzhykov.purs.location.details.LocationDetails
import com.github.yuriisurzhykov.purs.location.uikit.theme.PursTestTheme
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -22,25 +20,9 @@ class MainActivity : ComponentActivity() {
setContent {
PursTestTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
LocationDetails(modifier = Modifier.padding(innerPadding))
}
}
}
}
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
LocationDetails()
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
PursTestTheme {
Greeting("Android")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package com.github.yuriisurzhykov.purs.data.cache

import javax.inject.Inject

/**
* An interface to provide a location id to persist data. The logic to provide the location if
* moved to this interface because currently we have only one location at the moment, but if
* it will be decided to show more than one location, the new logic will be implemented.
* */
interface GetLocationId {

fun locationId(): Long
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

/**
* Cache data source for location details.
* */
interface LocationCacheDataSource {

suspend fun fetchLocation(): Flow<RequestResult<LocationWithWorkingHours>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@ interface LocationCloudDataSource {
) : LocationCloudDataSource {
override suspend fun fetchLocation(): Flow<RequestResult<LocationCloud>> {
return flow {
// Start progress
emit(RequestResult.InProgress())
try {
// Fetch location details from the cloud
val locationResponse = locationService.fetchLocation()
if (locationResponse.isSuccessful) {
// Map body to whether success response or error if body is empty or null
val responseBody = locationResponse.body()
if (responseBody != null) {
emit(RequestResult.Success(responseBody))
} else {
emit(RequestResult.Error(data = null, error = null))
}
} else {
// if response is not successful then map error to error response
val error = ServerError(
locationResponse.code(),
locationResponse.errorBody()?.string().orEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import com.github.yuriisurzhykov.purs.data.cloud.model.LocationCloud
import retrofit2.Response
import retrofit2.http.GET

/**
* `LocationService` is a service interface to talk to the cloud to fetch the location data.
* */
interface LocationService {

// https://purs-demo-bucket-test.s3.us-west-2.amazonaws.com/location.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import com.github.yuriisurzhykov.purs.data.cache.model.LocationWithWorkingHours
import com.github.yuriisurzhykov.purs.data.cloud.LocationCloudDataSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

Expand All @@ -35,15 +33,11 @@ interface LocationRepository {
response.map { cacheDataSource.persistLocation(it) }
}

// Produce initial state that is loading
val initial =
flowOf<RequestResult<LocationWithWorkingHours>>(RequestResult.InProgress())

// Combine two data sources together with the strategy based on cloud and cache results
val combinedSources = cachedResponse.combine(cloudResponse) { cache, cloud ->
sourceMergeStrategy.merge(cache, cloud)
}
return merge(initial, combinedSources)
return combinedSources
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import com.github.yuriisurzhykov.purs.domain.model.WorkingDay
import java.time.DayOfWeek
import javax.inject.Inject

/**
* In the result list of working days, some days may be missing. So this interface
* appends all missing days to the collection and returns the result.
* */
interface AddMissingDaysUseCase : ProcessWorkingHoursCollection {

class Base @Inject constructor() : AddMissingDaysUseCase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ import com.github.yuriisurzhykov.purs.domain.model.WorkingDay
import java.time.LocalTime
import javax.inject.Inject

interface MergeCrossDayTimeSlotsUseCase : ProcessWorkingHoursCollection{
/**
* The chain processor for merging cross-day time slots. This class is responsible for merging
* working days that contain time slots that cross days. For example, if the starting time of
* the first time slot is 20:00 and the ending time is 24:00 but the starting time of next day
* is 00:00 and the ending time is 04:00, then the first time slot will be merged with the
* next day's time slot. So the merged time slots will be: 20:00-04:00. And will be removed from
* the next schedule day.
* */
interface MergeCrossDayTimeSlotsUseCase : ProcessWorkingHoursCollection {

class Base @Inject constructor(
private val sortMissingDaysUseCase: SortMissingDaysUseCase
private val sortWorkingDaysUseCase: SortWorkingDaysUseCase
) : MergeCrossDayTimeSlotsUseCase {

override fun process(collection: Collection<WorkingDay>): Collection<WorkingDay> {
if (collection.isEmpty() || collection.size == 1) return collection

val mergedTimeSlots = mutableSetOf<WorkingDay>()
val sortedTimeSlots = sortMissingDaysUseCase.process(collection).iterator()
val sortedTimeSlots = sortWorkingDaysUseCase.process(collection).iterator()

// Get the first time slot
var currentSlot = sortedTimeSlots.next()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import com.github.yuriisurzhykov.purs.domain.model.TimeSlot
import com.github.yuriisurzhykov.purs.domain.model.WorkingDay
import javax.inject.Inject

/**
* This interface defines a use case for merging time slots in a collection of working days that
* overlap each other. For example, time slots: 8:00-15:00 and 10:00-11:00 are considered overlapping
* time slots. To this interface merges all such time slots into one time slot: 8:00-15:00.
* */
interface MergeTimeSlotsUseCase {

fun mergeTimeSlots(workDay: WorkingDay): WorkingDay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package com.github.yuriisurzhykov.purs.domain.chain

import com.github.yuriisurzhykov.purs.domain.model.WorkingDay

/**
* Chain processor for the list of working hours.
* */
interface ProcessWorkingHoursCollection {

fun process(collection: Collection<WorkingDay>): Collection<WorkingDay>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package com.github.yuriisurzhykov.purs.domain.chain
import com.github.yuriisurzhykov.purs.domain.model.WorkingDay
import javax.inject.Inject

interface SortMissingDaysUseCase : ProcessWorkingHoursCollection {
/**
* Chain processor for the list of working hours collection. It sorts all working days by day of week
* based on the day name and then by start time.
* */
interface SortWorkingDaysUseCase : ProcessWorkingHoursCollection {

class Base @Inject constructor() : SortMissingDaysUseCase {
class Base @Inject constructor() : SortWorkingDaysUseCase {
override fun process(collection: Collection<WorkingDay>): Collection<WorkingDay> {
val daysOfWeek = listOf("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN")
val dayToIndex = daysOfWeek.withIndex().associate { it.value to it.index }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.github.yuriisurzhykov.purs.domain.chain

import com.github.yuriisurzhykov.purs.domain.model.WorkingDay
import javax.inject.Inject

/**
* The base interface for processing working hours collection. This is the primary
* class that contains all other processing chains and iterate over the chain to process
* the working hours collection.
* */
interface WorkingHourChainProcessor : ProcessWorkingHoursCollection {

class Base(
class Base @Inject constructor(
private vararg val processors: ProcessWorkingHoursCollection
) : WorkingHourChainProcessor {
override fun process(collection: Collection<WorkingDay>): Collection<WorkingDay> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,21 @@ interface BuildCurrentLocationStatusUseCase {
): LocationStatus {
val currentDate = LocalDate.now()
val currentTime = LocalTime.now()
// Get current schedule for the day
val currentSchedule = workingDays.toList()[currentDate.dayOfWeek.value - 1]
// Get next schedule for the current day or next day
val nextSchedule: Pair<String, TimeSlot>? =
findNextWorkingDay(workingDays.toList(), currentDate, currentTime)

// Return location status for whether it is open or closed now
return findCurrentOpenStatus(currentSchedule, nextSchedule!!, currentTime)
?: findClosedStatus(currentTime, nextSchedule)
}

/**
* Returns the location status if the location is closed currently. It looks for next
* schedule and returns [LocationStatus] based if it opens within 24 hours or not.
* */
private fun findClosedStatus(
currentTime: LocalTime,
nextSchedule: Pair<String, TimeSlot>
Expand All @@ -44,11 +51,16 @@ interface BuildCurrentLocationStatusUseCase {
}
}

/**
* Returns the location status if the location is open currently. It looks for next
* schedule and returns [LocationStatus] based if it REopens within 24 hours or not.
* */
private fun findCurrentOpenStatus(
workingDay: WorkingDay,
nextWorkingDay: Pair<String, TimeSlot>,
currentTime: LocalTime
): LocationStatus? {
// Looking for schedule time slot that applied to the current time
val currentOpenSchedule: TimeSlot? = workingDay.scheduleList.find { timeSlot ->
if (timeSlot.endTime < timeSlot.startTime) {
timeSlot.startTime.isBefore(currentTime)
Expand All @@ -59,6 +71,8 @@ interface BuildCurrentLocationStatusUseCase {
return if (currentOpenSchedule != null) {
val timeDifference =
currentTime.until(currentOpenSchedule.endTime, ChronoUnit.MINUTES)
// If the location closes within 24 hours, return the location status
// that it closing soon. Otherwise, return the location status that it opens
return if (timeDifference <= 60) {
val nextOpenTime = nextWorkingDay.second.startTime
val reopenTimeDifference = currentTime.until(nextOpenTime, ChronoUnit.HOURS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

/**
* The use case to build a list of working hours for a location. Operates using flow
* to emit [RequestResult].
* */
interface BuildWorkingHoursListUseCase {

fun workingHours(): Flow<RequestResult<Location>>
Expand All @@ -28,10 +32,14 @@ interface BuildWorkingHoursListUseCase {
val repositoryResult = repository.fetchLocationDetails()
.map { requestResult ->
requestResult
// Map the cache to domain model
.map { cache -> mapper.map(cache) }
// Process the working hours using the chain processor, to process
// all edge cases.
.map { location ->
location.copy(workingDays = processor.process(location.workingDays))
}
// Get the current status of the location.
.map { location ->
location.copy(status = buildLocationStatus.currentStatus(location.workingDays))
}
Expand Down

This file was deleted.

Loading

0 comments on commit 08f2b19

Please sign in to comment.