Skip to content

Commit

Permalink
Merge pull request #86 from DarkXanteR/master
Browse files Browse the repository at this point in the history
Support ISO DateTime strings in parameters
  • Loading branch information
Wicpar committed Feb 28, 2021
2 parents 6c8bf3b + 247aa2f commit 4c62c01
Show file tree
Hide file tree
Showing 6 changed files with 503 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ package com.papsign.ktor.openapigen.parameters.parsers.converters.primitive
import com.papsign.ktor.openapigen.getKType
import com.papsign.ktor.openapigen.parameters.parsers.converters.Converter
import com.papsign.ktor.openapigen.parameters.parsers.converters.ConverterSelector
import com.papsign.ktor.openapigen.parameters.util.localDateTimeFormatter
import com.papsign.ktor.openapigen.parameters.util.offsetDateTimeFormatter
import com.papsign.ktor.openapigen.parameters.util.zonedDateTimeFormatter
import java.math.BigDecimal
import java.math.BigInteger
import java.text.SimpleDateFormat
import java.time.*
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.*
import kotlin.reflect.KType

Expand All @@ -17,7 +22,7 @@ object PrimitiveConverter : ConverterSelector {
}
}

private val dateFormat = SimpleDateFormat()
// private val dateFormat = SimpleDateFormat()

private val primitiveParsers = mapOf(
primitive { it.toByteOrNull() ?: 0 },
Expand Down Expand Up @@ -47,10 +52,85 @@ object PrimitiveConverter : ConverterSelector {
primitive { it.toBoolean() },
primitive<Boolean?> { it.toBoolean() },
// removed temporarily because behavior may not be standard or expected
// primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) ?: Date() },
// primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) },
// primitive { it?.toLongOrNull()?.let(Instant::ofEpochMilli) ?: it?.let(Instant::parse) ?: Instant.now() },
// primitive { it?.toLongOrNull()?.let(Instant::ofEpochMilli) ?: it?.let(Instant::parse) },

primitive {
LocalDate.parse(it, DateTimeFormatter.ISO_DATE)
},
primitive {
try {
LocalDate.parse(it, DateTimeFormatter.ISO_DATE)
} catch(e: DateTimeParseException) {
null
}
},

primitive {
LocalTime.parse(it, DateTimeFormatter.ISO_LOCAL_TIME)
},
primitive {
try {
LocalTime.parse(it, DateTimeFormatter.ISO_LOCAL_TIME)
} catch(e: DateTimeParseException) {
null
}
},

primitive {
OffsetTime.parse(it, DateTimeFormatter.ISO_OFFSET_TIME)
},
primitive {
try {
OffsetTime.parse(it, DateTimeFormatter.ISO_OFFSET_TIME)
} catch(e: DateTimeParseException) {
null
}
},

primitive {
LocalDateTime.parse(it, localDateTimeFormatter)
},
primitive {
try {
LocalDateTime.parse(it, localDateTimeFormatter)
} catch(e: DateTimeParseException) {
null
}
},

primitive {
OffsetDateTime.parse(it, offsetDateTimeFormatter)
},
primitive {
try {
OffsetDateTime.parse(it, offsetDateTimeFormatter)
} catch(e: DateTimeParseException) {
null
}
},

primitive {
ZonedDateTime.parse(it, zonedDateTimeFormatter)
},
primitive {
try {
ZonedDateTime.parse(it, zonedDateTimeFormatter)
} catch(e: DateTimeParseException) {
null
}
},

primitive { it.toLongOrNull()?.let(Instant::ofEpochMilli) ?: Instant.from(offsetDateTimeFormatter.parse(it)) },
primitive {
try {
it.toLongOrNull()?.let(Instant::ofEpochMilli) ?: Instant.from(offsetDateTimeFormatter.parse(it))
} catch(e: DateTimeParseException) {
null
}
},

// primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) ?: Date() },
// primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) },

primitive {
try {
UUID.fromString(it)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.papsign.ktor.openapigen.parameters.util

import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField

private fun baseDateTimeFormatterBuilder() = DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd['T'][ ]HH:mm[:ss]")
.optionalStart()
.appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true)
.optionalEnd()

val localDateTimeFormatter: DateTimeFormatter = baseDateTimeFormatterBuilder().toFormatter()

val offsetDateTimeFormatter: DateTimeFormatter = baseDateTimeFormatterBuilder()
.appendPattern("[xxx][xx][X]")
.toFormatter()

val zonedDateTimeFormatter: DateTimeFormatter = baseDateTimeFormatterBuilder()
.appendPattern("[xxx][xx][X]")
.optionalStart()
.appendLiteral('[')
.optionalEnd()
.optionalStart()
.appendZoneId()
.optionalEnd()
.optionalStart()
.appendLiteral(']')
.optionalEnd()
.toFormatter()
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import com.papsign.ktor.openapigen.schema.builder.SchemaBuilder
import java.io.InputStream
import java.math.BigDecimal
import java.math.BigInteger
import java.time.Instant
import java.time.*
import java.util.*
import kotlin.reflect.KType

Expand All @@ -33,9 +33,9 @@ object DefaultPrimitiveSchemaProvider: SchemaBuilderProviderModule, OpenAPIGenMo
)
}

inline operator fun <reified T> invoke(type: DataType, format: DataFormat? = null): Builder {
inline operator fun <reified T> invoke(type: DataType, format: DataFormat? = null, pattern: String? = null, example: T? = null): Builder {
return Builder(
SchemaModel.SchemaModelLitteral<T>(type, format)
SchemaModel.SchemaModelLitteral<T>(type, format, pattern = pattern, example = example)
)
}
}
Expand Down Expand Up @@ -74,9 +74,40 @@ object DefaultPrimitiveSchemaProvider: SchemaBuilderProviderModule, OpenAPIGenMo
Builder<BigDecimal>(
DataType.number
),
Builder<LocalDate>(
DataType.string,
DataFormat.date,
example = LocalDate.now()
),
Builder<LocalTime>(
DataType.string,
pattern = "HH:mm:ss",
example = LocalTime.now()
),
Builder<OffsetTime>(
DataType.string,
pattern = "HH:mm:ss+XXX",
example = OffsetTime.now()
),
Builder<LocalDateTime>(
DataType.string,
DataFormat.`date-time`,
example = LocalDateTime.now()
),
Builder<OffsetDateTime>(
DataType.string,
DataFormat.`date-time`,
example = OffsetDateTime.now()
),
Builder<ZonedDateTime>(
DataType.string,
DataFormat.`date-time`,
example = ZonedDateTime.now()
),
Builder<Instant>(
DataType.string,
DataFormat.`date-time`
DataFormat.`date-time`,
example = Instant.now()
),
Builder<Date>(
DataType.string,
Expand Down
98 changes: 82 additions & 16 deletions src/test/kotlin/TestServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.papsign.ktor.openapigen.annotations.Response
import com.papsign.ktor.openapigen.annotations.mapping.OpenAPIName
import com.papsign.ktor.openapigen.annotations.parameters.HeaderParam
import com.papsign.ktor.openapigen.annotations.parameters.PathParam
import com.papsign.ktor.openapigen.annotations.parameters.QueryParam
import com.papsign.ktor.openapigen.annotations.properties.description.Description
import com.papsign.ktor.openapigen.annotations.type.`object`.example.ExampleProvider
import com.papsign.ktor.openapigen.annotations.type.`object`.example.WithExample
Expand All @@ -39,22 +40,17 @@ import com.papsign.ktor.openapigen.route.path.normal.post
import com.papsign.ktor.openapigen.route.response.respond
import com.papsign.ktor.openapigen.schema.namer.DefaultSchemaNamer
import com.papsign.ktor.openapigen.schema.namer.SchemaNamer
import io.ktor.application.application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.features.StatusPages
import io.ktor.features.origin
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.request.host
import io.ktor.request.port
import io.ktor.response.respond
import io.ktor.response.respondRedirect
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.jackson.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import java.time.*
import java.util.*
import kotlin.reflect.KType

object TestServer {
Expand Down Expand Up @@ -98,6 +94,9 @@ object TestServer {

enable(SerializationFeature.WRAP_EXCEPTIONS, SerializationFeature.INDENT_OUTPUT)

disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)

setSerializationInclusion(JsonInclude.Include.NON_NULL)

setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
Expand Down Expand Up @@ -273,6 +272,53 @@ object TestServer {
}
}
}


route("datetime") {
route("date") {
get<LocalDateQuery, LocalDateResponse> { params ->
respond(LocalDateResponse(params.date))
}
route("optional") {
get<LocalDateOptionalQuery, LocalDateResponse> { params ->
println(params)
respond(LocalDateResponse(params.date))
}
}
}
route("local-time") {
get<LocalTimeQuery, LocalTimeResponse> { params ->
respond(LocalTimeResponse(params.time))
}
}
route("offset-time") {
get<OffsetTimeQuery, OffsetTimeResponse> { params ->
respond(OffsetTimeResponse(params.time))
}
}

route("local-date-time") {
get<LocalDateTimeQuery, LocalDateTimeResponse> { params ->
respond(LocalDateTimeResponse(params.date))
}
}
route("offset-date-time") {
get<OffsetDateTimeQuery, OffsetDateTimeResponse> { params ->
respond(OffsetDateTimeResponse(params.date))
}
}
route("zoned-date-time") {
get<ZonedDateTimeQuery, ZonedDateTimeResponse> { params ->
println(ZonedDateTime.now())
respond(ZonedDateTimeResponse(params.date))
}
}
route("instant") {
get<InstantQuery, InstantResponse> { params ->
respond(InstantResponse(params.date))
}
}
}
}
}.start(true)
}
Expand Down Expand Up @@ -346,4 +392,24 @@ object TestServer {
}

data class APIPrincipal(val a: String, val b: String)


@Request("A LocalDate Request")
data class LocalDateQuery(@QueryParam("LocalDate") val date: LocalDate)
data class LocalDateOptionalQuery(@QueryParam("LocalDate") val date: LocalDate?)
data class LocalDateTimeQuery(@QueryParam("LocalDateTime") val date: LocalDateTime)
data class OffsetDateTimeQuery(@QueryParam("OffsetDateTime") val date: OffsetDateTime)
data class ZonedDateTimeQuery(@QueryParam("OffsetDateTime") val date: ZonedDateTime)
data class InstantQuery(@QueryParam("Instant") val date: Instant)

data class LocalTimeQuery(@QueryParam("LocalTime") val time: LocalTime)
data class OffsetTimeQuery(@QueryParam("OffsetTime") val time: OffsetTime)

data class LocalDateResponse(val date: LocalDate?)
data class LocalDateTimeResponse(val date: LocalDateTime?)
data class OffsetDateTimeResponse(val date: OffsetDateTime?)
data class ZonedDateTimeResponse(val date: ZonedDateTime?)
data class InstantResponse(val instant: Instant)
data class LocalTimeResponse(val time: LocalTime?)
data class OffsetTimeResponse(val time: OffsetTime?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.papsign.ktor.openapigen.parameters.parsers.builders.BuilderFactory
import com.papsign.ktor.openapigen.parameters.parsers.builders.BuilderSelector
import java.lang.reflect.Array
import kotlin.reflect.full.isSuperclassOf
import kotlin.test.assertFails
import kotlin.test.assertNotNull

inline fun <reified T> BuilderSelector<*>.testSelector(
Expand Down Expand Up @@ -50,8 +51,24 @@ inline fun <reified T, B: Builder<S>, S> BuilderFactory<B, S>.testSelector(
val builder = buildBuilder(type, explode)
assertNotNull(builder, "BuilderSelector ${javaClass.simpleName} could not be generated for type $type")
val actual = builder.build(key, parseData)
println("$expect = $actual")
if (actual != null) {
assert(T::class.isSuperclassOf(actual::class)) { "Actual class ${actual.javaClass.simpleName} from builder ${builder.javaClass.simpleName} must be subclass of ${T::class.java.simpleName}" }
}
assert(equals(expect, actual as T)) { "Expected ${toStr(expect)}, Actual: ${toStr(actual)}" }
}


inline fun <reified T> BuilderFactory<*, *>.testSelectorFails(
key: String,
parseData: Map<String, List<String>>,
explode: Boolean
) {
val type = getKType<T>()
val builder = buildBuilder(type, explode)
assertNotNull(builder, "BuilderSelector ${javaClass.simpleName} could not be generated for type $type")
assertFails("Expected to fail $parseData") {
builder.build(key, parseData)
}
}

Loading

0 comments on commit 4c62c01

Please sign in to comment.