Skip to content

Commit

Permalink
introduce oneTimeOnly parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
avan1235 committed Aug 14, 2024
1 parent 7660462 commit cbb18c7
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ package `in`.procyk.shin.component
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.childContext
import com.arkivanov.decompose.value.Value
import `in`.procyk.shin.ui.util.createHttpClient
import `in`.procyk.shin.model.ShortenedProtocol
import `in`.procyk.shin.shared.*
import `in`.procyk.shin.shared.Option.None
import `in`.procyk.shin.shared.Option.Some
import `in`.procyk.shin.ui.util.createHttpClient
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.*
Expand All @@ -30,6 +31,8 @@ interface MainComponent : Component {

val customPrefixVisible: Value<Boolean>

val oneTimeOnly: Value<Boolean>

val expirationDate: Value<LocalDate>

val expirationDateVisible: Value<Boolean>
Expand All @@ -50,6 +53,8 @@ interface MainComponent : Component {

fun onCustomPrefixVisibleChange(visible: Boolean)

fun onOneTimeOnlyChange(oneTimeOnly: Boolean)

fun onExpirationDateChange(expirationDate: LocalDate?): Boolean

fun onExpirationDateVisibleChange(visible: Boolean)
Expand Down Expand Up @@ -77,7 +82,8 @@ class MainComponentImpl(

private val httpClient: HttpClient = createHttpClient()

override val favourites: FavouritesComponent = FavouritesComponentImpl(appContext, componentContext.childContext(key = "Favourites"))
override val favourites: FavouritesComponent =
FavouritesComponentImpl(appContext, componentContext.childContext(key = "Favourites"))

private val _extraElementsVisible = MutableStateFlow(false)
override val extraElementsVisible: Value<Boolean> = _extraElementsVisible.asValue()
Expand All @@ -88,6 +94,9 @@ class MainComponentImpl(
private val _customPrefixVisible = MutableStateFlow(false)
override val customPrefixVisible: Value<Boolean> = _customPrefixVisible.asValue()

private val _oneTimeOnly = MutableStateFlow(false)
override val oneTimeOnly: Value<Boolean> = _oneTimeOnly.asValue()

private val _expirationDate = MutableStateFlow(tomorrow)
override val expirationDate: Value<LocalDate> = _expirationDate.asValue()

Expand Down Expand Up @@ -121,6 +130,10 @@ class MainComponentImpl(
_customPrefixVisible.update { visible }
}

override fun onOneTimeOnlyChange(oneTimeOnly: Boolean) {
_oneTimeOnly.update { oneTimeOnly }
}

override fun onExpirationDateChange(expirationDate: LocalDate?): Boolean = when {
expirationDate == null -> {
val updatedDate = tomorrow
Expand Down Expand Up @@ -172,9 +185,10 @@ class MainComponentImpl(
httpClient.requestShortenedUrl(
url = _fullUrl.value,
shortenedProtocol = _protocol.value,
customPrefix = _customPrefix.value.takeIfExtraElementsVisibleAnd(customPrefixVisible),
expirationDate = _expirationDate.value.takeIfExtraElementsVisibleAnd(expirationDateVisible),
redirectType = _redirectType.value.takeIfExtraElementsVisibleAnd(redirectTypeVisible),
customPrefix = _customPrefix.takeIfExtraElementsVisibleAnd(customPrefixVisible),
oneTimeOnly = _oneTimeOnly.takeIfExtraElementsVisible(),
expirationDate = _expirationDate.takeIfExtraElementsVisibleAnd(expirationDateVisible),
redirectType = _redirectType.takeIfExtraElementsVisibleAnd(redirectTypeVisible),
onResponse = { code, response ->
when (code) {
HttpStatusCode.OK -> {
Expand All @@ -193,14 +207,19 @@ class MainComponentImpl(
}

@Suppress("NOTHING_TO_INLINE")
private inline fun <T> T.takeIfExtraElementsVisibleAnd(value: Value<Boolean>): T? =
takeIf { _extraElementsVisible.value && value.value }
private inline fun <T : Any> StateFlow<T>.takeIfExtraElementsVisibleAnd(value: Value<Boolean>): T? =
this.value.takeIf { _extraElementsVisible.value && value.value }

@Suppress("NOTHING_TO_INLINE")
private inline fun <T : Any> StateFlow<T>.takeIfExtraElementsVisible(): T? =
this.value.takeIf { _extraElementsVisible.value }
}

private suspend inline fun HttpClient.requestShortenedUrl(
url: String,
shortenedProtocol: ShortenedProtocol,
customPrefix: String?,
oneTimeOnly: Boolean?,
expirationDate: LocalDate?,
redirectType: RedirectType?,
onResponse: (HttpStatusCode, String) -> Unit,
Expand All @@ -209,7 +228,7 @@ private suspend inline fun HttpClient.requestShortenedUrl(
try {
val expirationAt = expirationDate?.plus(1, DateTimeUnit.DAY)
?.atStartOfDayIn(TimeZone.currentSystemDefault())
val shorten = Shorten(shortenedProtocol.buildUrl(url), customPrefix, expirationAt, redirectType)
val shorten = Shorten(shortenedProtocol.buildUrl(url), customPrefix, oneTimeOnly, expirationAt, redirectType)
val response = post(ShortenPath) {
contentType(ContentType.Application.Cbor)
setBody(ShinCbor.encodeToByteArray(shorten))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ private fun ExpandableSettings(
modifier = Modifier.fillMaxWidth(),
)
}
ExpandableSetting(
name = "One-Time Only",
visible = component.oneTimeOnly,
isVertical = isVertical,
fillMaxWidth = true,
onVisibleChange = component::onOneTimeOnlyChange,
)
ExpandableSetting(
name = "Expiration Date",
visible = component.expirationDateVisible,
Expand Down Expand Up @@ -185,15 +192,13 @@ private inline val RedirectType.presentableName: String
}

@Composable
private inline fun ExpandableSetting(
private fun ExpandableSetting(
name: String,
visible: Value<Boolean>,
isVertical: Boolean,
fillMaxWidth: Boolean,
noinline onVisibleChange: (Boolean) -> Unit,
crossinline content:
@Composable
() -> Unit,
onVisibleChange: (Boolean) -> Unit,
content: (@Composable () -> Unit)? = null,
) {
Column(
modifier = when {
Expand All @@ -218,13 +223,16 @@ private inline fun ExpandableSetting(
onCheckedChange = onVisibleChange,
)
}
if (content == null) return@Column

val alpha by animateFloatAsState(if (contentVisible) 1f else 0f)
Box(
modifier = Modifier
.alpha(alpha)
.applyIf(fillMaxWidth && isVertical) { fillMaxWidth() }
.applyIf(!fillMaxWidth || !isVertical) { sizeIn(maxWidth = 270.dp) }
.applyIf(!contentVisible) { height(1.dp) },
.applyIf(!contentVisible) { height(0.dp) }
.applyIf(contentVisible) { padding(8.dp) },
) {
content()
}
Expand Down
2 changes: 1 addition & 1 deletion iosApp/Configuration/Config.xcconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
TEAM_ID=
BUNDLE_ID=in.procyk.shin
BUNDLE_ID=in.procyk.shin.test6
APP_NAME=shin
2 changes: 1 addition & 1 deletion iosApp/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "${TEAM_ID}";
DEVELOPMENT_TEAM = ;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
Expand Down
15 changes: 0 additions & 15 deletions server/src/main/kotlin/in/procyk/shin/Routes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ internal fun Application.installRoutes(): Routing = routing {
val service by inject<ShortUrlService>()
val dotenv by inject<Dotenv>()
val redirectBaseUrl = dotenv.env<String>("REDIRECT_BASE_URL")
GlobalScope.launch {
deleteExpiredUrlsEvery(1.hours, service)
}
postBody(ShortenPath) {
val shorten = call.receive<Shorten>()
handleShorten(service, redirectBaseUrl, shorten)
Expand All @@ -36,18 +33,6 @@ internal fun Application.installRoutes(): Routing = routing {
}
}

private suspend inline fun deleteExpiredUrlsEvery(
duration: Duration,
service: ShortUrlService,
) {
coroutineScope {
while (isActive) {
service.deleteExpiredUrls()
delay(duration)
}
}
}

private suspend inline fun PipelineContext<Unit, ApplicationCall>.handleShorten(
service: ShortUrlService,
redirectBaseUrl: String,
Expand Down
4 changes: 4 additions & 0 deletions server/src/main/kotlin/in/procyk/shin/db/ShortUrl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ internal object ShortUrls : IdTable<String>() {

val url = text("url")
val expirationAt = timestamp("expiration_at").nullable()
val oneTimeOnly = bool("one_time_only").default(false)
val active = bool("active").default(true)
val redirectType = customEnumeration(
name = "redirect_type",
sql = "redirect_type",
Expand All @@ -28,6 +30,8 @@ internal class ShortUrl(id: EntityID<String>) : Entity<String>(id) {

var url by ShortUrls.url
var expirationAt by ShortUrls.expirationAt
var oneTimeOnly by ShortUrls.oneTimeOnly
var active by ShortUrls.active
var redirectType by ShortUrls.redirectType
var usageCount by ShortUrls.usageCount
}
Expand Down
41 changes: 23 additions & 18 deletions server/src/main/kotlin/in/procyk/shin/service/ShortUrlService.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package `in`.procyk.shin.service

import `in`.procyk.shin.db.ShortUrl
import `in`.procyk.shin.db.ShortUrls
import `in`.procyk.shin.shared.RedirectType
import `in`.procyk.shin.shared.Shorten
import io.ktor.http.*
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNotNull
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.koin.core.module.Module
import java.net.URI
Expand All @@ -23,8 +18,6 @@ internal interface ShortUrlService {
suspend fun findShortenedUrl(shortenedId: String): ShortenedUrl?

suspend fun increaseShortenedUrlUsageCount(shortenedId: String)

suspend fun deleteExpiredUrls()
}

internal fun Module.singleShortUrlService() {
Expand All @@ -34,6 +27,7 @@ internal fun Module.singleShortUrlService() {
private class ShortUrlServiceImpl : ShortUrlService {
override suspend fun findOrCreateShortenedId(shorten: Shorten): String? {
val shortened = shorten.createShortenedIdentifier() ?: return null
val oneTimeOnly = shorten.oneTimeOnly
val expirationAt = shorten.expirationAt
return newSuspendedTransaction txn@{
for (count in shortened.takeCounts) {
Expand All @@ -43,22 +37,31 @@ private class ShortUrlServiceImpl : ShortUrlService {
null -> ShortUrl.new(shortId) {
this.url = shortened.url
this.expirationAt = expirationAt
this.oneTimeOnly = oneTimeOnly ?: false
this.active = true
this.redirectType = RedirectType.from(shorten.redirectType)
this.usageCount = 0
}.let { return@txn shortId }

shortened.url -> {
val prevExpirationAt = existing.expirationAt
when {
prevExpirationAt == null -> return@txn shortId
prevExpirationAt == null -> {}

expirationAt == null || expirationAt > prevExpirationAt -> {
existing.expirationAt = expirationAt
return@txn shortId
}

else -> return@txn shortId
else -> {}
}

existing.active = true

val prevOneTimeOnly = existing.oneTimeOnly
if (prevOneTimeOnly && (oneTimeOnly == null || !oneTimeOnly)) {
existing.oneTimeOnly = false
}
return@txn shortId
}

else -> continue
Expand All @@ -69,22 +72,24 @@ private class ShortUrlServiceImpl : ShortUrlService {
}

override suspend fun findShortenedUrl(shortenedId: String): ShortenedUrl? = newSuspendedTransaction {
ShortUrl.findById(shortenedId)
val shortUrl = ShortUrl.findById(shortenedId)
when {
shortUrl == null || !shortUrl.active -> null
shortUrl.oneTimeOnly -> shortUrl.also { it.active = false }
shortUrl.expirationAt.let { exp -> exp != null && exp <= Clock.System.now() } ->
null.also { shortUrl.active = false }

else -> shortUrl
}
}?.let {
ShortenedUrl(it.url, it.redirectType)
}

override suspend fun increaseShortenedUrlUsageCount(shortenedId: String) = newSuspendedTransaction txn@{
val shortenedUrl = ShortUrl.findById(shortenedId) ?: return@txn
println("increase for ${shortenedUrl.id}")
shortenedUrl.usageCount += 1
}

override suspend fun deleteExpiredUrls() {
val now = Clock.System.now()
newSuspendedTransaction {
ShortUrls.deleteWhere { (expirationAt.isNotNull()) and (expirationAt.lessEq(now)) }
}
}
}

data class ShortenedUrl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum class RedirectType {
class Shorten(
val url: String,
val customPrefix: String? = null,
val oneTimeOnly: Boolean? = null,
val expirationAt: Instant? = null,
val redirectType: RedirectType? = null,
)
Expand Down

0 comments on commit cbb18c7

Please sign in to comment.