Skip to content

Commit 4c62c01

Browse files
authored
Merge pull request #86 from DarkXanteR/master
Support ISO DateTime strings in parameters
2 parents 6c8bf3b + 247aa2f commit 4c62c01

File tree

6 files changed

+503
-26
lines changed

6 files changed

+503
-26
lines changed

src/main/kotlin/com/papsign/ktor/openapigen/parameters/parsers/converters/primitive/PrimitiveConverter.kt

+86-6
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ package com.papsign.ktor.openapigen.parameters.parsers.converters.primitive
33
import com.papsign.ktor.openapigen.getKType
44
import com.papsign.ktor.openapigen.parameters.parsers.converters.Converter
55
import com.papsign.ktor.openapigen.parameters.parsers.converters.ConverterSelector
6+
import com.papsign.ktor.openapigen.parameters.util.localDateTimeFormatter
7+
import com.papsign.ktor.openapigen.parameters.util.offsetDateTimeFormatter
8+
import com.papsign.ktor.openapigen.parameters.util.zonedDateTimeFormatter
69
import java.math.BigDecimal
710
import java.math.BigInteger
8-
import java.text.SimpleDateFormat
11+
import java.time.*
12+
import java.time.format.DateTimeFormatter
13+
import java.time.format.DateTimeParseException
914
import java.util.*
1015
import kotlin.reflect.KType
1116

@@ -17,7 +22,7 @@ object PrimitiveConverter : ConverterSelector {
1722
}
1823
}
1924

20-
private val dateFormat = SimpleDateFormat()
25+
// private val dateFormat = SimpleDateFormat()
2126

2227
private val primitiveParsers = mapOf(
2328
primitive { it.toByteOrNull() ?: 0 },
@@ -47,10 +52,85 @@ object PrimitiveConverter : ConverterSelector {
4752
primitive { it.toBoolean() },
4853
primitive<Boolean?> { it.toBoolean() },
4954
// removed temporarily because behavior may not be standard or expected
50-
// primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) ?: Date() },
51-
// primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) },
52-
// primitive { it?.toLongOrNull()?.let(Instant::ofEpochMilli) ?: it?.let(Instant::parse) ?: Instant.now() },
53-
// primitive { it?.toLongOrNull()?.let(Instant::ofEpochMilli) ?: it?.let(Instant::parse) },
55+
56+
primitive {
57+
LocalDate.parse(it, DateTimeFormatter.ISO_DATE)
58+
},
59+
primitive {
60+
try {
61+
LocalDate.parse(it, DateTimeFormatter.ISO_DATE)
62+
} catch(e: DateTimeParseException) {
63+
null
64+
}
65+
},
66+
67+
primitive {
68+
LocalTime.parse(it, DateTimeFormatter.ISO_LOCAL_TIME)
69+
},
70+
primitive {
71+
try {
72+
LocalTime.parse(it, DateTimeFormatter.ISO_LOCAL_TIME)
73+
} catch(e: DateTimeParseException) {
74+
null
75+
}
76+
},
77+
78+
primitive {
79+
OffsetTime.parse(it, DateTimeFormatter.ISO_OFFSET_TIME)
80+
},
81+
primitive {
82+
try {
83+
OffsetTime.parse(it, DateTimeFormatter.ISO_OFFSET_TIME)
84+
} catch(e: DateTimeParseException) {
85+
null
86+
}
87+
},
88+
89+
primitive {
90+
LocalDateTime.parse(it, localDateTimeFormatter)
91+
},
92+
primitive {
93+
try {
94+
LocalDateTime.parse(it, localDateTimeFormatter)
95+
} catch(e: DateTimeParseException) {
96+
null
97+
}
98+
},
99+
100+
primitive {
101+
OffsetDateTime.parse(it, offsetDateTimeFormatter)
102+
},
103+
primitive {
104+
try {
105+
OffsetDateTime.parse(it, offsetDateTimeFormatter)
106+
} catch(e: DateTimeParseException) {
107+
null
108+
}
109+
},
110+
111+
primitive {
112+
ZonedDateTime.parse(it, zonedDateTimeFormatter)
113+
},
114+
primitive {
115+
try {
116+
ZonedDateTime.parse(it, zonedDateTimeFormatter)
117+
} catch(e: DateTimeParseException) {
118+
null
119+
}
120+
},
121+
122+
primitive { it.toLongOrNull()?.let(Instant::ofEpochMilli) ?: Instant.from(offsetDateTimeFormatter.parse(it)) },
123+
primitive {
124+
try {
125+
it.toLongOrNull()?.let(Instant::ofEpochMilli) ?: Instant.from(offsetDateTimeFormatter.parse(it))
126+
} catch(e: DateTimeParseException) {
127+
null
128+
}
129+
},
130+
131+
// primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) ?: Date() },
132+
// primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) },
133+
54134
primitive {
55135
try {
56136
UUID.fromString(it)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.papsign.ktor.openapigen.parameters.util
2+
3+
import java.time.format.DateTimeFormatter
4+
import java.time.format.DateTimeFormatterBuilder
5+
import java.time.temporal.ChronoField
6+
7+
private fun baseDateTimeFormatterBuilder() = DateTimeFormatterBuilder()
8+
.appendPattern("yyyy-MM-dd['T'][ ]HH:mm[:ss]")
9+
.optionalStart()
10+
.appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true)
11+
.optionalEnd()
12+
13+
val localDateTimeFormatter: DateTimeFormatter = baseDateTimeFormatterBuilder().toFormatter()
14+
15+
val offsetDateTimeFormatter: DateTimeFormatter = baseDateTimeFormatterBuilder()
16+
.appendPattern("[xxx][xx][X]")
17+
.toFormatter()
18+
19+
val zonedDateTimeFormatter: DateTimeFormatter = baseDateTimeFormatterBuilder()
20+
.appendPattern("[xxx][xx][X]")
21+
.optionalStart()
22+
.appendLiteral('[')
23+
.optionalEnd()
24+
.optionalStart()
25+
.appendZoneId()
26+
.optionalEnd()
27+
.optionalStart()
28+
.appendLiteral(']')
29+
.optionalEnd()
30+
.toFormatter()

src/main/kotlin/com/papsign/ktor/openapigen/schema/builder/provider/DefaultPrimitiveSchemaProvider.kt

+35-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import com.papsign.ktor.openapigen.schema.builder.SchemaBuilder
1313
import java.io.InputStream
1414
import java.math.BigDecimal
1515
import java.math.BigInteger
16-
import java.time.Instant
16+
import java.time.*
1717
import java.util.*
1818
import kotlin.reflect.KType
1919

@@ -33,9 +33,9 @@ object DefaultPrimitiveSchemaProvider: SchemaBuilderProviderModule, OpenAPIGenMo
3333
)
3434
}
3535

36-
inline operator fun <reified T> invoke(type: DataType, format: DataFormat? = null): Builder {
36+
inline operator fun <reified T> invoke(type: DataType, format: DataFormat? = null, pattern: String? = null, example: T? = null): Builder {
3737
return Builder(
38-
SchemaModel.SchemaModelLitteral<T>(type, format)
38+
SchemaModel.SchemaModelLitteral<T>(type, format, pattern = pattern, example = example)
3939
)
4040
}
4141
}
@@ -74,9 +74,40 @@ object DefaultPrimitiveSchemaProvider: SchemaBuilderProviderModule, OpenAPIGenMo
7474
Builder<BigDecimal>(
7575
DataType.number
7676
),
77+
Builder<LocalDate>(
78+
DataType.string,
79+
DataFormat.date,
80+
example = LocalDate.now()
81+
),
82+
Builder<LocalTime>(
83+
DataType.string,
84+
pattern = "HH:mm:ss",
85+
example = LocalTime.now()
86+
),
87+
Builder<OffsetTime>(
88+
DataType.string,
89+
pattern = "HH:mm:ss+XXX",
90+
example = OffsetTime.now()
91+
),
92+
Builder<LocalDateTime>(
93+
DataType.string,
94+
DataFormat.`date-time`,
95+
example = LocalDateTime.now()
96+
),
97+
Builder<OffsetDateTime>(
98+
DataType.string,
99+
DataFormat.`date-time`,
100+
example = OffsetDateTime.now()
101+
),
102+
Builder<ZonedDateTime>(
103+
DataType.string,
104+
DataFormat.`date-time`,
105+
example = ZonedDateTime.now()
106+
),
77107
Builder<Instant>(
78108
DataType.string,
79-
DataFormat.`date-time`
109+
DataFormat.`date-time`,
110+
example = Instant.now()
80111
),
81112
Builder<Date>(
82113
DataType.string,

src/test/kotlin/TestServer.kt

+82-16
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.papsign.ktor.openapigen.annotations.Response
1515
import com.papsign.ktor.openapigen.annotations.mapping.OpenAPIName
1616
import com.papsign.ktor.openapigen.annotations.parameters.HeaderParam
1717
import com.papsign.ktor.openapigen.annotations.parameters.PathParam
18+
import com.papsign.ktor.openapigen.annotations.parameters.QueryParam
1819
import com.papsign.ktor.openapigen.annotations.properties.description.Description
1920
import com.papsign.ktor.openapigen.annotations.type.`object`.example.ExampleProvider
2021
import com.papsign.ktor.openapigen.annotations.type.`object`.example.WithExample
@@ -39,22 +40,17 @@ import com.papsign.ktor.openapigen.route.path.normal.post
3940
import com.papsign.ktor.openapigen.route.response.respond
4041
import com.papsign.ktor.openapigen.schema.namer.DefaultSchemaNamer
4142
import com.papsign.ktor.openapigen.schema.namer.SchemaNamer
42-
import io.ktor.application.application
43-
import io.ktor.application.call
44-
import io.ktor.application.install
45-
import io.ktor.features.ContentNegotiation
46-
import io.ktor.features.StatusPages
47-
import io.ktor.features.origin
48-
import io.ktor.http.HttpStatusCode
49-
import io.ktor.jackson.jackson
50-
import io.ktor.request.host
51-
import io.ktor.request.port
52-
import io.ktor.response.respond
53-
import io.ktor.response.respondRedirect
54-
import io.ktor.routing.get
55-
import io.ktor.routing.routing
56-
import io.ktor.server.engine.embeddedServer
57-
import io.ktor.server.netty.Netty
43+
import io.ktor.application.*
44+
import io.ktor.features.*
45+
import io.ktor.http.*
46+
import io.ktor.jackson.*
47+
import io.ktor.request.*
48+
import io.ktor.response.*
49+
import io.ktor.routing.*
50+
import io.ktor.server.engine.*
51+
import io.ktor.server.netty.*
52+
import java.time.*
53+
import java.util.*
5854
import kotlin.reflect.KType
5955

6056
object TestServer {
@@ -98,6 +94,9 @@ object TestServer {
9894

9995
enable(SerializationFeature.WRAP_EXCEPTIONS, SerializationFeature.INDENT_OUTPUT)
10096

97+
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
98+
disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
99+
101100
setSerializationInclusion(JsonInclude.Include.NON_NULL)
102101

103102
setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
@@ -273,6 +272,53 @@ object TestServer {
273272
}
274273
}
275274
}
275+
276+
277+
route("datetime") {
278+
route("date") {
279+
get<LocalDateQuery, LocalDateResponse> { params ->
280+
respond(LocalDateResponse(params.date))
281+
}
282+
route("optional") {
283+
get<LocalDateOptionalQuery, LocalDateResponse> { params ->
284+
println(params)
285+
respond(LocalDateResponse(params.date))
286+
}
287+
}
288+
}
289+
route("local-time") {
290+
get<LocalTimeQuery, LocalTimeResponse> { params ->
291+
respond(LocalTimeResponse(params.time))
292+
}
293+
}
294+
route("offset-time") {
295+
get<OffsetTimeQuery, OffsetTimeResponse> { params ->
296+
respond(OffsetTimeResponse(params.time))
297+
}
298+
}
299+
300+
route("local-date-time") {
301+
get<LocalDateTimeQuery, LocalDateTimeResponse> { params ->
302+
respond(LocalDateTimeResponse(params.date))
303+
}
304+
}
305+
route("offset-date-time") {
306+
get<OffsetDateTimeQuery, OffsetDateTimeResponse> { params ->
307+
respond(OffsetDateTimeResponse(params.date))
308+
}
309+
}
310+
route("zoned-date-time") {
311+
get<ZonedDateTimeQuery, ZonedDateTimeResponse> { params ->
312+
println(ZonedDateTime.now())
313+
respond(ZonedDateTimeResponse(params.date))
314+
}
315+
}
316+
route("instant") {
317+
get<InstantQuery, InstantResponse> { params ->
318+
respond(InstantResponse(params.date))
319+
}
320+
}
321+
}
276322
}
277323
}.start(true)
278324
}
@@ -346,4 +392,24 @@ object TestServer {
346392
}
347393

348394
data class APIPrincipal(val a: String, val b: String)
395+
396+
397+
@Request("A LocalDate Request")
398+
data class LocalDateQuery(@QueryParam("LocalDate") val date: LocalDate)
399+
data class LocalDateOptionalQuery(@QueryParam("LocalDate") val date: LocalDate?)
400+
data class LocalDateTimeQuery(@QueryParam("LocalDateTime") val date: LocalDateTime)
401+
data class OffsetDateTimeQuery(@QueryParam("OffsetDateTime") val date: OffsetDateTime)
402+
data class ZonedDateTimeQuery(@QueryParam("OffsetDateTime") val date: ZonedDateTime)
403+
data class InstantQuery(@QueryParam("Instant") val date: Instant)
404+
405+
data class LocalTimeQuery(@QueryParam("LocalTime") val time: LocalTime)
406+
data class OffsetTimeQuery(@QueryParam("OffsetTime") val time: OffsetTime)
407+
408+
data class LocalDateResponse(val date: LocalDate?)
409+
data class LocalDateTimeResponse(val date: LocalDateTime?)
410+
data class OffsetDateTimeResponse(val date: OffsetDateTime?)
411+
data class ZonedDateTimeResponse(val date: ZonedDateTime?)
412+
data class InstantResponse(val instant: Instant)
413+
data class LocalTimeResponse(val time: LocalTime?)
414+
data class OffsetTimeResponse(val time: OffsetTime?)
349415
}

src/test/kotlin/com/papsign/ktor/openapigen/parameters/parsers/Util.kt

+17
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.papsign.ktor.openapigen.parameters.parsers.builders.BuilderFactory
77
import com.papsign.ktor.openapigen.parameters.parsers.builders.BuilderSelector
88
import java.lang.reflect.Array
99
import kotlin.reflect.full.isSuperclassOf
10+
import kotlin.test.assertFails
1011
import kotlin.test.assertNotNull
1112

1213
inline fun <reified T> BuilderSelector<*>.testSelector(
@@ -50,8 +51,24 @@ inline fun <reified T, B: Builder<S>, S> BuilderFactory<B, S>.testSelector(
5051
val builder = buildBuilder(type, explode)
5152
assertNotNull(builder, "BuilderSelector ${javaClass.simpleName} could not be generated for type $type")
5253
val actual = builder.build(key, parseData)
54+
println("$expect = $actual")
5355
if (actual != null) {
5456
assert(T::class.isSuperclassOf(actual::class)) { "Actual class ${actual.javaClass.simpleName} from builder ${builder.javaClass.simpleName} must be subclass of ${T::class.java.simpleName}" }
5557
}
5658
assert(equals(expect, actual as T)) { "Expected ${toStr(expect)}, Actual: ${toStr(actual)}" }
5759
}
60+
61+
62+
inline fun <reified T> BuilderFactory<*, *>.testSelectorFails(
63+
key: String,
64+
parseData: Map<String, List<String>>,
65+
explode: Boolean
66+
) {
67+
val type = getKType<T>()
68+
val builder = buildBuilder(type, explode)
69+
assertNotNull(builder, "BuilderSelector ${javaClass.simpleName} could not be generated for type $type")
70+
assertFails("Expected to fail $parseData") {
71+
builder.build(key, parseData)
72+
}
73+
}
74+

0 commit comments

Comments
 (0)