params.kt (6199B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2024-2025 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 package tech.libeufin.common 21 22 import io.ktor.http.* 23 import kotlin.math.min 24 import java.util.* 25 26 fun Parameters.expect(name: String): String 27 = get(name) ?: throw badRequest("Missing '$name' parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) 28 fun Parameters.int(name: String): Int? 29 = get(name)?.run { toIntOrNull() ?: throw paramsMalformed("Param '$name' not a number") } 30 fun Parameters.expectInt(name: String): Int 31 = int(name) ?: throw badRequest("Missing '$name' number parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) 32 fun Parameters.long(name: String): Long? 33 = get(name)?.run { toLongOrNull() ?: throw paramsMalformed("Param '$name' not a number") } 34 fun Parameters.expectLong(name: String): Long 35 = long(name) ?: throw badRequest("Missing '$name' number parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) 36 fun Parameters.uuid(name: String): UUID? 37 = get(name)?.run { 38 try { 39 UUID.fromString(this) 40 } catch (e: Exception) { 41 throw paramsMalformed("Param '$name' not an UUID") 42 } 43 } 44 fun Parameters.expectUuid(name: String): UUID 45 = uuid(name) ?: throw badRequest("Missing '$name' UUID parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) 46 fun Parameters.payto(name: String): Payto? 47 = get(name)?.run { 48 try { 49 Payto.parse(this) 50 } catch (e: Exception) { 51 throw paramsMalformed("Param '$name' not a valid payto") 52 } 53 } 54 fun Parameters.expectPayto(name: String): Payto 55 = payto(name) ?: throw badRequest("Missing '$name' payto parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) 56 fun Parameters.amount(name: String): TalerAmount? 57 = get(name)?.run { 58 try { 59 TalerAmount(this) 60 } catch (e: Exception) { 61 throw paramsMalformed("Param '$name' not a taler amount") 62 } 63 } 64 65 data class PageParams( 66 val limit: Int, val offset: Long 67 ) { 68 companion object { 69 fun extract(params: Parameters): PageParams { 70 val legacy_limit_value = params.int("delta") 71 val new_limit_value = params.int("limit") 72 if (legacy_limit_value != null && new_limit_value != null && legacy_limit_value != new_limit_value) 73 throw paramsMalformed("Param 'limit' cannot be used with param 'delta'") 74 75 val legacy_offset_value = params.long("start") 76 val new_offset_value = params.long("offset") 77 if (legacy_offset_value != null && new_offset_value != null && legacy_offset_value != new_offset_value) 78 throw paramsMalformed("Param 'offset' cannot be used with param 'start'") 79 80 val limit: Int = new_limit_value ?: legacy_limit_value ?: -20 81 if (limit == 0) throw paramsMalformed("Param 'limit' must be non-zero") 82 else if (limit > MAX_PAGE_SIZE) throw paramsMalformed("Param 'limit' must be <= ${MAX_PAGE_SIZE}") 83 // The sign of 'limit' only selects the direction, so the negative 84 // side needs the same bound. Note that abs(Int.MIN_VALUE) silently 85 // returns Int.MIN_VALUE, so an unbounded negative limit would reach 86 // SQL as a negative LIMIT. 87 else if (limit < -MAX_PAGE_SIZE) throw paramsMalformed("Param 'limit' must be >= ${-MAX_PAGE_SIZE}") 88 val offset: Long = new_offset_value ?: legacy_offset_value ?: if (limit >= 0) 0L else Long.MAX_VALUE 89 if (offset < 0) throw paramsMalformed("Param 'offset' must be a positive number") 90 91 return PageParams(limit, offset) 92 } 93 } 94 } 95 96 data class TransferParams( 97 val page: PageParams, val status: TransferStatusState? 98 ) { 99 companion object { 100 private val names = TransferStatusState.entries.map { it.name } 101 private val names_fmt = names.joinToString() 102 fun extract(params: Parameters): TransferParams { 103 val status = params["status"]?.let { 104 if (!names.contains(it)) { 105 throw paramsMalformed("Param 'status' must be one of $names_fmt") 106 } 107 TransferStatusState.valueOf(it) 108 } 109 return TransferParams(PageParams.extract(params), status) 110 } 111 } 112 } 113 114 data class PollingParams( 115 val timeout_ms: Long 116 ) { 117 companion object { 118 fun extract(params: Parameters): PollingParams { 119 val legacy_value = params.long("long_poll_ms") 120 val new_value = params.long("timeout_ms") 121 if (legacy_value != null && new_value != null && legacy_value != new_value) 122 throw paramsMalformed("Param 'timeout_ms' cannot be used with param 'long_poll_ms'") 123 val timeout_ms: Long = min(new_value ?: legacy_value ?: 0, MAX_TIMEOUT_MS) 124 if (timeout_ms < 0) throw paramsMalformed("Param 'timeout_ms' must be a positive number") 125 return PollingParams(timeout_ms) 126 } 127 } 128 } 129 130 data class HistoryParams( 131 val page: PageParams, val polling: PollingParams 132 ) { 133 companion object { 134 fun extract(params: Parameters): HistoryParams { 135 return HistoryParams(PageParams.extract(params), PollingParams.extract(params)) 136 } 137 } 138 } 139 140 data class AccountCheckParams( 141 val account: Payto 142 ) { 143 companion object { 144 fun extract(params: Parameters): AccountCheckParams { 145 val account = params.expectPayto("account") 146 return AccountCheckParams(account) 147 } 148 } 149 }