diff --git a/values4k/src/main/kotlin/dev/forkhandles/values/ValueFactory.kt b/values4k/src/main/kotlin/dev/forkhandles/values/ValueFactory.kt index 10f0874..e93d07b 100644 --- a/values4k/src/main/kotlin/dev/forkhandles/values/ValueFactory.kt +++ b/values4k/src/main/kotlin/dev/forkhandles/values/ValueFactory.kt @@ -7,7 +7,9 @@ abstract class ValueFactory, PRIMITIVE : Any>( internal val coerceFn: (PRIMITIVE) -> DOMAIN, private val validation: Validation? = null, internal val parseFn: (String) -> PRIMITIVE, - internal val showFn: (PRIMITIVE) -> String = { it.toString() } + internal val showFn: (PRIMITIVE) -> String = { it.toString() }, + internal val onInvalid: ValueFactory.(PRIMITIVE, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + internal val onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) } ) { internal fun validate(value: PRIMITIVE): DOMAIN { validation?.check(value) @@ -17,21 +19,21 @@ abstract class ValueFactory, PRIMITIVE : Any>( @Deprecated("Use of()", ReplaceWith("of(value)")) operator fun invoke(value: PRIMITIVE): Any = error("invoke() factory method is not to be used for building microtypes - use of() instead!") - open fun parse(value: String) = attempt { validate(parseFn(value)) } + open fun parse(value: String): DOMAIN { + val parsed = attempt({ onParseFailure(value, it) }) { parseFn(value) } + return attempt({ onInvalid(parsed, it) }) { validate(parsed) } + } fun show(value: DOMAIN) = showFn(unwrap(value)) - open fun of(value: PRIMITIVE) = attempt { validate(value) } + open fun of(value: PRIMITIVE) = attempt({ onInvalid(value, it) }) { validate(value) } fun unwrap(value: DOMAIN) = value.value - private fun attempt(value: () -> T) = try { + private fun attempt(onError: (Exception) -> Nothing, value: () -> T) = try { value() } catch (e: Exception) { - throw IllegalArgumentException( - this::class.java.name.substringBeforeLast('$') + - ": " + e::class.java.name + " " + e.localizedMessage - ) + onError(e) } } @@ -46,3 +48,10 @@ fun , PRIMITIVE : Any> ValueFactory fun , PRIMITIVE : Any> ValueFactory.showList(values: List) = values.map(::show) + +internal fun ValueFactory<*, *>.defaultOnInvalid(e: Exception): Nothing { + throw IllegalArgumentException( + this::class.java.name.substringBeforeLast('$') + + ": " + e::class.java.name + " " + e.localizedMessage + ) +} diff --git a/values4k/src/main/kotlin/dev/forkhandles/values/factories.kt b/values4k/src/main/kotlin/dev/forkhandles/values/factories.kt index d1167c4..3e9c0f5 100644 --- a/values4k/src/main/kotlin/dev/forkhandles/values/factories.kt +++ b/values4k/src/main/kotlin/dev/forkhandles/values/factories.kt @@ -35,25 +35,33 @@ private val base36Alphabet get() = "^[0-9A-Z=]+$".toRegex() open class StringValueFactory>( fn: (String) -> DOMAIN, validation: Validation? = null, - showFn: (String) -> String = { it } -) : ValueFactory(fn, validation, { it }, showFn) + showFn: (String) -> String = { it }, + onInvalid: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = onInvalid, +) : ValueFactory(fn, validation, { it }, showFn, onInvalid, onParseFailure) open class NonEmptyStringValueFactory>( fn: (String) -> DOMAIN, - showFn: (String) -> String = { it } -) : ValueFactory(fn, 1.minLength, { it }, showFn) + showFn: (String) -> String = { it }, + onInvalid: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = onInvalid, +) : ValueFactory(fn, 1.minLength, { it }, showFn, onInvalid, onParseFailure) open class NonBlankStringValueFactory>( fn: (String) -> DOMAIN, - showFn: (String) -> String = { it } -) : ValueFactory(fn, 1.minLength.let { v -> { v(it.trim()) } }, { it }, showFn) + showFn: (String) -> String = { it }, + onInvalid: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = onInvalid, +) : ValueFactory(fn, 1.minLength.let { v -> { v(it.trim()) } }, { it }, showFn, onInvalid, onParseFailure) open class Base64StringValueFactory>( fn: (String) -> DOMAIN, validation: Validation = { true }, parseFn: (String) -> String = { it }, showFn: (String) -> String = { it }, -) : ValueFactory(fn, rfcBase64Alphabet::matches.and(validation), parseFn, showFn) { + onInvalid: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = onInvalid, +) : ValueFactory(fn, rfcBase64Alphabet::matches.and(validation), parseFn, showFn, onInvalid, onParseFailure) { private val encoder = Base64.getEncoder() fun encode(value: ByteArray) = encoder.encodeToString(value).let(coerceFn) } @@ -63,21 +71,27 @@ open class Base36StringValueFactory>( validation: Validation = { true }, parseFn: (String) -> String = { it }, showFn: (String) -> String = { it }, -) : ValueFactory(fn, base36Alphabet::matches.and(validation), parseFn, showFn) + onInvalid: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = onInvalid, +) : ValueFactory(fn, base36Alphabet::matches.and(validation), parseFn, showFn, onInvalid, onParseFailure) open class Base32StringValueFactory>( fn: (String) -> DOMAIN, validation: Validation = { true }, parseFn: (String) -> String = { it }, showFn: (String) -> String = { it }, -) : ValueFactory(fn, rfcBase32Alphabet::matches.and(validation), parseFn, showFn) + onInvalid: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = onInvalid, +) : ValueFactory(fn, rfcBase32Alphabet::matches.and(validation), parseFn, showFn, onInvalid, onParseFailure) open class Base16StringValueFactory>( fn: (String) -> DOMAIN, validation: Validation = { true }, parseFn: (String) -> String = { it }, showFn: (String) -> String = { it }, -) : ValueFactory(fn, rfcBase16Alphabet::matches.and(validation), parseFn, showFn) { + onInvalid: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = onInvalid, +) : ValueFactory(fn, rfcBase16Alphabet::matches.and(validation), parseFn, showFn, onInvalid, onParseFailure) { // Source: https://stackoverflow.com/a/9855338/1253613 private val base16Chars = "0123456789ABCDEF".toCharArray() fun encode(bytes: ByteArray): DOMAIN { @@ -92,106 +106,150 @@ open class Base16StringValueFactory>( } open class CharValueFactory>( - fn: (Char) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, String::first) + fn: (Char) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(Char, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, String::first, onInvalid = onInvalid, onParseFailure = onParseFailure) open class IntValueFactory>( - fn: (Int) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, String::toInt) + fn: (Int) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(Int, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, String::toInt, onInvalid = onInvalid, onParseFailure = onParseFailure) open class LongValueFactory>( - fn: (Long) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, String::toLong) + fn: (Long) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(Long, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, String::toLong, onInvalid = onInvalid, onParseFailure = onParseFailure) open class DoubleValueFactory>( - fn: (Double) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, String::toDouble) + fn: (Double) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(Double, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, String::toDouble, onInvalid = onInvalid, onParseFailure = onParseFailure) open class FloatValueFactory>( - fn: (Float) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, String::toFloat) + fn: (Float) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(Float, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, String::toFloat, onInvalid = onInvalid, onParseFailure = onParseFailure) open class BooleanValueFactory>( - fn: (Boolean) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, String::toBoolean) + fn: (Boolean) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(Boolean, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, String::toBoolean, onInvalid = onInvalid, onParseFailure = onParseFailure) open class BigIntegerValueFactory>( - fn: (BigInteger) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, String::toBigInteger) + fn: (BigInteger) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(BigInteger, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, String::toBigInteger, onInvalid = onInvalid, onParseFailure = onParseFailure) open class BigDecimalValueFactory>( - fn: (BigDecimal) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, String::toBigDecimal) + fn: (BigDecimal) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(BigDecimal, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, String::toBigDecimal, onInvalid = onInvalid, onParseFailure = onParseFailure) open class UUIDValueFactory>( - fn: (UUID) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, UUID::fromString) + fn: (UUID) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(UUID, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, UUID::fromString, onInvalid = onInvalid, onParseFailure = onParseFailure) open class URLValueFactory>( - fn: (URL) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, ::URL) + fn: (URL) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(URL, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, ::URL, onInvalid = onInvalid, onParseFailure = onParseFailure) open class DurationValueFactory>( - fn: (Duration) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, { Duration.parse(it) }) + fn: (Duration) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(Duration, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { Duration.parse(it) }, onInvalid = onInvalid, onParseFailure = onParseFailure) open class InstantValueFactory>( fn: (Instant) -> DOMAIN, validation: Validation? = null, - fmt: DateTimeFormatter = ISO_INSTANT -) : ValueFactory(fn, validation, { fmt.parse(it, Instant::from) }, fmt::format) + fmt: DateTimeFormatter = ISO_INSTANT, + onInvalid: ValueFactory.(Instant, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { fmt.parse(it, Instant::from) }, fmt::format, onInvalid, onParseFailure) open class LocalDateValueFactory>( fn: (LocalDate) -> DOMAIN, validation: Validation? = null, - fmt: DateTimeFormatter = ISO_LOCAL_DATE -) : ValueFactory(fn, validation, { LocalDate.parse(it, fmt) }, fmt::format) + fmt: DateTimeFormatter = ISO_LOCAL_DATE, + onInvalid: ValueFactory.(LocalDate, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { LocalDate.parse(it, fmt) }, fmt::format, onInvalid, onParseFailure) open class LocalTimeValueFactory>( fn: (LocalTime) -> DOMAIN, validation: Validation? = null, - fmt: DateTimeFormatter = ISO_LOCAL_TIME -) : ValueFactory(fn, validation, { LocalTime.parse(it, fmt) }, fmt::format) + fmt: DateTimeFormatter = ISO_LOCAL_TIME, + onInvalid: ValueFactory.(LocalTime, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { LocalTime.parse(it, fmt) }, fmt::format, onInvalid, onParseFailure) open class LocalDateTimeValueFactory>( fn: (LocalDateTime) -> DOMAIN, validation: Validation? = null, - fmt: DateTimeFormatter = ISO_LOCAL_DATE_TIME -) : ValueFactory(fn, validation, { LocalDateTime.parse(it, fmt) }, fmt::format) + fmt: DateTimeFormatter = ISO_LOCAL_DATE_TIME, + onInvalid: ValueFactory.(LocalDateTime, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { LocalDateTime.parse(it, fmt) }, fmt::format, onInvalid, onParseFailure) open class OffsetDateTimeValueFactory>( fn: (OffsetDateTime) -> DOMAIN, validation: Validation? = null, - fmt: DateTimeFormatter = ISO_OFFSET_DATE_TIME -) : ValueFactory(fn, validation, { OffsetDateTime.parse(it, fmt) }, fmt::format) + fmt: DateTimeFormatter = ISO_OFFSET_DATE_TIME, + onInvalid: ValueFactory.(OffsetDateTime, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { OffsetDateTime.parse(it, fmt) }, fmt::format, onInvalid, onParseFailure) open class OffsetTimeValueFactory>( fn: (OffsetTime) -> DOMAIN, validation: Validation? = null, - fmt: DateTimeFormatter = ISO_OFFSET_TIME -) : ValueFactory(fn, validation, { OffsetTime.parse(it, fmt) }, fmt::format) + fmt: DateTimeFormatter = ISO_OFFSET_TIME, + onInvalid: ValueFactory.(OffsetTime, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { OffsetTime.parse(it, fmt) }, fmt::format, onInvalid, onParseFailure) open class PeriodValueFactory>( fn: (Period) -> DOMAIN, - validation: Validation? = null -) : ValueFactory(fn, validation, { Period.parse(it) }) + validation: Validation? = null, + onInvalid: ValueFactory.(Period, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { Period.parse(it) }, Period::toString, onInvalid, onParseFailure) open class YearMonthValueFactory>( fn: (YearMonth) -> DOMAIN, validation: Validation? = null, - fmt: DateTimeFormatter = ofPattern("yyyy-MM") -) : ValueFactory(fn, validation, { YearMonth.parse(it, fmt) }, fmt::format) + fmt: DateTimeFormatter = ofPattern("yyyy-MM"), + onInvalid: ValueFactory.(YearMonth, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { YearMonth.parse(it, fmt) }, fmt::format, onInvalid, onParseFailure) open class YearValueFactory>( fn: (Year) -> DOMAIN, validation: Validation? = null, - fmt: DateTimeFormatter = ofPattern("yyyy") -) : ValueFactory(fn, validation, { Year.parse(it, fmt) }, fmt::format) + fmt: DateTimeFormatter = ofPattern("yyyy"), + onInvalid: ValueFactory.(Year, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { Year.parse(it, fmt) }, fmt::format, onInvalid, onParseFailure) open class ZonedDateTimeValueFactory>( fn: (ZonedDateTime) -> DOMAIN, validation: Validation? = null, - fmt: DateTimeFormatter = ISO_ZONED_DATE_TIME -) : ValueFactory(fn, validation, { ZonedDateTime.parse(it, fmt) }, fmt::format) + fmt: DateTimeFormatter = ISO_ZONED_DATE_TIME, + onInvalid: ValueFactory.(ZonedDateTime, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { ZonedDateTime.parse(it, fmt) }, fmt::format, onInvalid, onParseFailure) open class FileValueFactory>( - fn: (File) -> DOMAIN, validation: Validation? = null -) : ValueFactory(fn, validation, { File(it) }) + fn: (File) -> DOMAIN, validation: Validation? = null, + onInvalid: ValueFactory.(File, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, + onParseFailure: ValueFactory.(String, Exception) -> Nothing = { _, e -> defaultOnInvalid(e) }, +) : ValueFactory(fn, validation, { File(it) }, onInvalid = onInvalid, onParseFailure = onParseFailure) diff --git a/values4k/src/test/kotlin/dev/forkhandles/values/ValueFactoryTest.kt b/values4k/src/test/kotlin/dev/forkhandles/values/ValueFactoryTest.kt index b15df1e..f270997 100644 --- a/values4k/src/test/kotlin/dev/forkhandles/values/ValueFactoryTest.kt +++ b/values4k/src/test/kotlin/dev/forkhandles/values/ValueFactoryTest.kt @@ -7,6 +7,7 @@ import com.natpryce.hamkrest.throws import dev.forkhandles.result4k.Failure import dev.forkhandles.result4k.Success import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows class ValueFactoryTest { @@ -114,4 +115,20 @@ class ValueFactoryTest { assertThat(MyIntValue.showList(MyIntValue.of(123), MyIntValue.of(456)), equalTo(listOf("123", "456"))) } + @Test + fun customOnInvalid() { + val exception = assertThrows { + MyIntValue.parse("-1") + } + assertThat(exception.message, equalTo("Value (-1) must be greater than 0")) + } + + @Test + fun customOnFailedParse() { + val exception = assertThrows { + MyIntValue.parse("cat") + } + assertThat(exception.message, equalTo("Value (cat) must be an integer")) + } + } diff --git a/values4k/src/test/kotlin/dev/forkhandles/values/ValueTest.kt b/values4k/src/test/kotlin/dev/forkhandles/values/ValueTest.kt index 6a71bf9..6331eb6 100644 --- a/values4k/src/test/kotlin/dev/forkhandles/values/ValueTest.kt +++ b/values4k/src/test/kotlin/dev/forkhandles/values/ValueTest.kt @@ -16,7 +16,10 @@ class MyValue private constructor(value: String) : StringValue(value) { } class MyIntValue private constructor(value: Int) : IntValue(value) { - companion object : IntValueFactory(::MyIntValue, { it > 0 }) + companion object : IntValueFactory(::MyIntValue, { it > 0 }, + onInvalid = { value, e -> throw IllegalArgumentException("Value ($value) must be greater than 0", e) }, + onParseFailure = { value, e -> throw IllegalArgumentException("Value ($value) must be an integer", e) } + ) } class HiddenValue private constructor(value: String) : StringValue(value, masking = hidden('t')) {