Compare commits

...

107 Commits

Author SHA1 Message Date
f2786fec08 major: v4 alpha (#505) 2023-09-04 04:22:46 +00:00
904cc6816d fix(deps): update kotestversion to v5.7.1 (#501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-03 21:54:20 +00:00
a06821a910 fix(deps): update dependency org.slf4j:slf4j-api to v2.0.9 (#499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-03 19:34:31 +00:00
a9dffcf8a5 fix(deps): update dependency org.slf4j:slf4j-simple to v2.0.9 (#500)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-03 17:08:11 +00:00
548b1ff4bc fix(deps): update kotestversion to v5.7.0 (#498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-02 01:20:46 +00:00
6f85ee476a fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-datetime to v0.4.1 (#497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-01 08:02:24 +00:00
326bbb4419 fix(deps): update ktor to v2.3.4 (#496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-31 19:30:07 +00:00
438eb52dc8 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.24.2 (#495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-26 01:50:52 +00:00
1fd59db4e1 fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.9.10 (#494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 19:09:49 +00:00
238e372460 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.9.10 (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 17:12:05 +00:00
473e2c22af chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.9.10 (#493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 13:30:22 +00:00
7fb7e688ac fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.6.0 (#491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-22 19:10:33 +00:00
9f2c27aa0f fix(deps): update dependency com.google.protobuf:protobuf-java to v3.24.1 (#490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-18 21:11:22 +00:00
dc762d1d3e chore(deps): update dependency gradle to v8.3 (#489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 08:22:47 +00:00
b039803246 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.24.0 (#488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-09 06:42:32 +00:00
6ec240d7cd fix(deps): update ktor to v2.3.3 (#487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-01 17:30:05 +00:00
dd57347b90 chore(deps): update dependency gradle to v8.2.1 (#486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-10 16:31:44 +00:00
43c8fd1338 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.23.4 (#485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-07 05:14:32 +00:00
6c2725680e fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.9.0 (#484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-06 22:00:21 +00:00
84dfe4e6fe chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.9.0 (#483)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-06 18:02:48 +00:00
a0c1abf011 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.9.0 (#482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-06 13:19:39 +00:00
25820d0920 chore(deps): update dependency gradle to v8.2 (#481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-30 23:11:04 +00:00
7c18283ac8 fix(deps): update ktor to v2.3.2 (#480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-28 19:10:33 +00:00
c5f3b7fa8b fix(deps): update dependency com.google.protobuf:protobuf-java to v3.23.3 (#478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-15 00:06:06 +00:00
8b81010d72 fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.8.22 (#477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-08 10:28:12 +00:00
c07ffa6a14 chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.8.22 (#476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-08 08:04:51 +00:00
7793908ab6 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.8.22 (#475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-08 03:19:07 +00:00
bf6d08c2bd chore: prep for 3.14.4 release 2023-06-05 16:13:54 -04:00
68bae4918e fix: References for for protobuf objects (#466) 2023-06-05 16:12:31 -04:00
592c116c3b fix(deps): update dependency io.ktor:ktor-server-core to v2.3.1 (#474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-01 10:46:44 +00:00
c7fe5c288f fix(deps): update ktor to v2.3.1 (#473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-31 08:54:22 -04:00
e059633055 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.23.2 (#472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-26 20:27:33 +00:00
884a50fc83 chore: prep for 3.14.3 release 2023-05-22 09:39:54 -04:00
588e52c9df fix: allow for request bodies to be marked as required=false (#470) 2023-05-22 09:39:04 -04:00
f49fcb2a22 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.23.1 (#469)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-17 23:39:18 +00:00
0a9475a7ab fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.5.1 (#463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-11 21:54:50 +00:00
f8fbb7ad25 fix(deps): update kotestversion to v5.6.2 (#462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-11 02:53:10 +00:00
f792fb5d1f fix(deps): update dependency com.google.protobuf:protobuf-java to v3.23.0 (#461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-10 11:08:22 +00:00
845d1a971d fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.8.21 (#460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-10 06:19:56 +00:00
9396b2ecfe chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.8.21 (#459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-10 03:36:01 +00:00
d74b7b3f28 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.8.21 (#458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-10 03:31:13 +00:00
e783630845 chore(deps): update dependency gradle to v8.1.1 (#457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-09 23:17:06 +00:00
d907ba004a chore: prep for 3.14.2 release 2023-05-08 11:50:45 -04:00
5c7ec4fdb4 fix: route with parameter declared via ktor function (#455) 2023-05-08 11:49:29 -04:00
097413067b chore: prep for 3.14.1 release 2023-04-28 10:59:27 -04:00
eb7360b8d2 fix: fixed generic property enrichment (#454) 2023-04-28 10:58:20 -04:00
3ec467c1d8 fix(deps): update ktor to v2.3.0 (#452)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-22 18:14:04 +00:00
152476c26e fix(deps): update dependency io.kotest:kotest-assertions-json-jvm to v5.6.1 (#451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-18 21:15:51 +00:00
c4df4c7b2c fix(deps): update dependency io.kotest:kotest-runner-junit5-jvm to v5.6.0 (#450)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-17 05:49:54 +00:00
87fa0b5602 chore(deps): update dependency gradle to v8.1 (#449)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-15 17:00:27 +00:00
53c76d6037 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.22.3 (#448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-13 08:56:13 +00:00
44324ca3a4 chore: rearrange docs (#447) 2023-04-10 19:00:26 +00:00
9364fb19e4 chore: prep for 3.14.0 release 2023-04-06 15:49:07 -04:00
ca1f632665 feat: Introduce Support for Response Headers (#446) 2023-04-06 19:47:48 +00:00
9bb3184735 docs: add showcase link to readme 2023-04-03 19:44:42 -04:00
9a139b5713 chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.8.20 (#440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-03 10:54:36 -04:00
9861c8e258 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.8.20 (#439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-03 13:10:37 +00:00
02f8ed04f2 fix(deps): update dependency dev.forst:ktor-api-key to v2.2.4 (#442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-03 12:54:06 +00:00
3e23939386 fix(deps): update dependency joda-time:joda-time to v2.12.5 (#441)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-31 00:21:18 +00:00
d0575944db fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.8.20 (#438)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-30 11:17:39 +00:00
ccc81c2813 fix(deps): update dependency joda-time:joda-time to v2.12.4 (#436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-24 10:20:45 +00:00
e8da52fd75 fix(deps): update dependency joda-time:joda-time to v2.12.3 (#435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-23 12:49:37 +00:00
34c14e26da fix(deps): update dependency org.slf4j:slf4j-simple to v2.0.7 (#434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-18 01:17:47 +00:00
209b5e38a3 fix(deps): update dependency org.slf4j:slf4j-api to v2.0.7 (#433)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-17 23:50:21 +00:00
902faa9471 fix(deps): update dependency io.gitlab.arturbosch.detekt:detekt-formatting to v1.22.0 (#388) 2023-03-16 12:53:15 +00:00
c7ef70a844 fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.5.0 (#432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-16 12:25:44 +00:00
f83c3d203d fix(deps): update ktor to v2.2.4 (#430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-16 10:38:18 +00:00
bd05dbcfcb chore(deps): update plugin io.github.gradle-nexus.publish-plugin to v1.3.0 (#431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-16 08:48:23 +00:00
cb5c0e71a3 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.22.2 (#429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-16 08:45:44 +00:00
a209cafa74 chore(deps): update dependency gradle to v7.6.1 (#428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-16 07:27:45 +00:00
3d27d30a31 feat: support no request body for post, put and patch (#427) 2023-03-15 12:39:35 +00:00
8cc229667e chore: prep for 3.12.0 release 2023-03-14 16:52:16 -04:00
54d12de67a feat: reintroduce swagger compatability (#426) 2023-03-14 20:43:06 +00:00
9b93c887a2 chore(deps): update plugin io.github.gradle-nexus.publish-plugin to v1.2.0 (#424)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 06:04:16 +00:00
19d828956b fix(deps): update dependency org.apache.logging.log4j:log4j-core to v2.20.0 (#423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-21 18:05:45 +00:00
eabe90acfc fix(deps): update dependency org.apache.logging.log4j:log4j-api to v2.20.0 (#422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-21 17:58:41 +00:00
7c2a2a9c9d fix(deps): update dependency com.google.protobuf:protobuf-java to v3.22.0 (#421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-17 05:27:43 +00:00
15cdfb229d fix(deps): update kotestversion to v5.5.5 (#420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-05 21:56:57 +00:00
5a40f37f81 fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.8.10 (#418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-03 05:51:57 +00:00
64f2516f19 chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.8.10 (#416)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-03 00:22:16 +00:00
2e5e39d3b2 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.8.10 (#415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-02 19:52:53 +00:00
5342cf00d1 fix(deps): update ktor to v2.2.3 (#414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-01 04:08:51 +00:00
8c0b658033 fix: drop logback (#413) 2023-01-31 08:37:57 -05:00
b7b1171685 fix(deps): update dependency dev.forst:ktor-api-key to v2.2.2 (#410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-17 17:26:25 +00:00
1f730869a8 chore: fix incorrect line in documentation 2023-01-07 17:16:05 -05:00
377a60614e feat: constraints (#409) 2023-01-05 14:44:24 +00:00
e34bea1769 fix(deps): update ktor (#397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-04 21:49:06 -05:00
91bf93a866 feat: type enrichment (#408) 2023-01-04 21:32:31 -05:00
73fb8b137f fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.8.0 (#405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-28 22:11:08 +00:00
d3d6e79329 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.21.12 (#402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-15 10:02:49 +00:00
f2b7d924e0 fix(deps): update dependency org.slf4j:slf4j-simple to v2.0.6 (#401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-13 10:38:53 +00:00
f62010b4e7 fix(deps): update dependency org.slf4j:slf4j-api to v2.0.6 (#400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-13 07:39:57 +00:00
5bd1534270 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.21.11 (#398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-08 22:19:05 +00:00
4c151ffeea fix(deps): update dependency joda-time:joda-time to v2.12.2 (#396)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 18:21:42 +00:00
08a0d2e47c fix(deps): update dependency com.google.protobuf:protobuf-java to v3.21.10 (#395)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 09:58:10 +00:00
f19476d3a3 chore: bump gradle version 2022-11-30 12:48:46 -05:00
8ede53fd5c fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.7.22 (#394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-28 21:50:54 +00:00
31a9f44ec1 chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.7.22 (#393)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-28 15:31:11 +00:00
0eb2f126b3 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.7.22 (#392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-28 15:24:18 +00:00
0d658bd6a8 fix(deps): update dependency org.slf4j:slf4j-simple to v2.0.5 (#390)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-25 19:01:28 +00:00
e924671c2b fix(deps): update dependency org.slf4j:slf4j-api to v2.0.5 (#389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-25 18:54:39 +00:00
04996631b9 fix(deps): update dependency ch.qos.logback:logback-classic to v1.4.5 (#387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-19 06:38:07 +00:00
82d80873c8 fix(deps): update dependency org.slf4j:slf4j-simple to v2.0.4 (#386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-18 09:50:16 +00:00
487eaba741 fix(deps): update dependency org.slf4j:slf4j-api to v2.0.4 (#385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-18 06:42:30 +00:00
b935cc1e1d chore: prep for 3.9.0 release 2022-11-15 20:49:58 -05:00
9f2d2dd4e3 fix: do not include rootPath in serialized route path 2022-11-16 01:46:29 +00:00
20f8a4f08f feat: protobuf java converters (#382) 2022-11-15 15:50:26 -05:00
153 changed files with 13847 additions and 644 deletions

View File

@ -12,6 +12,90 @@
## Released
## [4.0.0-alpha] - September 3rd, 2023
### Added
- Support for `type` on sealed interfaces
- Ability to provide custom serializers
### Fixed
- Exception thrown when inheriting variable
- Notarized routes not discarded on test completion
- Data classes with property members breaks schema generation
- Security cannot be applied to individual path operations
- Serialization fails on generic response
- Parameter example descriptions not being applied
### Removed
- Out of the box support for Jackson and Gson (can still be implemented through custom schema configurators)
## [3.14.4] - June 5th, 2023
### Changed
- Components definitions were not in the proper schema section. Prefixed the path with component slug in `protobuf java converter`.
## [3.14.3] - May 22nd, 2023
### Added
- Added `required` parameter in request info builder.
## [3.14.2] - May 8rd, 2023
### Changed
- Fixed bug where routes were not resolving when a parameter was declared via the ktor SDK
## [3.14.1] - April 28th, 2023
### Changed
- Generating enrichments for generic classes no longer throws the `Slugs should not be generated for field enrichments` error.
## [3.14.0] - April 6th, 2023
### Added
- Add support for response headers
## [3.13.0] - March 15th, 2023
### Changed
- Post, Put, and Patch now support not providing request info
## [3.12.0] - March 14th, 2023
### Added
- Add support for swagger documentation
## [3.11.0] - January 5th, 2023
### Added
- Support for type constraints.
## [3.10.0] - January 4th, 2023
### Added
- Support for type enrichments! `deprecated` and `description` to start
## [3.9.0] - November 15th, 2022
### Added
- `protobuf-java-converter` module for converting generated protobuf objects to `JsonSchema` representations
### Changed
- Application `rootPath` is no longer prefixed to serialized route path when `NotarizedRoute` is resolved
## [3.8.0] - November 9th, 2022
### Added

View File

@ -3,3 +3,7 @@
[![version](https://img.shields.io/maven-central/v/io.bkbn/kompendium-core?style=flat-square)](https://search.maven.org/search?q=io.bkbn%20kompendium)
Documentation is stored in the `docs` folder and is hosted [here](https://bkbn.gitbook.io/kompendium)
## Showcase
If you would like to showcase docs that you build using Kompendium, add them to [the showcase discussion](https://github.com/bkbnio/kompendium/discussions/444)

View File

@ -1,12 +1,12 @@
plugins {
kotlin("jvm") version "1.7.21" apply false
kotlin("plugin.serialization") version "1.7.21" apply false
kotlin("jvm") version "1.9.10" apply false
kotlin("plugin.serialization") version "1.9.10" apply false
id("io.bkbn.sourdough.library.jvm") version "0.12.0" apply false
id("io.bkbn.sourdough.application.jvm") version "0.12.0" apply false
id("io.bkbn.sourdough.root") version "0.12.0"
id("com.github.jakemarsden.git-hooks") version "0.0.2"
id("org.jetbrains.kotlinx.kover") version "0.6.1"
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
}
gitHooks {

View File

@ -33,7 +33,6 @@ dependencies {
implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("ch.qos.logback:logback-classic:1.4.4")
// Formatting
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")
@ -49,8 +48,6 @@ dependencies {
testFixturesApi("io.ktor:ktor-server-core:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-test-host:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-jackson:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-gson:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-content-negotiation:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-auth:$ktorVersion")
@ -58,9 +55,9 @@ dependencies {
testFixturesApi("io.ktor:ktor-client:$ktorVersion")
testFixturesApi("io.ktor:ktor-client-cio:$ktorVersion")
testFixturesApi("dev.forst:ktor-api-key:2.1.3")
testFixturesApi("dev.forst:ktor-api-key:2.2.4")
testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
testing {

View File

@ -10,6 +10,7 @@ class DeleteInfo private constructor(
override val summary: String,
override val description: String,
override val externalDocumentation: ExternalDocumentation?,
override val security: Map<String, List<String>>?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
@ -33,7 +34,8 @@ class DeleteInfo private constructor(
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
parameters = parameters,
security = security
)
}
}

View File

@ -12,7 +12,8 @@ class GetInfo private constructor(
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?
) : MethodInfo {
companion object {
@ -33,7 +34,8 @@ class GetInfo private constructor(
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
parameters = parameters,
security = security
)
}
}

View File

@ -12,7 +12,8 @@ class HeadInfo private constructor(
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?,
) : MethodInfo {
companion object {
@ -33,7 +34,8 @@ class HeadInfo private constructor(
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
parameters = parameters,
security = security,
)
}
}

View File

@ -9,6 +9,9 @@ sealed interface MethodInfo {
val tags: Set<String>
val summary: String
val description: String
val security: Map<String, List<String>>?
get() = null
val externalDocumentation: ExternalDocumentation?
get() = null
val operationId: String?
@ -28,6 +31,7 @@ sealed interface MethodInfo {
internal var tags: Set<String> = emptySet()
internal var parameters: List<Parameter> = emptyList()
internal var errors: MutableList<ResponseInfo> = mutableListOf()
internal var security: Map<String, List<String>>? = null
fun response(init: ResponseInfo.Builder.() -> Unit) = apply {
val builder = ResponseInfo.Builder()
@ -59,6 +63,8 @@ sealed interface MethodInfo {
fun parameters(vararg parameters: Parameter) = apply { this.parameters = parameters.toList() }
fun security(security: Map<String, List<String>>) = apply { this.security = security }
abstract fun build(): T
}
}

View File

@ -1,7 +1,7 @@
package io.bkbn.kompendium.core.metadata
sealed interface MethodInfoWithRequest : MethodInfo {
val request: RequestInfo
val request: RequestInfo?
abstract class Builder<T : MethodInfoWithRequest> : MethodInfo.Builder<T>() {
internal var request: RequestInfo? = null

View File

@ -12,7 +12,8 @@ class OptionsInfo private constructor(
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?,
) : MethodInfo {
companion object {
@ -33,7 +34,8 @@ class OptionsInfo private constructor(
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
parameters = parameters,
security = security,
)
}
}

View File

@ -4,7 +4,7 @@ import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
class PatchInfo private constructor(
override val request: RequestInfo,
override val request: RequestInfo?,
override val errors: MutableList<ResponseInfo>,
override val response: ResponseInfo,
override val tags: Set<String>,
@ -13,7 +13,8 @@ class PatchInfo private constructor(
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?,
) : MethodInfoWithRequest {
companion object {
@ -26,7 +27,7 @@ class PatchInfo private constructor(
class Builder : MethodInfoWithRequest.Builder<PatchInfo>() {
override fun build() = PatchInfo(
request = request ?: error("request info must be present"),
request = request,
errors = errors,
response = response ?: error("response info must be present"),
tags = tags,
@ -35,7 +36,8 @@ class PatchInfo private constructor(
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
parameters = parameters,
security = security,
)
}
}

View File

@ -4,7 +4,7 @@ import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
class PostInfo private constructor(
override val request: RequestInfo,
override val request: RequestInfo?,
override val errors: MutableList<ResponseInfo>,
override val response: ResponseInfo,
override val tags: Set<String>,
@ -13,7 +13,8 @@ class PostInfo private constructor(
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?,
) : MethodInfoWithRequest {
companion object {
@ -26,7 +27,7 @@ class PostInfo private constructor(
class Builder : MethodInfoWithRequest.Builder<PostInfo>() {
override fun build() = PostInfo(
request = request ?: error("request info must be present"),
request = request,
errors = errors,
response = response ?: error("response info must be present"),
tags = tags,
@ -35,7 +36,8 @@ class PostInfo private constructor(
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
parameters = parameters,
security = security,
)
}
}

View File

@ -4,7 +4,7 @@ import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
class PutInfo private constructor(
override val request: RequestInfo,
override val request: RequestInfo?,
override val errors: MutableList<ResponseInfo>,
override val response: ResponseInfo,
override val tags: Set<String>,
@ -13,7 +13,8 @@ class PutInfo private constructor(
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
override val parameters: List<Parameter>,
override val security: Map<String, List<String>>?,
) : MethodInfoWithRequest {
companion object {
@ -26,7 +27,7 @@ class PutInfo private constructor(
class Builder : MethodInfoWithRequest.Builder<PutInfo>() {
override fun build() = PutInfo(
request = request ?: error("request info must be present"),
request = request,
errors = errors,
response = response ?: error("response info must be present"),
tags = tags,
@ -35,7 +36,8 @@ class PutInfo private constructor(
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
parameters = parameters,
security = security,
)
}
}

View File

@ -1,14 +1,17 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.oas.payload.MediaType
import kotlin.reflect.KType
import kotlin.reflect.typeOf
class RequestInfo private constructor(
val requestType: KType,
val typeEnrichment: TypeEnrichment<*>?,
val description: String,
val examples: Map<String, MediaType.Example>?,
val mediaTypes: Set<String>
val mediaTypes: Set<String>,
val required: Boolean
) {
companion object {
@ -21,20 +24,33 @@ class RequestInfo private constructor(
class Builder {
private var requestType: KType? = null
private var typeEnrichment: TypeEnrichment<*>? = null
private var description: String? = null
private var examples: Map<String, MediaType.Example>? = null
private var mediaTypes: Set<String>? = null
private var required: Boolean? = null
fun required(r: Boolean) = apply {
this.required = r
}
fun requestType(t: KType) = apply {
this.requestType = t
}
inline fun <reified T> requestType() = apply { requestType(typeOf<T>()) }
fun enrichment(t: TypeEnrichment<*>) = apply {
this.typeEnrichment = t
}
inline fun <reified T> requestType(enrichment: TypeEnrichment<T>? = null) = apply {
requestType(typeOf<T>())
enrichment?.let { enrichment(it) }
}
fun description(s: String) = apply { this.description = s }
fun examples(vararg e: Pair<String, Any>) = apply {
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) }
fun examples(vararg e: Pair<String, MediaType.Example>) = apply {
this.examples = e.toMap()
}
fun mediaTypes(vararg m: String) = apply {
@ -44,8 +60,10 @@ class RequestInfo private constructor(
fun build() = RequestInfo(
requestType = requestType ?: error("Request type must be present"),
description = description ?: error("Description must be present"),
typeEnrichment = typeEnrichment,
examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json")
mediaTypes = mediaTypes ?: setOf("application/json"),
required = required ?: true
)
}
}

View File

@ -1,5 +1,7 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.oas.payload.Header
import io.bkbn.kompendium.oas.payload.MediaType
import io.ktor.http.HttpStatusCode
import kotlin.reflect.KType
@ -8,9 +10,11 @@ import kotlin.reflect.typeOf
class ResponseInfo private constructor(
val responseCode: HttpStatusCode,
val responseType: KType,
val typeEnrichment: TypeEnrichment<*>?,
val description: String,
val examples: Map<String, MediaType.Example>?,
val mediaTypes: Set<String>
val mediaTypes: Set<String>,
val responseHeaders: Map<String, Header>?
) {
companion object {
@ -24,9 +28,15 @@ class ResponseInfo private constructor(
class Builder {
private var responseCode: HttpStatusCode? = null
private var responseType: KType? = null
private var typeEnrichment: TypeEnrichment<*>? = null
private var description: String? = null
private var examples: Map<String, MediaType.Example>? = null
private var mediaTypes: Set<String>? = null
private var responseHeaders: Map<String, Header>? = null
fun responseHeaders(headers: Map<String, Header>) = apply {
this.responseHeaders = headers
}
fun responseCode(code: HttpStatusCode) = apply {
this.responseCode = code
@ -36,12 +46,19 @@ class ResponseInfo private constructor(
this.responseType = t
}
inline fun <reified T> responseType() = apply { responseType(typeOf<T>()) }
fun enrichment(t: TypeEnrichment<*>) = apply {
this.typeEnrichment = t
}
inline fun <reified T> responseType(enrichment: TypeEnrichment<T>? = null) = apply {
responseType(typeOf<T>())
enrichment?.let { enrichment(it) }
}
fun description(s: String) = apply { this.description = s }
fun examples(vararg e: Pair<String, Any>) = apply {
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) }
fun examples(vararg e: Pair<String, MediaType.Example>) = apply {
this.examples = e.toMap()
}
fun mediaTypes(vararg m: String) = apply {
@ -52,8 +69,10 @@ class ResponseInfo private constructor(
responseCode = responseCode ?: error("You must provide a response code in order to build a Response!"),
responseType = responseType ?: error("You must provide a response type in order to build a Response!"),
description = description ?: error("You must provide a description in order to build a Response!"),
typeEnrichment = typeEnrichment,
examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json")
mediaTypes = mediaTypes ?: setOf("application/json"),
responseHeaders = responseHeaders
)
}
}

View File

@ -1,16 +1,15 @@
package io.bkbn.kompendium.core.plugin
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.oas.OpenApiSpec
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.response.respond
import io.ktor.server.routing.Routing
import io.ktor.server.routing.application
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
@ -19,25 +18,26 @@ import kotlin.reflect.KType
object NotarizedApplication {
class Config {
lateinit var spec: OpenApiSpec
var openApiJson: Routing.() -> Unit = {
route("/openapi.json") {
lateinit var spec: () -> OpenApiSpec
var specRoute: (OpenApiSpec, Routing) -> Unit = { spec, routing ->
routing.route("/openapi.json") {
get {
call.respond(HttpStatusCode.OK, this@route.application.attributes[KompendiumAttributes.openApiSpec])
call.respond(spec)
}
}
}
var customTypes: Map<KType, JsonSchema> = emptyMap()
var schemaConfigurator: SchemaConfigurator = SchemaConfigurator.Default()
var schemaConfigurator: SchemaConfigurator = KotlinXSchemaConfigurator()
}
operator fun invoke() = createApplicationPlugin(
name = "NotarizedApplication",
createConfiguration = ::Config
) {
val spec = pluginConfig.spec
val routing = application.routing { }
pluginConfig.openApiJson(routing)
val spec = pluginConfig.spec()
val routing = application.routing {}
this@createApplicationPlugin.pluginConfig.specRoute(spec, routing)
// pluginConfig.openApiJson(routing)
pluginConfig.customTypes.forEach { (type, schema) ->
spec.components.schemas[type.getSimpleSlug()] = schema
}

View File

@ -18,6 +18,7 @@ import io.ktor.server.application.Hook
import io.ktor.server.application.PluginBuilder
import io.ktor.server.application.createRouteScopedPlugin
import io.ktor.server.routing.Route
import io.ktor.server.routing.application
object NotarizedRoute {
class Config : SpecConfig {
@ -43,7 +44,6 @@ object NotarizedRoute {
name = "NotarizedRoute",
createConfiguration = ::Config
) {
// This is required in order to introspect the route path and authentication
on(InstallHook) {
val route = it as? Route ?: return@on
@ -76,7 +76,18 @@ object NotarizedRoute {
spec.paths[fullPath] = path
}
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
fun Route.calculateRoutePath() = toString()
.let {
application.environment.rootPath.takeIf { root -> root.isNotEmpty() }
?.let { root ->
val sanitizedRoute = if (root.startsWith("/")) root else "/$root"
it.replace(sanitizedRoute, "")
}
?: it
}
.replace(Regex("/\\(.+\\)"), "")
.replace(Regex("/\\[.+\\]"), "")
fun Route.collectAuthMethods() = toString()
.split("/")
.filter { it.contains(Regex("\\(authenticate .*\\)")) }

View File

@ -18,7 +18,7 @@ import kotlinx.html.unsafe
/**
* Provides an out-of-the-box route to view docs using ReDoc on the specified [path].
* @param pageTitle Webpage title you wish to be displayed on your docs
* @param route path to docs resource
* @param path path to docs resource
* @param specUrl url to point ReDoc to the OpenAPI json document
*/
fun Route.redoc(pageTitle: String = "Docs", path: String = "/docs", specUrl: String = "/openapi.json") {

View File

@ -0,0 +1,91 @@
package io.bkbn.kompendium.core.routes
import io.ktor.server.application.call
import io.ktor.server.html.respondHtml
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import kotlinx.html.body
import kotlinx.html.div
import kotlinx.html.head
import kotlinx.html.id
import kotlinx.html.link
import kotlinx.html.meta
import kotlinx.html.script
import kotlinx.html.title
import kotlinx.html.unsafe
/**
* Provides an out-of-the-box route to view docs using Swagger
* @see <a href="https://swagger.io/specification/">Swagger OpenApi Specification</a>
* for the latest supported open api version.
* @param pageTitle Webpage title you wish to be displayed on your docs
* @param path path to docs resource
* @param specUrl url to point Swagger to the OpenAPI json document
* @param swaggerVersion version of swagger-ui distribution
*/
fun Route.swagger(
pageTitle: String = "Docs",
path: String = "/swagger-ui",
specUrl: String = "/openapi.json",
swaggerVersion: String? = null
) {
val swaggerVersionSuffix = if (swaggerVersion == null) "" else "@$swaggerVersion"
route(path) {
get {
call.respondHtml {
head {
title {
+pageTitle
}
meta {
charset = "utf-8"
}
meta {
name = "viewport"
content = "width=device-width, initial-scale=1"
}
link {
href = "https://unpkg.com/swagger-ui-dist$swaggerVersionSuffix/swagger-ui.css"
rel = "stylesheet"
}
}
body {
div {
id = "swagger-ui"
}
script {
src = "https://unpkg.com/swagger-ui-dist$swaggerVersionSuffix/swagger-ui-standalone-preset.js"
}
script {
src = "https://unpkg.com/swagger-ui-dist$swaggerVersionSuffix/swagger-ui-bundle.js"
}
unsafe {
+"""
<script>
window.onload = function () {
// Build a system
const ui = SwaggerUIBundle({
url: "$specUrl",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
})
window.ui = ui
}
</script>
""".trimIndent()
}
}
}
}
}
}

View File

@ -10,13 +10,14 @@ import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation
@ -50,26 +51,36 @@ object Helpers {
authMethods: List<String> = emptyList()
) {
SchemaGenerator.fromTypeOrUnit(
this.response.responseType,
spec.components.schemas, schemaConfigurator
type = this.response.responseType,
cache = spec.components.schemas,
schemaConfigurator = schemaConfigurator,
enrichment = this.response.typeEnrichment,
)?.let { schema ->
spec.components.schemas[this.response.responseType.getSimpleSlug()] = schema
spec.components.schemas[this.response.responseType.getSlug(this.response.typeEnrichment)] = schema
}
errors.forEach { error ->
SchemaGenerator.fromTypeOrUnit(error.responseType, spec.components.schemas, schemaConfigurator)?.let { schema ->
spec.components.schemas[error.responseType.getSimpleSlug()] = schema
SchemaGenerator.fromTypeOrUnit(
type = error.responseType,
cache = spec.components.schemas,
schemaConfigurator = schemaConfigurator,
enrichment = error.typeEnrichment,
)?.let { schema ->
spec.components.schemas[error.responseType.getSlug(error.typeEnrichment)] = schema
}
}
when (this) {
is MethodInfoWithRequest -> {
this.request?.let { reqInfo ->
SchemaGenerator.fromTypeOrUnit(
this.request.requestType,
spec.components.schemas,
schemaConfigurator
type = reqInfo.requestType,
cache = spec.components.schemas,
schemaConfigurator = schemaConfigurator,
enrichment = reqInfo.typeEnrichment,
)?.let { schema ->
spec.components.schemas[this.request.requestType.getSimpleSlug()] = schema
spec.components.schemas[reqInfo.requestType.getSlug(reqInfo.typeEnrichment)] = schema
}
}
}
@ -107,46 +118,83 @@ object Helpers {
operationId = this.operationId,
deprecated = this.deprecated,
parameters = this.parameters,
security = config.security
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toMutableList(),
security = this.createCombinedSecurityContext(config),
requestBody = when (this) {
is MethodInfoWithRequest -> Request(
description = this.request.description,
content = this.request.requestType.toReferenceContent(this.request.examples, this.request.mediaTypes),
required = true
is MethodInfoWithRequest -> this.request?.let { reqInfo ->
Request(
description = reqInfo.description,
content = reqInfo.requestType.toReferenceContent(
examples = reqInfo.examples,
mediaTypes = reqInfo.mediaTypes,
enrichment = reqInfo.typeEnrichment
),
required = reqInfo.required
)
}
else -> null
},
responses = mapOf(
this.response.responseCode.value to Response(
description = this.response.description,
content = this.response.responseType.toReferenceContent(this.response.examples, this.response.mediaTypes)
headers = this.response.responseHeaders,
content = this.response.responseType.toReferenceContent(
examples = this.response.examples,
mediaTypes = this.response.mediaTypes,
enrichment = this.response.typeEnrichment
)
)
).plus(this.errors.toResponseMap())
)
private fun MethodInfo.createCombinedSecurityContext(config: SpecConfig): MutableList<Map<String, List<String>>>? {
val configSecurity = config.security
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toMutableList()
val methodSecurity = this.security
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toMutableList()
return when {
configSecurity == null && methodSecurity == null -> null
configSecurity == null -> methodSecurity
methodSecurity == null -> configSecurity
else -> configSecurity.plus(methodSecurity).toMutableList()
}
}
private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error ->
error.responseCode.value to Response(
description = error.description,
content = error.responseType.toReferenceContent(error.examples, error.mediaTypes)
headers = error.responseHeaders,
content = error.responseType.toReferenceContent(
examples = error.examples,
mediaTypes = error.mediaTypes,
enrichment = error.typeEnrichment
)
)
}
private fun KType.toReferenceContent(
examples: Map<String, MediaType.Example>?,
mediaTypes: Set<String>
mediaTypes: Set<String>,
enrichment: TypeEnrichment<*>?
): Map<String, MediaType>? =
when (this.classifier as KClass<*>) {
Unit::class -> null
else -> mediaTypes.associateWith {
MediaType(
schema = if (this.isMarkedNullable) OneOfDefinition(
schema = if (this.isMarkedNullable) {
OneOfDefinition(
NullableDefinition(),
ReferenceDefinition(this.getReferenceSlug())
) else ReferenceDefinition(this.getReferenceSlug()),
ReferenceDefinition(this.getReferenceSlug(enrichment))
)
} else {
ReferenceDefinition(this.getReferenceSlug(enrichment))
},
examples = examples
)
}

View File

@ -2,26 +2,38 @@ package io.bkbn.kompendium.core
import dev.forst.ktor.apikey.apiKey
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers
import io.bkbn.kompendium.core.util.arrayConstraints
import io.bkbn.kompendium.core.util.complexRequest
import io.bkbn.kompendium.core.util.customAuthConfig
import io.bkbn.kompendium.core.util.customFieldNameResponse
import io.bkbn.kompendium.core.util.customScopesOnSiblingPathOperations
import io.bkbn.kompendium.core.util.dateTimeString
import io.bkbn.kompendium.core.util.defaultAuthConfig
import io.bkbn.kompendium.core.util.defaultField
import io.bkbn.kompendium.core.util.defaultParameter
import io.bkbn.kompendium.core.util.doubleConstraints
import io.bkbn.kompendium.core.util.enrichedComplexGenericType
import io.bkbn.kompendium.core.util.enrichedGenericResponse
import io.bkbn.kompendium.core.util.enrichedNestedCollection
import io.bkbn.kompendium.core.util.enrichedSimpleRequest
import io.bkbn.kompendium.core.util.enrichedSimpleResponse
import io.bkbn.kompendium.core.util.exampleParams
import io.bkbn.kompendium.core.util.exampleSummaryAndDescription
import io.bkbn.kompendium.core.util.fieldOutsideConstructor
import io.bkbn.kompendium.core.util.genericException
import io.bkbn.kompendium.core.util.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls
import io.bkbn.kompendium.core.util.gnarlyGenericResponse
import io.bkbn.kompendium.core.util.headerParameter
import io.bkbn.kompendium.core.util.ignoredFieldsResponse
import io.bkbn.kompendium.core.util.intConstraints
import io.bkbn.kompendium.core.util.multipleAuthStrategies
import io.bkbn.kompendium.core.util.multipleExceptions
import io.bkbn.kompendium.core.util.nestedGenericCollection
import io.bkbn.kompendium.core.util.nestedGenericMultipleParamsCollection
import io.bkbn.kompendium.core.util.nestedGenericResponse
import io.bkbn.kompendium.core.util.nestedTypeName
import io.bkbn.kompendium.core.util.nestedUnderRoot
import io.bkbn.kompendium.core.util.nonRequiredParam
import io.bkbn.kompendium.core.util.nonRequiredParams
import io.bkbn.kompendium.core.util.notarizedDelete
@ -35,27 +47,35 @@ import io.bkbn.kompendium.core.util.nullableEnumField
import io.bkbn.kompendium.core.util.nullableField
import io.bkbn.kompendium.core.util.nullableNestedObject
import io.bkbn.kompendium.core.util.nullableReference
import io.bkbn.kompendium.core.util.optionalReqExample
import io.bkbn.kompendium.core.util.overrideMediaTypes
import io.bkbn.kompendium.core.util.overrideSealedTypeIdentifier
import io.bkbn.kompendium.core.util.paramWrapper
import io.bkbn.kompendium.core.util.polymorphicCollectionResponse
import io.bkbn.kompendium.core.util.polymorphicException
import io.bkbn.kompendium.core.util.polymorphicMapResponse
import io.bkbn.kompendium.core.util.polymorphicResponse
import io.bkbn.kompendium.core.util.postNoReqBody
import io.bkbn.kompendium.core.util.primitives
import io.bkbn.kompendium.core.util.reqRespExamples
import io.bkbn.kompendium.core.util.requiredParams
import io.bkbn.kompendium.core.util.responseHeaders
import io.bkbn.kompendium.core.util.returnsList
import io.bkbn.kompendium.core.util.rootRoute
import io.bkbn.kompendium.core.util.samePathDifferentMethodsAndAuth
import io.bkbn.kompendium.core.util.samePathSameMethod
import io.bkbn.kompendium.core.util.simpleGenericResponse
import io.bkbn.kompendium.core.util.simplePathParsing
import io.bkbn.kompendium.core.util.simpleRecursive
import io.bkbn.kompendium.core.util.singleException
import io.bkbn.kompendium.core.util.stringConstraints
import io.bkbn.kompendium.core.util.stringContentEncodingConstraints
import io.bkbn.kompendium.core.util.stringPatternConstraints
import io.bkbn.kompendium.core.util.subtypeNotCompleteSetOfParentProperties
import io.bkbn.kompendium.core.util.topLevelNullable
import io.bkbn.kompendium.core.util.trailingSlash
import io.bkbn.kompendium.core.util.unbackedFieldsResponse
import io.bkbn.kompendium.core.util.withOperationId
import io.bkbn.kompendium.core.util.nestedUnderRoot
import io.bkbn.kompendium.core.util.rootRoute
import io.bkbn.kompendium.core.util.simplePathParsing
import io.bkbn.kompendium.core.util.trailingSlash
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException
import io.bkbn.kompendium.oas.component.Components
@ -63,6 +83,7 @@ import io.bkbn.kompendium.oas.security.ApiKeyAuth
import io.bkbn.kompendium.oas.security.BasicAuth
import io.bkbn.kompendium.oas.security.BearerAuth
import io.bkbn.kompendium.oas.security.OAuth
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.should
@ -70,6 +91,8 @@ import io.kotest.matchers.string.startWith
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.http.HttpMethod
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.OAuthServerSettings
@ -77,6 +100,12 @@ import io.ktor.server.auth.UserIdPrincipal
import io.ktor.server.auth.basic
import io.ktor.server.auth.jwt.jwt
import io.ktor.server.auth.oauth
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.net.URI
import java.time.Instant
import kotlin.reflect.typeOf
@ -118,6 +147,12 @@ class KompendiumTest : DescribeSpec({
it("Can override media types") {
openApiTestAllSerializers("T0052__override_media_types.json") { overrideMediaTypes() }
}
it("Can support a post request with no request body") {
openApiTestAllSerializers("T0065__post_no_req_body.json") { postNoReqBody() }
}
it("Can notarize a route with response headers") {
openApiTestAllSerializers("T0066__notarized_get_with_response_headers.json") { responseHeaders() }
}
}
describe("Route Parsing") {
it("Can parse a simple path and store it under the expected route") {
@ -132,6 +167,9 @@ class KompendiumTest : DescribeSpec({
it("Can notarize a route with a trailing slash") {
openApiTestAllSerializers("T0015__trailing_slash.json") { trailingSlash() }
}
it("Can notarize a route with a parameter") {
openApiTestAllSerializers("T0068__param_wrapper.json") { paramWrapper() }
}
}
describe("Exceptions") {
it("Can add an exception status code to a response") {
@ -154,6 +192,12 @@ class KompendiumTest : DescribeSpec({
it("Can describe example parameters") {
openApiTestAllSerializers("T0021__example_parameters.json") { exampleParams() }
}
it("Can generate example optional request body") {
openApiTestAllSerializers("T0069__example_optional_req.json") { optionalReqExample() }
}
it("Can generate example summary and description") {
openApiTestAllSerializers("T0075__example_summary_and_description.json") { exampleSummaryAndDescription() }
}
}
describe("Defaults") {
it("Can generate a default parameter value") {
@ -207,6 +251,13 @@ class KompendiumTest : DescribeSpec({
it("Can handle a really gnarly generic example") {
openApiTestAllSerializers("T0043__gnarly_generic_example.json") { gnarlyGenericResponse() }
}
it("Can override the type name for a sealed interface implementation") {
openApiTestAllSerializers("T0070__sealed_interface_type_name_override.json") { overrideSealedTypeIdentifier() }
}
it("Can serialize an object where the subtype is not a complete set of parent properties") {
openApiTestAllSerializers("T0071__subtype_not_complete_set_of_parent_properties.json") {
subtypeNotCompleteSetOfParentProperties()
}
}
describe("Custom Serializable Reader tests") {
it("Can support ignoring fields") {
@ -275,6 +326,52 @@ class KompendiumTest : DescribeSpec({
}
) { samePathDifferentMethodsAndAuth() }
}
it("Can generate paths without application root-path") {
openApiTestAllSerializers(
"T0054__app_with_rootpath.json",
applicationEnvironmentBuilder = {
rootPath = "/example"
},
specOverrides = {
copy(
servers = servers.map { it.copy(url = URI("${it.url}/example")) }.toMutableList()
)
}
) { notarizedGet() }
}
it("Can apply a custom serialization strategy to the openapi document") {
val customJsonEncoder = Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
}
openApiTestAllSerializers(
snapshotName = "T0072__custom_serialization_strategy.json",
notarizedApplicationConfigOverrides = {
specRoute = { spec, routing ->
routing {
route("/openapi.json") {
get {
call.response.headers.append("Content-Type", "application/json")
call.respondText { customJsonEncoder.encodeToString(spec) }
}
}
}
}
},
contentNegotiation = {
json(
Json {
encodeDefaults = true
explicitNulls = true
}
)
}
) { notarizedGet() }
}
it("Can serialize a data class with a field outside of the constructor") {
openApiTestAllSerializers("T0073__data_class_with_field_outside_constructor.json") { fieldOutsideConstructor() }
}
}
describe("Error Handling") {
it("Throws a clear exception when an unidentified type is encountered") {
@ -283,16 +380,23 @@ class KompendiumTest : DescribeSpec({
}
it("Throws an exception when same method for same path has been previously registered") {
val exception = shouldThrow<IllegalArgumentException> {
openApiTestAllSerializers("") {
openApiTestAllSerializers(
snapshotName = "",
applicationSetup = {
install(Authentication) {
basic("basic") {
realm = "Ktor Server"
validate { UserIdPrincipal("Placeholder") }
}
}
},
) {
samePathSameMethod()
}
}
exception.message should startWith("A route has already been registered for path: /test/{a} and method: GET")
}
}
describe("Constraints") {
// TODO Assess strategies here
}
describe("Formats") {
it("Can set a format for a simple type schema") {
openApiTestAllSerializers(
@ -399,5 +503,89 @@ class KompendiumTest : DescribeSpec({
}
) { multipleAuthStrategies() }
}
it("Can provide different scopes on path operations in the same route") {
openApiTestAllSerializers(
snapshotName = "T0074__auth_on_specific_path_operation.json",
applicationSetup = {
install(Authentication) {
oauth("auth-oauth-google") {
urlProvider = { "http://localhost:8080/callback" }
providerLookup = {
OAuthServerSettings.OAuth2ServerSettings(
name = "google",
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
accessTokenUrl = "https://accounts.google.com/o/oauth2/token",
requestMethod = HttpMethod.Post,
clientId = "DUMMY_VAL",
clientSecret = "DUMMY_VAL",
defaultScopes = listOf("https://www.googleapis.com/auth/userinfo.profile"),
extraTokenParameters = listOf("access_type" to "offline")
)
}
client = HttpClient(CIO)
}
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"auth-oauth-google" to OAuth(
flows = OAuth.Flows(
implicit = OAuth.Flows.Implicit(
authorizationUrl = "https://accounts.google.com/o/oauth2/auth",
scopes = mapOf(
"write:pets" to "modify pets in your account",
"read:pets" to "read your pets"
)
)
)
)
)
)
)
}
) { customScopesOnSiblingPathOperations() }
}
}
describe("Enrichment") {
it("Can enrich a simple request") {
openApiTestAllSerializers("T0055__enriched_simple_request.json") { enrichedSimpleRequest() }
}
it("Can enrich a simple response") {
openApiTestAllSerializers("T0058__enriched_simple_response.json") { enrichedSimpleResponse() }
}
it("Can enrich a nested collection") {
openApiTestAllSerializers("T0056__enriched_nested_collection.json") { enrichedNestedCollection() }
}
it("Can enrich a complex generic type") {
openApiTestAllSerializers("T0057__enriched_complex_generic_type.json") { enrichedComplexGenericType() }
}
it("Can enrich a generic object") {
openApiTestAllSerializers("T0067__enriched_generic_object.json") { enrichedGenericResponse() }
}
}
describe("Constraints") {
it("Can apply constraints to an int field") {
openApiTestAllSerializers("T0059__int_constraints.json") { intConstraints() }
}
it("Can apply constraints to a double field") {
openApiTestAllSerializers("T0060__double_constraints.json") { doubleConstraints() }
}
it("Can apply a min and max length to a string field") {
openApiTestAllSerializers("T0061__string_min_max_constraints.json") { stringConstraints() }
}
it("Can apply a pattern to a string field") {
openApiTestAllSerializers("T0062__string_pattern_constraints.json") { stringPatternConstraints() }
}
it("Can apply a content encoding and media type to a string field") {
openApiTestAllSerializers("T0063__string_content_encoding_constraints.json") {
stringContentEncodingConstraints()
}
}
it("Can apply constraints to an array field") {
openApiTestAllSerializers("T0064__array_constraints.json") { arrayConstraints() }
}
}
}
})

View File

@ -2,6 +2,7 @@ package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription
import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
@ -11,6 +12,7 @@ import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
import io.ktor.server.auth.authenticate
import io.ktor.server.routing.Routing
import io.ktor.server.routing.post
import io.ktor.server.routing.route
fun Routing.defaultAuthConfig() {
@ -42,6 +44,43 @@ fun Routing.customAuthConfig() {
}
}
fun Routing.customScopesOnSiblingPathOperations() {
authenticate("auth-oauth-google") {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
security = mapOf(
"auth-oauth-google" to listOf("read:pets")
)
}
post = PostInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
request {
description(defaultResponseDescription)
requestType<TestResponse>()
}
security = mapOf(
"auth-oauth-google" to listOf("write:pets")
)
}
}
}
}
}
fun Routing.multipleAuthStrategies() {
authenticate("jwt", "api-key") {
route(rootPath) {

View File

@ -0,0 +1,160 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.DoubleResponse
import io.bkbn.kompendium.core.fixtures.Page
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
import io.bkbn.kompendium.core.fixtures.TestNested
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultPath
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
import io.ktor.server.routing.Routing
import io.ktor.server.routing.route
fun Routing.intConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get an int")
description("Get an int")
response {
responseCode(HttpStatusCode.OK)
description("An int")
responseType(
enrichment = TypeEnrichment("example") {
TestCreatedResponse::id {
minimum = 2
maximum = 100
multipleOf = 2
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}
fun Routing.doubleConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get a double")
description("Get a double")
response {
responseCode(HttpStatusCode.OK)
description("A double")
responseType(
enrichment = TypeEnrichment("example") {
DoubleResponse::payload {
minimum = 2.0
maximum = 100.0
multipleOf = 2.0
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}
fun Routing.stringConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get a string")
description("Get a string with constraints")
response {
responseCode(HttpStatusCode.OK)
description("A string")
responseType(
enrichment = TypeEnrichment("example") {
TestNested::nesty {
maxLength = 10
minLength = 2
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}
fun Routing.stringPatternConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get a string")
description("This is a description")
response {
responseCode(HttpStatusCode.OK)
description("A string")
responseType(
enrichment = TypeEnrichment("example") {
TestNested::nesty {
pattern = "[a-z]+"
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}
fun Routing.stringContentEncodingConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get a string")
description("This is a description")
response {
responseCode(HttpStatusCode.OK)
description("A string")
responseType(
enrichment = TypeEnrichment("example") {
TestNested::nesty {
contentEncoding = "base64"
contentMediaType = "image/png"
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}
fun Routing.arrayConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get an array")
description("Get an array of strings")
response {
responseCode(HttpStatusCode.OK)
description("An array")
responseType(
enrichment = TypeEnrichment("example") {
Page<String>::content {
minItems = 2
maxItems = 10
uniqueItems = true
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}

View File

@ -0,0 +1,168 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics
import io.bkbn.kompendium.core.fixtures.NestedComplexItem
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
import io.bkbn.kompendium.core.fixtures.GenericObject
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
import io.ktor.server.routing.Routing
import io.ktor.server.routing.route
fun Routing.enrichedSimpleResponse() {
route("/enriched") {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
response {
responseType(
enrichment = TypeEnrichment("simple") {
TestResponse::c {
description = "A simple description"
}
}
)
description("A good response")
responseCode(HttpStatusCode.Created)
}
}
}
}
}
fun Routing.enrichedSimpleRequest() {
route("/example") {
install(NotarizedRoute()) {
parameters = TestModules.defaultParams
post = PostInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
request {
requestType(
enrichment = TypeEnrichment("simple") {
TestSimpleRequest::a {
description = "A simple description"
}
TestSimpleRequest::b {
deprecated = true
}
}
)
description("A test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(TestModules.defaultResponseDescription)
}
}
}
}
}
fun Routing.enrichedNestedCollection() {
route("/example") {
install(NotarizedRoute()) {
parameters = TestModules.defaultParams
post = PostInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
request {
requestType(
enrichment = TypeEnrichment("simple") {
ComplexRequest::tables {
description = "A nested item"
typeEnrichment = TypeEnrichment("nested") {
NestedComplexItem::name {
description = "A nested description"
}
}
}
}
)
description("A test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(TestModules.defaultResponseDescription)
}
}
}
}
}
fun Routing.enrichedComplexGenericType() {
route("/example") {
install(NotarizedRoute()) {
parameters = TestModules.defaultParams
post = PostInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
request {
requestType(
enrichment = TypeEnrichment("simple") {
MultiNestedGenerics<String, ComplexRequest>::content {
description = "Getting pretty crazy"
typeEnrichment = TypeEnrichment("nested") {
ComplexRequest::tables {
description = "A nested item"
typeEnrichment = TypeEnrichment("nested") {
NestedComplexItem::name {
description = "A nested description"
}
}
}
}
}
}
)
description("A test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(TestModules.defaultResponseDescription)
}
}
}
}
}
fun Routing.enrichedGenericResponse() {
route("/example") {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
response {
responseType(
enrichment = TypeEnrichment("generic") {
GenericObject<TestSimpleRequest>::data {
description = "A simple description"
typeEnrichment = TypeEnrichment("simple") {
TestSimpleRequest::a {
description = "A simple description"
}
TestSimpleRequest::b {
deprecated = true
}
}
}
}
)
description("A good response")
responseCode(HttpStatusCode.Created)
}
}
}
}
}

View File

@ -9,7 +9,7 @@ import io.ktor.server.routing.route
fun Routing.samePathSameMethod() {
route(defaultPath) {
basicGetGenerator<TestResponse>()
authenticate {
authenticate("basic") {
basicGetGenerator<TestResponse>()
}
}

View File

@ -11,6 +11,7 @@ import io.bkbn.kompendium.core.util.TestModules.defaultRequestDescription
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
import io.bkbn.kompendium.core.util.TestModules.rootPath
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.MediaType
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
@ -27,7 +28,7 @@ fun Routing.reqRespExamples() {
description(defaultRequestDescription)
requestType<TestRequest>()
examples(
"Testerina" to TestRequest(TestNested("asdf"), 1.5, emptyList())
"Testerina" to MediaType.Example(TestRequest(TestNested("asdf"), 1.5, emptyList()))
)
}
response {
@ -35,7 +36,7 @@ fun Routing.reqRespExamples() {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
examples(
"Testerino" to TestResponse("Heya")
"Testerino" to MediaType.Example(TestResponse("Heya"))
)
}
}
@ -50,8 +51,65 @@ fun Routing.exampleParams() = basicGetGenerator<TestResponse>(
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING,
examples = mapOf(
"foo" to Parameter.Example("testing")
"foo" to MediaType.Example("testing")
)
)
)
)
fun Routing.optionalReqExample() {
route(rootPath) {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
description(defaultRequestDescription)
requestType<TestRequest>()
examples(
"Testerina" to MediaType.Example(TestRequest(TestNested("asdf"), 1.5, emptyList()))
)
required(false)
}
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
examples(
"Testerino" to MediaType.Example(TestResponse("Heya"))
)
}
}
}
}
}
fun Routing.exampleSummaryAndDescription() {
route(rootPath) {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary("This is a summary")
description("This is a description")
request {
description("This is a request description")
requestType<TestRequest>()
examples(
"Testerina" to MediaType.Example(
TestRequest(TestNested("asdf"), 1.5, emptyList()),
"summary",
"description"
)
)
}
response {
description("This is a response description")
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
examples(
"Testerino" to MediaType.Example(TestResponse("Heya"), "summary", "description")
)
}
}
}
}
}

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.SomethingSimilar
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
import io.bkbn.kompendium.core.fixtures.TestRequest
import io.bkbn.kompendium.core.fixtures.TestResponse
@ -20,7 +21,9 @@ import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
import io.bkbn.kompendium.core.util.TestModules.rootPath
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Header
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.application.install
@ -56,6 +59,38 @@ fun Routing.notarizedGet() {
}
}
fun Routing.responseHeaders() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
get = GetInfo.builder {
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description(defaultResponseDescription)
responseHeaders(
mapOf(
HttpHeaders.ETag to Header(
TypeDefinition.STRING,
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag"
),
HttpHeaders.LastModified to Header(
TypeDefinition.STRING,
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified"
),
)
)
}
summary(defaultPathSummary)
description(defaultPathDescription)
}
}
get {
call.respondText { "hey dude ‼️ congrats on the get request" }
}
}
}
fun Routing.notarizedPost() {
route(defaultPath) {
install(NotarizedRoute()) {
@ -299,3 +334,39 @@ fun Routing.overrideMediaTypes() {
}
}
}
fun Routing.postNoReqBody() {
route("/no_req_body") {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
responseType<TestResponse>()
description("Cool response")
responseCode(HttpStatusCode.Created)
}
}
}
}
}
fun Routing.fieldOutsideConstructor() {
route("/field_outside_constructor") {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
requestType<SomethingSimilar>()
description("A cool request")
}
response {
responseType<TestResponse>()
description("Cool response")
responseCode(HttpStatusCode.Created)
}
}
}
}
}

View File

@ -1,11 +1,13 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.Barzo
import io.bkbn.kompendium.core.fixtures.ChillaxificationMaximization
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.Flibbity
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
import io.bkbn.kompendium.core.fixtures.Foosy
import io.bkbn.kompendium.core.fixtures.Gibbity
import io.bkbn.kompendium.core.fixtures.Gizmo
import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics
import io.bkbn.kompendium.core.fixtures.Page
import io.ktor.server.routing.Routing
@ -20,3 +22,5 @@ fun Routing.genericPolymorphicResponse() = basicGetGenerator<Flibbity<Double>>()
fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator<Flibbity<FlibbityGibbit>>()
fun Routing.nestedGenericCollection() = basicGetGenerator<Page<Int>>()
fun Routing.nestedGenericMultipleParamsCollection() = basicGetGenerator<MultiNestedGenerics<String, ComplexRequest>>()
fun Routing.overrideSealedTypeIdentifier() = basicGetGenerator<ChillaxificationMaximization>()
fun Routing.subtypeNotCompleteSetOfParentProperties() = basicGetGenerator<Gizmo>()

View File

@ -14,6 +14,7 @@ import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
import io.ktor.server.routing.Routing
import io.ktor.server.routing.route
import io.ktor.server.routing.param
fun Routing.simplePathParsing() {
route("/this") {
@ -100,3 +101,32 @@ fun Routing.trailingSlash() {
}
}
}
fun Routing.paramWrapper() {
route("/test") {
param("a") {
param("b") {
param("c") {
install(NotarizedRoute()) {
parameters = listOf(
Parameter(
name = "test",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING
)
)
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
}
}
}
}
}

View File

@ -86,12 +86,19 @@
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.ComplexGibbit"
]
}
},
"required": [
"b",
"c",
"z"
"z",
"type"
]
},
"SimpleGibbit": {
@ -102,10 +109,17 @@
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.SimpleGibbit"
]
}
},
"required": [
"a"
"a",
"type"
]
},
"FlibbityGibbit": {

View File

@ -82,11 +82,18 @@
},
"f": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Bibbity"
]
}
},
"required": [
"b",
"f"
"f",
"type"
]
},
"Gibbity-String": {
@ -94,10 +101,17 @@
"properties": {
"a": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Gibbity"
]
}
},
"required": [
"a"
"a",
"type"
]
},
"Flibbity-String": {

View File

@ -65,12 +65,19 @@
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.ComplexGibbit"
]
}
},
"required": [
"b",
"c",
"z"
"z",
"type"
]
},
"SimpleGibbit": {
@ -81,10 +88,17 @@
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.SimpleGibbit"
]
}
},
"required": [
"a"
"a",
"type"
]
},
"FlibbityGibbit": {

View File

@ -65,12 +65,19 @@
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.ComplexGibbit"
]
}
},
"required": [
"b",
"c",
"z"
"z",
"type"
]
},
"SimpleGibbit": {
@ -81,10 +88,17 @@
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.SimpleGibbit"
]
}
},
"required": [
"a"
"a",
"type"
]
},
"List-FlibbityGibbit": {

View File

@ -65,12 +65,19 @@
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.ComplexGibbit"
]
}
},
"required": [
"b",
"c",
"z"
"z",
"type"
]
},
"SimpleGibbit": {
@ -81,10 +88,17 @@
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.SimpleGibbit"
]
}
},
"required": [
"a"
"a",
"type"
]
},
"Map-String-FlibbityGibbit": {

View File

@ -62,11 +62,18 @@
"f": {
"type": "number",
"format": "double"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Bibbity"
]
}
},
"required": [
"b",
"f"
"f",
"type"
]
},
"Gibbity-Double": {
@ -75,10 +82,17 @@
"a": {
"type": "number",
"format": "double"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Gibbity"
]
}
},
"required": [
"a"
"a",
"type"
]
},
"Flibbity-Double": {

View File

@ -53,40 +53,6 @@
"webhooks": {},
"components": {
"schemas": {
"ComplexGibbit": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"c": {
"type": "number",
"format": "int32"
},
"z": {
"type": "string"
}
},
"required": [
"b",
"c",
"z"
]
},
"SimpleGibbit": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"z": {
"type": "string"
}
},
"required": [
"a"
]
},
"Bibbity-FlibbityGibbit": {
"type": "object",
"properties": {
@ -102,11 +68,66 @@
"$ref": "#/components/schemas/SimpleGibbit"
}
]
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Bibbity"
]
}
},
"required": [
"b",
"f"
"f",
"type"
]
},
"ComplexGibbit": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"c": {
"type": "number",
"format": "int32"
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.ComplexGibbit"
]
}
},
"required": [
"b",
"c",
"z",
"type"
]
},
"SimpleGibbit": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"z": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.SimpleGibbit"
]
}
},
"required": [
"a",
"type"
]
},
"Gibbity-FlibbityGibbit": {
@ -121,10 +142,17 @@
"$ref": "#/components/schemas/SimpleGibbit"
}
]
},
"type": {
"type": "string",
"enum": [
"io.bkbn.kompendium.core.fixtures.Gibbity"
]
}
},
"required": [
"a"
"a",
"type"
]
},
"Flibbity-FlibbityGibbit": {

View File

@ -0,0 +1,92 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com/example",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com/example",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,126 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/example": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A test request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestSimpleRequest-simple"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestCreatedResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32"
}
},
"required": [
"c",
"id"
]
},
"TestSimpleRequest-simple": {
"type": "object",
"properties": {
"a": {
"type": "string",
"description": "A simple description"
},
"b": {
"type": "number",
"format": "int32",
"deprecated": true
}
},
"required": [
"a",
"b"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,168 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/example": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A test request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ComplexRequest-simple"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestCreatedResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32"
}
},
"required": [
"c",
"id"
]
},
"ComplexRequest-simple": {
"type": "object",
"properties": {
"amazingField": {
"type": "string"
},
"org": {
"type": "string"
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem-nested"
},
"description": "A nested item",
"type": "array"
}
},
"required": [
"amazingField",
"org",
"tables"
]
},
"NestedComplexItem-nested": {
"type": "object",
"properties": {
"alias": {
"additionalProperties": {
"$ref": "#/components/schemas/CrazyItem"
},
"type": "object"
},
"name": {
"type": "string",
"description": "A nested description"
}
},
"required": [
"alias",
"name"
]
},
"CrazyItem": {
"type": "object",
"properties": {
"enumeration": {
"$ref": "#/components/schemas/SimpleEnum"
}
},
"required": [
"enumeration"
]
},
"SimpleEnum": {
"type": "string",
"enum": [
"ONE",
"TWO"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,183 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/example": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A test request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MultiNestedGenerics-String-ComplexRequest-simple"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestCreatedResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32"
}
},
"required": [
"c",
"id"
]
},
"MultiNestedGenerics-String-ComplexRequest-simple": {
"type": "object",
"properties": {
"content": {
"additionalProperties": {
"$ref": "#/components/schemas/ComplexRequest-nested"
},
"description": "Getting pretty crazy",
"type": "object"
}
},
"required": [
"content"
]
},
"ComplexRequest-nested": {
"type": "object",
"properties": {
"amazingField": {
"type": "string"
},
"org": {
"type": "string"
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem-nested"
},
"description": "A nested item",
"type": "array"
}
},
"required": [
"amazingField",
"org",
"tables"
]
},
"NestedComplexItem-nested": {
"type": "object",
"properties": {
"alias": {
"additionalProperties": {
"$ref": "#/components/schemas/CrazyItem"
},
"type": "object"
},
"name": {
"type": "string",
"description": "A nested description"
}
},
"required": [
"alias",
"name"
]
},
"CrazyItem": {
"type": "object",
"properties": {
"enumeration": {
"$ref": "#/components/schemas/SimpleEnum"
}
},
"required": [
"enumeration"
]
},
"SimpleEnum": {
"type": "string",
"enum": [
"ONE",
"TWO"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,73 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/enriched": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"201": {
"description": "A good response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse-simple"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse-simple": {
"type": "object",
"properties": {
"c": {
"type": "string",
"description": "A simple description"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,80 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Get an int",
"description": "Get an int",
"parameters": [],
"responses": {
"200": {
"description": "An int",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestCreatedResponse-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestCreatedResponse-example": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32",
"multipleOf": 2,
"maximum": 100,
"minimum": 2
}
},
"required": [
"c",
"id"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,76 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Get a double",
"description": "Get a double",
"parameters": [],
"responses": {
"200": {
"description": "A double",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DoubleResponse-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"DoubleResponse-example": {
"type": "object",
"properties": {
"payload": {
"type": "number",
"format": "double",
"multipleOf": 2.0,
"maximum": 100.0,
"minimum": 2.0
}
},
"required": [
"payload"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,74 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Get a string",
"description": "Get a string with constraints",
"parameters": [],
"responses": {
"200": {
"description": "A string",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestNested-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestNested-example": {
"type": "object",
"properties": {
"nesty": {
"type": "string",
"maxLength": 10,
"minLength": 2
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,73 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Get a string",
"description": "This is a description",
"parameters": [],
"responses": {
"200": {
"description": "A string",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestNested-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestNested-example": {
"type": "object",
"properties": {
"nesty": {
"type": "string",
"pattern": "[a-z]+"
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,74 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Get a string",
"description": "This is a description",
"parameters": [],
"responses": {
"200": {
"description": "A string",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestNested-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestNested-example": {
"type": "object",
"properties": {
"nesty": {
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "image/png"
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,103 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Get an array",
"description": "Get an array of strings",
"parameters": [],
"responses": {
"200": {
"description": "An array",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Page-String-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"Page-String-example": {
"type": "object",
"properties": {
"content": {
"items": {
"type": "string"
},
"maxItems": 10,
"minItems": 2,
"uniqueItems": true,
"type": "array"
},
"number": {
"type": "number",
"format": "int32"
},
"numberOfElements": {
"type": "number",
"format": "int32"
},
"size": {
"type": "number",
"format": "int32"
},
"totalElements": {
"type": "number",
"format": "int64"
},
"totalPages": {
"type": "number",
"format": "int32"
}
},
"required": [
"content",
"number",
"numberOfElements",
"size",
"totalElements",
"totalPages"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,72 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/no_req_body": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"201": {
"description": "Cool response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,110 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"headers": {
"ETag": {
"schema": {
"type": "string"
},
"description": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag",
"required": true,
"deprecated": false
},
"Last-Modified": {
"schema": {
"type": "string"
},
"description": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified",
"required": true,
"deprecated": false
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,91 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/example": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"201": {
"description": "A good response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenericObject-TestSimpleRequest-generic"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"GenericObject-TestSimpleRequest-generic": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/TestSimpleRequest-simple",
"description": "A simple description"
}
},
"required": [
"data"
]
},
"TestSimpleRequest-simple": {
"type": "object",
"properties": {
"a": {
"type": "string",
"description": "A simple description"
},
"b": {
"type": "number",
"format": "int32",
"deprecated": true
}
},
"required": [
"a",
"b"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,82 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "test",
"in": "query",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,136 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "You gotta send it",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
},
"examples": {
"Testerina": {
"value": {
"fieldName": {
"nesty": "asdf"
},
"b": 1.5,
"aaa": []
}
}
}
}
},
"required": false
},
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
},
"examples": {
"Testerino": {
"value": {
"c": "Heya"
}
}
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
},
"TestRequest": {
"type": "object",
"properties": {
"aaa": {
"items": {
"type": "number",
"format": "int64"
},
"type": "array"
},
"b": {
"type": "number",
"format": "double"
},
"fieldName": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"aaa",
"b",
"fieldName"
]
},
"TestNested": {
"type": "object",
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,108 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChillaxificationMaximization"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"Chillax": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"chillax"
]
}
},
"required": [
"a",
"type"
]
},
"ToDaMax": {
"type": "object",
"properties": {
"b": {
"type": "number",
"format": "int32"
},
"type": {
"type": "string",
"enum": [
"maximize"
]
}
},
"required": [
"b",
"type"
]
},
"ChillaxificationMaximization": {
"anyOf": [
{
"$ref": "#/components/schemas/Chillax"
},
{
"$ref": "#/components/schemas/ToDaMax"
}
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,72 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Gizmo"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"Gizmo": {
"type": "object",
"properties": {
"title": {
"type": "string"
}
},
"required": [
"title"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,92 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,94 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/field_outside_constructor": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A cool request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SomethingSimilar"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "Cool response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
},
"SomethingSimilar": {
"type": "object",
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,129 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false,
"security": [
{
"auth-oauth-google": [
"read:pets"
]
}
]
},
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false,
"security": [
{
"auth-oauth-google": [
"write:pets"
]
}
]
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {
"auth-oauth-google": {
"flows": {
"implicit": {
"authorizationUrl": "https://accounts.google.com/o/oauth2/auth",
"scopes": {
"write:pets": "modify pets in your account",
"read:pets": "read your pets"
}
}
},
"type": "oauth2"
}
}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,140 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/": {
"post": {
"tags": [],
"summary": "This is a summary",
"description": "This is a description",
"parameters": [],
"requestBody": {
"description": "This is a request description",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
},
"examples": {
"Testerina": {
"value": {
"fieldName": {
"nesty": "asdf"
},
"b": 1.5,
"aaa": []
},
"summary": "summary",
"description": "description"
}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "This is a response description",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
},
"examples": {
"Testerino": {
"value": {
"c": "Heya"
},
"summary": "summary",
"description": "description"
}
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
},
"TestRequest": {
"type": "object",
"properties": {
"aaa": {
"items": {
"type": "number",
"format": "int64"
},
"type": "array"
},
"b": {
"type": "number",
"format": "double"
},
"fieldName": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"aaa",
"b",
"fieldName"
]
},
"TestNested": {
"type": "object",
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,54 +0,0 @@
package io.bkbn.kompendium.core.fixtures
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
/*
These are test implementation and may well be a good starting point for creating production ones.
Both Gson and Jackson are complex and can achieve this things is more than one way therefore
these will not always work hence why they are in the test package
*/
class GsonSchemaConfigurator: SchemaConfigurator {
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> {
// NOTE: This is test logic Expose is set at a global Gson level so configure to match your Gson set up
val hasAnyExpose = clazz.memberProperties.any { it.hasJavaAnnotation<Expose>() }
return if(hasAnyExpose) {
clazz.memberProperties
.filter { it.hasJavaAnnotation<Expose>() }
} else clazz.memberProperties
}
override fun serializableName(property: KProperty1<out Any, *>): String =
property.getJavaAnnotation<SerializedName>()?.value?: property.name
}
class JacksonSchemaConfigurator: SchemaConfigurator {
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> =
clazz.memberProperties
.filterNot {
it.hasJavaAnnotation<JsonIgnore>()
}
override fun serializableName(property: KProperty1<out Any, *>): String =
property.getJavaAnnotation<JsonProperty>()?.value?: property.name
}
inline fun <reified T: Annotation> KProperty1<*, *>.hasJavaAnnotation(): Boolean {
return javaField?.isAnnotationPresent(T::class.java)?: false
}
inline fun <reified T: Annotation> KProperty1<*, *>.getJavaAnnotation(): T? {
return javaField?.getDeclaredAnnotation(T::class.java)
}

View File

@ -1,7 +0,0 @@
package io.bkbn.kompendium.core.fixtures
enum class SupportedSerializer {
KOTLINX,
GSON,
JACKSON
}

View File

@ -1,10 +1,9 @@
package io.bkbn.kompendium.core.fixtures
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.fixtures.TestSpecs.defaultSpec
import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.oas.OpenApiSpec
@ -15,19 +14,18 @@ import io.kotest.matchers.shouldNot
import io.kotest.matchers.string.beBlank
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.gson.gson
import io.ktor.serialization.jackson.jackson
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.engine.ApplicationEngineEnvironmentBuilder
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.contentnegotiation.ContentNegotiationConfig
import io.ktor.server.routing.Routing
import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.server.testing.testApplication
import kotlin.reflect.KType
import kotlinx.serialization.json.Json
import java.io.File
import kotlin.reflect.KType
object TestHelpers {
private const val OPEN_API_ENDPOINT = "/openapi.json"
@ -43,8 +41,8 @@ object TestHelpers {
* exists as expected, and that the content matches the expected blob found in the specified file
* @param snapshotName The snapshot file to retrieve from the resources folder
*/
private suspend fun ApplicationTestBuilder.compareOpenAPISpec(snapshotName: String) {
val response = client.get(OPEN_API_ENDPOINT)
private suspend fun ApplicationTestBuilder.compareOpenAPISpec(rootPath: String, snapshotName: String) {
val response = client.get("$rootPath$OPEN_API_ENDPOINT")
response shouldHaveStatus HttpStatusCode.OK
response.bodyAsText() shouldNot beBlank()
response.bodyAsText() shouldEqualJson getFileSnapshot(snapshotName)
@ -53,7 +51,7 @@ object TestHelpers {
/**
* This will take a provided JSON snapshot file, retrieve it from the resource folder,
* and build a test ktor server to compare the expected output with the output found in the default
* OpenAPI json endpoint. By default, this will run the same test with Gson, Kotlinx, and Jackson serializers
* OpenAPI json endpoint.
* @param snapshotName The snapshot file to retrieve from the resources folder
*/
fun openApiTestAllSerializers(
@ -61,50 +59,56 @@ object TestHelpers {
customTypes: Map<KType, JsonSchema> = emptyMap(),
applicationSetup: Application.() -> Unit = { },
specOverrides: OpenApiSpec.() -> OpenApiSpec = { this },
routeUnderTest: Routing.() -> Unit
) {
openApiTest(snapshotName, SupportedSerializer.KOTLINX, routeUnderTest, applicationSetup, specOverrides, customTypes)
openApiTest(snapshotName, SupportedSerializer.JACKSON, routeUnderTest, applicationSetup, specOverrides, customTypes)
openApiTest(snapshotName, SupportedSerializer.GSON, routeUnderTest, applicationSetup, specOverrides, customTypes)
}
private fun openApiTest(
snapshotName: String,
serializer: SupportedSerializer,
routeUnderTest: Routing.() -> Unit,
applicationSetup: Application.() -> Unit,
specOverrides: OpenApiSpec.() -> OpenApiSpec,
typeOverrides: Map<KType, JsonSchema> = emptyMap()
) = testApplication {
install(NotarizedApplication()) {
customTypes = typeOverrides
spec = defaultSpec().specOverrides()
schemaConfigurator = when (serializer) {
SupportedSerializer.KOTLINX -> KotlinXSchemaConfigurator()
SupportedSerializer.GSON -> GsonSchemaConfigurator()
SupportedSerializer.JACKSON -> JacksonSchemaConfigurator()
}
}
install(ContentNegotiation) {
when (serializer) {
SupportedSerializer.KOTLINX -> json(Json {
applicationEnvironmentBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
notarizedApplicationConfigOverrides: NotarizedApplication.Config.() -> Unit = {},
contentNegotiation: ContentNegotiationConfig.() -> Unit = {
json(Json {
encodeDefaults = true
explicitNulls = false
serializersModule = KompendiumSerializersModule.module
})
},
routeUnderTest: Routing.() -> Unit
) {
openApiTest(
snapshotName,
routeUnderTest,
applicationSetup,
specOverrides,
customTypes,
notarizedApplicationConfigOverrides,
contentNegotiation,
applicationEnvironmentBuilder
)
}
SupportedSerializer.GSON -> gson()
SupportedSerializer.JACKSON -> jackson(ContentType.Application.Json) {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
private fun openApiTest(
snapshotName: String,
routeUnderTest: Routing.() -> Unit,
applicationSetup: Application.() -> Unit,
specOverrides: OpenApiSpec.() -> OpenApiSpec,
typeOverrides: Map<KType, JsonSchema> = emptyMap(),
notarizedApplicationConfigOverrides: NotarizedApplication.Config.() -> Unit,
contentNegotiation: ContentNegotiationConfig.() -> Unit,
applicationBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit
) = testApplication {
environment(applicationBuilder)
install(NotarizedApplication()) {
customTypes = typeOverrides
spec = { specOverrides(defaultSpec()) }
schemaConfigurator = KotlinXSchemaConfigurator()
notarizedApplicationConfigOverrides()
}
install(ContentNegotiation) {
contentNegotiation()
}
application(applicationSetup)
routing {
swagger()
redoc()
routeUnderTest()
}
compareOpenAPISpec(snapshotName)
val root = ApplicationEngineEnvironmentBuilder().apply(applicationBuilder).rootPath
compareOpenAPISpec(root, snapshotName)
}
}

View File

@ -1,17 +1,18 @@
package io.bkbn.kompendium.core.fixtures
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
@Serializable
data class TestNested(val nesty: String)
@Serializable
data class DoubleResponse(val payload: Double)
@Serializable
data class TestRequest(
val fieldName: TestNested,
@ -88,6 +89,25 @@ data class AnothaJamma(val b: Float) : SlammaJamma
data class InsaneJamma(val c: SlammaJamma) : SlammaJamma
sealed interface ChillaxificationMaximization
@Serializable
@SerialName("chillax")
data class Chillax(val a: String) : ChillaxificationMaximization
@Serializable
@SerialName("maximize")
data class ToDaMax(val b: Int) : ChillaxificationMaximization
sealed class Gadget(
open val title: String,
open val description: String
)
class Gizmo(
override val title: String,
) : Gadget(title, "Just a gizmo")
sealed interface Flibbity<T>
data class Gibbity<T>(val a: T) : Flibbity<T>
@ -155,9 +175,7 @@ object Nested {
@Serializable
data class TransientObject(
@field:Expose
val nonTransient: String,
@field:JsonIgnore
@Transient
val transient: String = "transient"
)
@ -171,12 +189,14 @@ data class UnbackedObject(
@Serializable
data class SerialNameObject(
@field:JsonProperty("snake_case_name")
@field:SerializedName("snake_case_name")
@SerialName("snake_case_name")
val camelCaseName: String
)
data class GenericObject<T>(
val data: T
)
enum class Color {
RED,
GREEN,
@ -187,3 +207,8 @@ enum class Color {
data class ObjectWithEnum(
val color: Color
)
@Serializable
data class SomethingSimilar(val a: String) {
val b = "something else"
}

View File

@ -22,6 +22,8 @@ formatting:
indentSize: 2
ImportOrdering:
active: false
EnumEntryNameCase:
active: false
naming:
ConstructorParameterNaming:
active: false

View File

@ -6,4 +6,8 @@
* [Notarized Route](plugins/notarized_route.md)
* [Notarized Locations](plugins/notarized_locations.md)
* [Notarized Resources](plugins/notarized_resources.md)
* [Concepts](concepts/index.md)
* [Enrichment](concepts/enrichment.md)
* [Helpers](helpers/index.md)
* [Protobuf java converter](helpers/protobuf_java_converter.md)
* [The Playground](playground.md)

106
docs/concepts/enrichment.md Normal file
View File

@ -0,0 +1,106 @@
Kompendium allows users to enrich their data types with additional information. This can be done by defining a
`TypeEnrichment` object and passing it to the `enrichment` parameter of the relevant `requestType` or `responseType`.
```kotlin
data class SimpleData(val a: String, val b: Int? = null)
val myEnrichment = TypeEnrichment<SimpleData>(id = "simple-enrichment") {
SimpleData::a {
description = "This will update the field description"
}
SimpleData::b {
// Will indicate in the UI that the field will be removed soon
deprecated = true
}
}
// In your route documentation
fun Routing.enrichedSimpleRequest() {
route("/example") {
install(NotarizedRoute()) {
parameters = TestModules.defaultParams
post = PostInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
request {
requestType<SimpleData>(enrichment = myEnrichment) // Simply attach the enrichment to the request
description("A test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(TestModules.defaultResponseDescription)
}
}
}
}
}
```
{% hint style="warning" %}
An enrichment must provide an `id` field that is unique to the data class that is being enriched. This is because
under the hood, Kompendium appends this id to the data class identifier in order to support multiple different
enrichments
on the same data class.
If you provide duplicate ids, all but the first enrichment will be ignored, as Kompendium will view that as a cache hit,
and skip analyzing the new enrichment.
{% endhint %}
### Nested Enrichments
Enrichments are portable and composable, meaning that we can take an enrichment for a child data class
and apply it inside a parent data class using the `typeEnrichment` property.
```kotlin
data class ParentData(val a: String, val b: ChildData)
data class ChildData(val c: String, val d: Int? = null)
val childEnrichment = TypeEnrichment<ChildData>(id = "child-enrichment") {
ChildData::c {
description = "This will update the field description of field c on child data"
}
ChildData::d {
description = "This will update the field description of field d on child data"
}
}
val parentEnrichment = TypeEnrichment<ParentData>(id = "parent-enrichment") {
ParentData::a {
description = "This will update the field description"
}
ParentData::b {
description = "This will update the field description of field b on parent data"
typeEnrichment = childEnrichment // Will apply the child enrichment to the internals of field b
}
}
```
## Available Enrichments
All enrichments support the following properties:
- description -> Provides a reader friendly description of the field in the object
- deprecated -> Indicates that the field is deprecated and should not be used
### String
- minLength -> The minimum length of the string
- maxLength -> The maximum length of the string
- pattern -> A regex pattern that the string must match
- contentEncoding -> The encoding of the string
- contentMediaType -> The media type of the string
### Numbers
- minimum -> The minimum value of the number
- maximum -> The maximum value of the number
- exclusiveMinimum -> Indicates that the minimum value is exclusive
- exclusiveMaximum -> Indicates that the maximum value is exclusive
- multipleOf -> Indicates that the number must be a multiple of the provided value
### Arrays
- minItems -> The minimum number of items in the array
- maxItems -> The maximum number of items in the array
- uniqueItems -> Indicates that the array must contain unique items

2
docs/concepts/index.md Normal file
View File

@ -0,0 +1,2 @@
Various concepts that are core to Kompendium but not necessarily exclusive
to any given module or plugin

7
docs/helpers/index.md Normal file
View File

@ -0,0 +1,7 @@
Helper modules help you to interact with Kompendium.
Some functionality is not possible or difficult to do with Kompendium by default. Modules in this folder help you to get
functionality that would otherwise be difficult.
The first one of which is [Protobuf java converter](protobuf_java_converter.md) which translates java protobuf classes
to `customTypes` entries.

View File

@ -0,0 +1,153 @@
The `Protobuf java converter` functions allow you to generate documentation from your generated Java classes.
Since Kompendium relies a lot on `KProperties` we have yet to find a way to connect this with our Java.
For now the documentation is generated for the `customTypes` in `NotarizedApplication`.
## Usage with Kotlinx
setup:
```kotlin
install(ContentNegotiation) {
json(Json {
encodeDefaults = false
// Combine the kompendium serializers with your custom java proto serializers
serializersModule =
KompendiumSerializersModule.module + SerializersModule { serializersModule = yourCustomProtoSerializers }
})
}
```
For one message and all its nested sub messages:
```kotlin
private fun Application.mainModule() {
// ...
install(NotarizedApplication()) {
spec = baseSpec
customTypes = MyJavaProto.getDefaultInstance().createCustomTypesForTypeAndSubTypes().toMap()
}
}
```
For multiple messages and their submesages:
```kotlin
private fun Application.mainModule() {
// ...
install(NotarizedApplication()) {
spec = baseSpec
customTypes = MyJavaProto.getDefaultInstance().createCustomTypesForTypeAndSubTypes()
.plus(AnotherJavaProto.getDefaultInstance().createCustomTypesForTypeAndSubTypes()).toMap()
}
}
```
### Example User
The protobuf that is used on our endpoint
```proto
message User {
string id = 1;
string email = 2;
string mobile_phone = 3;
string name = 4;
}
```
A custom serializer deserializer:
```kotlin
@OptIn(ExperimentalSerializationApi::class)
object UserSerializer : KSerializer<User> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("User") {
element("id", serialDescriptor<String>())
element("email", serialDescriptor<String>())
element("mobile_phone", serialDescriptor<String>())
element("name", serialDescriptor<String>())
}
override fun deserialize(decoder: Decoder): User {
return decoder.decodeStructure(descriptor) {
var id: String? = null
var email: String? = null
var mobilePhone: String? = null
var name: String? = null
loop@ while (true) {
when (val index = decodeElementIndex(descriptor)) {
CompositeDecoder.DECODE_DONE -> break@loop
0 -> id = decodeStringElement(descriptor, index)
1 -> email = decodeStringElement(descriptor, index)
2 -> mobilePhone = decodeStringElement(descriptor, index)
3 -> name = decodeStringElement(descriptor, index)
else -> throw RuntimeException(
"Unexpected index field ${descriptor.getElementName(index)}"
)
}
}
// building the protobuf object
val user = User.newBuilder().apply {
id?.let { v -> this.id = v }
email?.let { v -> this.email = v }
mobilePhone?.let { v -> this.mobilePhone = v }
name?.let { v -> this.name = v }
}.build()
user
}
}
override fun serialize(encoder: Encoder, value: User) {
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.id)
encodeStringElement(descriptor, 1, value.email)
encodeStringElement(descriptor, 2, value.mobilePhone)
encodeStringElement(descriptor, 3, value.name)
}
}
}
```
Setting the content type:
```kotlin
install(ContentNegotiation) {
json(Json {
encodeDefaults = false
// Combine the kompendium serializers with your custom java proto serializers
serializersModule =
KompendiumSerializersModule.module + SerializersModule {
serializersModule = SerializersModule {
contextual(UserSerializer)
}
}
})
}
```
The installation of the noterized application:
```kotlin
install(NotarizedApplication()) {
spec = baseSpec
customTypes = User.getDefaultInstance().createCustomTypesForTypeAndSubTypes().toMap()
}
```
Route configuration as you normally would with one exception which is `createType()` to create kotlin type from a javaClass.
```kotlin
private fun Route.userDocumentation() {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary("My User API")
description("Create a user")
request {
requestType(User::class.createType())
description("My user creation object")
}
response {
responseCode(HttpStatusCode.OK)
responseType(CreateUserResponse::class.createType())
description("Returns simulation object")
}
canRespond {
responseCode(HttpStatusCode.NotFound)
responseType<String>()
description("Indicates that the user could not be found")
}
}
}
}
```

View File

@ -0,0 +1,34 @@
plugins {
kotlin("jvm")
id("io.bkbn.sourdough.library.jvm")
id("io.gitlab.arturbosch.detekt")
id("com.adarshr.test-logger")
id("maven-publish")
id("java-library")
id("signing")
id("org.jetbrains.kotlinx.kover")
}
sourdoughLibrary {
libraryName.set("Kompendium Type Enrichment")
libraryDescription.set("Utility library for creating portable type enrichments")
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
}
dependencies {
// Versions
val detektVersion: String by project
// Formatting
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")
testImplementation(testFixtures(projects.kompendiumCore))
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

@ -0,0 +1,3 @@
package io.bkbn.kompendium.enrichment
sealed interface Enrichment

View File

@ -0,0 +1,36 @@
package io.bkbn.kompendium.enrichment
/**
* Reference https://json-schema.org/draft/2020-12/json-schema-validation.html#name-multipleof
*/
class PropertyEnrichment : Enrichment {
// Metadata
var deprecated: Boolean? = null
var description: String? = null
var typeEnrichment: TypeEnrichment<*>? = null
// Number and Integer Constraints
var multipleOf: Number? = null
var maximum: Number? = null
var exclusiveMaximum: Number? = null
var minimum: Number? = null
var exclusiveMinimum: Number? = null
// String constraints
var maxLength: Int? = null
var minLength: Int? = null
var pattern: String? = null
var contentEncoding: String? = null
var contentMediaType: String? = null
// TODO how to handle contentSchema?
// Array constraints
var maxItems: Int? = null
var minItems: Int? = null
var uniqueItems: Boolean? = null
// TODO How to handle contains, minContains, maxContains?
// Object constraints
var maxProperties: Int? = null
var minProperties: Int? = null
}

View File

@ -0,0 +1,25 @@
package io.bkbn.kompendium.enrichment
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
class TypeEnrichment<T>(val id: String) : Enrichment {
private val enrichments: MutableMap<KProperty1<*, *>, Enrichment> = mutableMapOf()
fun getEnrichmentForProperty(property: KProperty<*>): Enrichment? = enrichments[property]
operator fun <R> KProperty1<T, R>.invoke(init: PropertyEnrichment.() -> Unit) {
require(!enrichments.containsKey(this)) { "${this.name} has already been registered" }
val propertyEnrichment = PropertyEnrichment()
init.invoke(propertyEnrichment)
enrichments[this] = propertyEnrichment
}
companion object {
inline operator fun <reified T> invoke(id: String, init: TypeEnrichment<T>.() -> Unit): TypeEnrichment<T> {
val builder = TypeEnrichment<T>(id)
return builder.apply(init)
}
}
}

View File

@ -1,13 +1,14 @@
# Kompendium
project.version=3.8.0
project.version=4.0.0-alpha
# Kotlin
kotlin.code.style=official
# Gradle
org.gradle.vfs.watch=true
org.gradle.vfs.verbose=true
org.gradle.jvmargs=-Xmx2000m
org.gradle.parallel=true
# Dependencies
ktorVersion=2.1.3
kotestVersion=5.5.4
detektVersion=1.21.0
ktorVersion=2.3.4
kotestVersion=5.7.1
detektVersion=1.22.0

Binary file not shown.

View File

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

31
gradlew vendored
View File

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +80,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,22 +131,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -193,6 +198,10 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
@ -205,6 +214,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

15
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@ -20,8 +20,11 @@ dependencies {
// Versions
val detektVersion: String by project
implementation("org.jetbrains.kotlin:kotlin-reflect:1.7.21")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
// Kompendium
api(projects.kompendiumEnrichment)
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.10")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
// Formatting
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")

View File

@ -1,20 +1,51 @@
package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import kotlinx.serialization.SerialName
import kotlinx.serialization.Transient
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
class KotlinXSchemaConfigurator : SchemaConfigurator {
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> =
clazz.memberProperties
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> {
return clazz.memberProperties
.filterNot { it.hasAnnotation<Transient>() }
.filter { clazz.primaryConstructor?.parameters?.map { it.name }?.contains(it.name) ?: true }
}
override fun serializableName(property: KProperty1<out Any, *>): String =
property.annotations
.filterIsInstance<SerialName>()
.firstOrNull()?.value ?: property.name
override fun sealedTypeEnrichment(
implementationType: KType,
implementationSchema: JsonSchema,
): JsonSchema {
return if (implementationSchema is TypeDefinition && implementationSchema.type == "object") {
implementationSchema.copy(
required = implementationSchema.required?.plus("type"),
properties = implementationSchema.properties?.plus(
mapOf(
"type" to EnumDefinition("string", enum = setOf(determineTypeQualifier(implementationType)))
)
)
)
} else {
implementationSchema
}
}
private fun determineTypeQualifier(type: KType): String {
val nameOverrideAnnotation = (type.classifier as KClass<*>).findAnnotation<SerialName>()
return nameOverrideAnnotation?.value ?: (type.classifier as KClass<*>).qualifiedName!!
}
}

View File

@ -1,17 +1,16 @@
package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.KType
interface SchemaConfigurator {
fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>>
fun serializableName(property: KProperty1<out Any, *>): String
open class Default : SchemaConfigurator {
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> =
clazz.memberProperties
override fun serializableName(property: KProperty1<out Any, *>): String = property.name
}
fun sealedTypeEnrichment(
implementationType: KType,
implementationSchema: JsonSchema
): JsonSchema
}

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
@ -9,28 +10,26 @@ import io.bkbn.kompendium.json.schema.handler.EnumHandler
import io.bkbn.kompendium.json.schema.handler.MapHandler
import io.bkbn.kompendium.json.schema.handler.SealedObjectHandler
import io.bkbn.kompendium.json.schema.handler.SimpleObjectHandler
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import java.util.UUID
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.typeOf
import java.util.UUID
object SchemaGenerator {
inline fun <reified T : Any?> fromTypeToSchema(
cache: MutableMap<String, JsonSchema> = mutableMapOf(),
schemaConfigurator: SchemaConfigurator = SchemaConfigurator.Default()
) = fromTypeToSchema(typeOf<T>(), cache, schemaConfigurator)
fun fromTypeToSchema(
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
): JsonSchema {
cache[type.getSimpleSlug()]?.let {
val slug = type.getSlug(enrichment)
cache[slug]?.let {
return it
}
return when (val clazz = type.classifier as KClass<*>) {
Unit::class -> error(
"""
@ -48,14 +47,14 @@ object SchemaGenerator {
Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN)
UUID::class -> checkForNull(type, TypeDefinition.UUID)
else -> when {
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache)
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator)
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator)
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache, enrichment)
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator, enrichment)
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator, enrichment)
else -> {
if (clazz.isSealed) {
SealedObjectHandler.handle(type, clazz, cache, schemaConfigurator)
SealedObjectHandler.handle(type, clazz, cache, schemaConfigurator, enrichment)
} else {
SimpleObjectHandler.handle(type, clazz, cache, schemaConfigurator)
SimpleObjectHandler.handle(type, clazz, cache, schemaConfigurator, enrichment)
}
}
}
@ -65,11 +64,12 @@ object SchemaGenerator {
fun fromTypeOrUnit(
type: KType,
cache: MutableMap<String, JsonSchema> = mutableMapOf(),
schemaConfigurator: SchemaConfigurator
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
): JsonSchema? =
when (type.classifier as KClass<*>) {
Unit::class -> null
else -> fromTypeToSchema(type, cache, schemaConfigurator)
else -> fromTypeToSchema(type, cache, schemaConfigurator, enrichment)
}
private fun checkForNull(type: KType, schema: JsonSchema): JsonSchema = when (type.isMarkedNullable) {

View File

@ -3,4 +3,8 @@ package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class AnyOfDefinition(val anyOf: Set<JsonSchema>) : JsonSchema
data class AnyOfDefinition(
val anyOf: Set<JsonSchema>,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema

View File

@ -4,7 +4,14 @@ import kotlinx.serialization.Serializable
@Serializable
data class ArrayDefinition(
val items: JsonSchema
val items: JsonSchema,
override val deprecated: Boolean? = null,
override val description: String? = null,
// Constraints
val maxItems: Int? = null,
val minItems: Int? = null,
val uniqueItems: Boolean? = null,
) : JsonSchema {
val type: String = "array"
}

View File

@ -5,5 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class EnumDefinition(
val type: String,
val enum: Set<String>
val enum: Set<String>,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema

View File

@ -11,6 +11,9 @@ import kotlinx.serialization.encoding.Encoder
@Serializable(with = JsonSchema.Serializer::class)
sealed interface JsonSchema {
val description: String?
val deprecated: Boolean?
object Serializer : KSerializer<JsonSchema> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("JsonSchema", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): JsonSchema {

View File

@ -4,7 +4,9 @@ import kotlinx.serialization.Serializable
@Serializable
data class MapDefinition(
val additionalProperties: JsonSchema
val additionalProperties: JsonSchema,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema {
val type: String = "object"
}

View File

@ -3,4 +3,8 @@ package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class NullableDefinition(val type: String = "null") : JsonSchema
data class NullableDefinition(
val type: String = "null",
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema

View File

@ -3,6 +3,10 @@ package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class OneOfDefinition(val oneOf: Set<JsonSchema>) : JsonSchema {
data class OneOfDefinition(
val oneOf: Set<JsonSchema>,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema {
constructor(vararg types: JsonSchema) : this(types.toSet())
}

View File

@ -3,4 +3,8 @@ package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class ReferenceDefinition(val `$ref`: String) : JsonSchema
data class ReferenceDefinition(
val `$ref`: String,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.json.schema.definition
import io.bkbn.kompendium.json.schema.util.Serializers
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@ -7,10 +8,35 @@ import kotlinx.serialization.Serializable
data class TypeDefinition(
val type: String,
val format: String? = null,
val description: String? = null,
val properties: Map<String, JsonSchema>? = null,
val required: Set<String>? = null,
@Contextual val default: Any? = null,
override val deprecated: Boolean? = null,
override val description: String? = null,
// Constraints
// Number
@Serializable(with = Serializers.Number::class)
val multipleOf: Number? = null,
@Serializable(with = Serializers.Number::class)
val maximum: Number? = null,
@Serializable(with = Serializers.Number::class)
val exclusiveMaximum: Number? = null,
@Serializable(with = Serializers.Number::class)
val minimum: Number? = null,
@Serializable(with = Serializers.Number::class)
val exclusiveMinimum: Number? = null,
// String
val maxLength: Int? = null,
val minLength: Int? = null,
val pattern: String? = null,
val contentEncoding: String? = null,
val contentMediaType: String? = null,
// Object
val maxProperties: Int? = null,
val minProperties: Int? = null,
) : JsonSchema {
fun withDefault(default: Any): TypeDefinition = this.copy(default = default)

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
@ -9,17 +10,22 @@ import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import kotlin.reflect.KType
object CollectionHandler {
fun handle(type: KType, cache: MutableMap<String, JsonSchema>, schemaConfigurator: SchemaConfigurator): JsonSchema {
fun handle(
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
): JsonSchema {
val collectionType = type.arguments.first().type
?: error("This indicates a bug in Kompendium, please open a GitHub issue!")
val typeSchema = SchemaGenerator.fromTypeToSchema(collectionType, cache, schemaConfigurator).let {
val typeSchema = SchemaGenerator.fromTypeToSchema(collectionType, cache, schemaConfigurator, enrichment).let {
if (it is TypeDefinition && it.type == "object") {
cache[collectionType.getSimpleSlug()] = it
ReferenceDefinition(collectionType.getReferenceSlug())
cache[collectionType.getSlug(enrichment)] = it
ReferenceDefinition(collectionType.getReferenceSlug(enrichment))
} else {
it
}

View File

@ -1,16 +1,22 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import kotlin.reflect.KClass
import kotlin.reflect.KType
object EnumHandler {
fun handle(type: KType, clazz: KClass<*>, cache: MutableMap<String, JsonSchema>): JsonSchema {
cache[type.getSimpleSlug()] = ReferenceDefinition(type.getReferenceSlug())
fun handle(
type: KType,
clazz: KClass<*>,
cache: MutableMap<String, JsonSchema>,
enrichment: TypeEnrichment<*>? = null
): JsonSchema {
cache[type.getSlug(enrichment)] = ReferenceDefinition(type.getReferenceSlug(enrichment))
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
return EnumDefinition(type = "string", enum = options)

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.JsonSchema
@ -9,21 +10,26 @@ import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import kotlin.reflect.KClass
import kotlin.reflect.KType
object MapHandler {
fun handle(type: KType, cache: MutableMap<String, JsonSchema>, schemaConfigurator: SchemaConfigurator): JsonSchema {
fun handle(
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
): JsonSchema {
require(type.arguments.first().type?.classifier as KClass<*> == String::class) {
"JSON requires that map keys MUST be Strings. You provided ${type.arguments.first().type}"
}
val valueType = type.arguments[1].type ?: error("this indicates a bug in Kompendium, please open a GitHub issue")
val valueSchema = SchemaGenerator.fromTypeToSchema(valueType, cache, schemaConfigurator).let {
val valueSchema = SchemaGenerator.fromTypeToSchema(valueType, cache, schemaConfigurator, enrichment).let {
if (it is TypeDefinition && it.type == "object") {
cache[valueType.getSimpleSlug()] = it
ReferenceDefinition(valueType.getReferenceSlug())
cache[valueType.getSlug(enrichment)] = it
ReferenceDefinition(valueType.getReferenceSlug(enrichment))
} else {
it
}

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.AnyOfDefinition
@ -7,7 +8,7 @@ import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.createType
@ -18,15 +19,20 @@ object SealedObjectHandler {
type: KType,
clazz: KClass<*>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null,
): JsonSchema {
val subclasses = clazz.sealedSubclasses
.map { it.createType(type.arguments) }
.map { t ->
SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator).let { js ->
SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator, enrichment)
.let {
schemaConfigurator.sealedTypeEnrichment(t, it)
}.let { js ->
if (js is TypeDefinition && js.type == "object") {
cache[t.getSimpleSlug()] = js
ReferenceDefinition(t.getReferenceSlug())
val slug = t.getSlug(enrichment)
cache[slug] = js
ReferenceDefinition(t.getReferenceSlug(enrichment))
} else {
js
}

View File

@ -1,16 +1,21 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.PropertyEnrichment
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.AnyOfDefinition
import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.MapDefinition
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType
@ -26,26 +31,33 @@ object SimpleObjectHandler {
type: KType,
clazz: KClass<*>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>?,
): JsonSchema {
cache[type.getSimpleSlug()] = ReferenceDefinition(type.getReferenceSlug())
cache[type.getSlug(enrichment)] = ReferenceDefinition(type.getReferenceSlug(enrichment))
val typeMap = clazz.typeParameters.zip(type.arguments).toMap()
val props = schemaConfigurator.serializableMemberProperties(clazz)
.filterNot { it.javaField == null }
.associate { prop ->
val propTypeEnrichment = when (val pe = enrichment?.getEnrichmentForProperty(prop)) {
is PropertyEnrichment -> pe
else -> null
}
val schema = when (prop.needsToInjectGenerics(typeMap)) {
true -> handleNestedGenerics(typeMap, prop, cache, schemaConfigurator)
true -> handleNestedGenerics(typeMap, prop, cache, schemaConfigurator, propTypeEnrichment)
false -> when (typeMap.containsKey(prop.returnType.classifier)) {
true -> handleGenericProperty(prop, typeMap, cache, schemaConfigurator)
false -> handleProperty(prop, cache, schemaConfigurator)
true -> handleGenericProperty(prop, typeMap, cache, schemaConfigurator, propTypeEnrichment)
false -> handleProperty(prop, cache, schemaConfigurator, propTypeEnrichment?.typeEnrichment)
}
}
val nullCheckSchema = when (prop.returnType.isMarkedNullable && !schema.isNullable()) {
true -> OneOfDefinition(NullableDefinition(), schema)
false -> schema
val enrichedSchema = propTypeEnrichment?.applyToSchema(schema) ?: schema
val nullCheckSchema = when (prop.returnType.isMarkedNullable && !enrichedSchema.isNullable()) {
true -> OneOfDefinition(NullableDefinition(), enrichedSchema)
false -> enrichedSchema
}
schemaConfigurator.serializableName(prop) to nullCheckSchema
@ -90,7 +102,8 @@ object SimpleObjectHandler {
typeMap: Map<KTypeParameter, KTypeProjection>,
prop: KProperty<*>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator
schemaConfigurator: SchemaConfigurator,
propEnrichment: PropertyEnrichment?
): JsonSchema {
val propClass = prop.returnType.classifier as KClass<*>
val types = prop.returnType.arguments.map {
@ -98,10 +111,11 @@ object SimpleObjectHandler {
typeMap.filterKeys { k -> k.name == typeSymbol }.values.first()
}
val constructedType = propClass.createType(types)
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator).let {
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator, propEnrichment?.typeEnrichment)
.let {
if (it.isOrContainsObjectOrEnumDef()) {
cache[constructedType.getSimpleSlug()] = it
ReferenceDefinition(prop.returnType.getReferenceSlug())
cache[constructedType.getSlug(propEnrichment)] = it
ReferenceDefinition(prop.returnType.getReferenceSlug(propEnrichment))
} else {
it
}
@ -112,14 +126,15 @@ object SimpleObjectHandler {
prop: KProperty<*>,
typeMap: Map<KTypeParameter, KTypeProjection>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator
schemaConfigurator: SchemaConfigurator,
propEnrichment: PropertyEnrichment?
): JsonSchema {
val type = typeMap[prop.returnType.classifier]?.type
?: error("This indicates a bug in Kompendium, please open a GitHub issue")
return SchemaGenerator.fromTypeToSchema(type, cache, schemaConfigurator).let {
return SchemaGenerator.fromTypeToSchema(type, cache, schemaConfigurator, propEnrichment?.typeEnrichment).let {
if (it.isOrContainsObjectOrEnumDef()) {
cache[type.getSimpleSlug()] = it
ReferenceDefinition(type.getReferenceSlug())
cache[type.getSlug(propEnrichment?.typeEnrichment)] = it
ReferenceDefinition(type.getReferenceSlug(propEnrichment?.typeEnrichment))
} else {
it
}
@ -129,12 +144,13 @@ object SimpleObjectHandler {
private fun handleProperty(
prop: KProperty<*>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator
schemaConfigurator: SchemaConfigurator,
propEnrichment: TypeEnrichment<*>?
): JsonSchema =
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator).let {
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator, propEnrichment).let {
if (it.isOrContainsObjectOrEnumDef()) {
cache[prop.returnType.getSimpleSlug()] = it
ReferenceDefinition(prop.returnType.getReferenceSlug())
cache[prop.returnType.getSlug(propEnrichment)] = it
ReferenceDefinition(prop.returnType.getReferenceSlug(propEnrichment))
} else {
it
}
@ -149,4 +165,37 @@ object SimpleObjectHandler {
}
private fun JsonSchema.isNullable(): Boolean = this is OneOfDefinition && this.oneOf.any { it is NullableDefinition }
private fun PropertyEnrichment.applyToSchema(schema: JsonSchema): JsonSchema = when (schema) {
is AnyOfDefinition -> schema.copy(deprecated = deprecated, description = description)
is ArrayDefinition -> schema.copy(
deprecated = deprecated,
description = description,
minItems = minItems,
maxItems = maxItems,
uniqueItems = uniqueItems,
)
is EnumDefinition -> schema.copy(deprecated = deprecated, description = description)
is MapDefinition -> schema.copy(deprecated = deprecated, description = description)
is NullableDefinition -> schema.copy(deprecated = deprecated, description = description)
is OneOfDefinition -> schema.copy(deprecated = deprecated, description = description)
is ReferenceDefinition -> schema.copy(deprecated = deprecated, description = description)
is TypeDefinition -> schema.copy(
deprecated = deprecated,
description = description,
multipleOf = multipleOf,
maximum = maximum,
exclusiveMaximum = exclusiveMaximum,
minimum = minimum,
exclusiveMinimum = exclusiveMinimum,
maxLength = maxLength,
minLength = minLength,
pattern = pattern,
contentEncoding = contentEncoding,
contentMediaType = contentMediaType,
maxProperties = maxProperties,
minProperties = minProperties,
)
}
}

View File

@ -1,18 +1,35 @@
package io.bkbn.kompendium.json.schema.util
import io.bkbn.kompendium.enrichment.Enrichment
import io.bkbn.kompendium.enrichment.PropertyEnrichment
import io.bkbn.kompendium.enrichment.TypeEnrichment
import kotlin.reflect.KClass
import kotlin.reflect.KType
object Helpers {
private const val COMPONENT_SLUG = "#/components/schemas"
const val COMPONENT_SLUG = "#/components/schemas"
fun KType.getSlug(enrichment: Enrichment? = null) = when (enrichment) {
is TypeEnrichment<*> -> getEnrichedSlug(enrichment)
is PropertyEnrichment -> error("Slugs should not be generated for field enrichments")
else -> getSimpleSlug()
}
fun KType.getSimpleSlug(): String = when {
this.arguments.isNotEmpty() -> genericNameAdapter(this, classifier as KClass<*>)
else -> (classifier as KClass<*>).kompendiumSlug() ?: error("Could not determine simple name for $this")
}
fun KType.getReferenceSlug(): String = when {
private fun KType.getEnrichedSlug(enrichment: TypeEnrichment<*>) = getSimpleSlug() + "-${enrichment.id}"
fun KType.getReferenceSlug(enrichment: Enrichment? = null): String = when (enrichment) {
is TypeEnrichment<*> -> getSimpleReferenceSlug() + "-${enrichment.id}"
is PropertyEnrichment -> error("Reference slugs should never be generated for field enrichments")
else -> getSimpleReferenceSlug()
}
private fun KType.getSimpleReferenceSlug() = when {
arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}"
else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).kompendiumSlug()}"
}

View File

@ -0,0 +1,43 @@
package io.bkbn.kompendium.json.schema.util
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.UUID
import kotlin.Number as KNumber
object Serializers {
object Uuid : KSerializer<UUID> {
override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): UUID {
return UUID.fromString(decoder.decodeString())
}
override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString())
}
}
object Number : KSerializer<KNumber> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Number", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): KNumber {
TODO("Not yet implemented")
}
override fun serialize(encoder: Encoder, value: KNumber) {
when (value) {
is Int -> encoder.encodeInt(value)
is Long -> encoder.encodeLong(value)
is Double -> encoder.encodeDouble(value)
is Float -> encoder.encodeFloat(value)
else -> throw IllegalArgumentException("Number is not a valid type")
}
}
}
}

View File

@ -2,21 +2,25 @@ package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
import io.bkbn.kompendium.core.fixtures.NestedComplexItem
import io.bkbn.kompendium.core.fixtures.ObjectWithEnum
import io.bkbn.kompendium.core.fixtures.SerialNameObject
import io.bkbn.kompendium.core.fixtures.SimpleEnum
import io.bkbn.kompendium.core.fixtures.SlammaJamma
import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
import io.bkbn.kompendium.core.fixtures.TransientObject
import io.bkbn.kompendium.core.fixtures.UnbackedObject
import io.bkbn.kompendium.core.fixtures.GenericObject
import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.kotest.assertions.json.shouldEqualJson
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import kotlinx.serialization.json.Json
import java.util.UUID
import kotlin.reflect.typeOf
class SchemaGeneratorTest : DescribeSpec({
describe("Scalars") {
@ -59,6 +63,9 @@ class SchemaGeneratorTest : DescribeSpec({
it("Can generate the schema for object with SerialName annotation") {
jsonSchemaTest<SerialNameObject>("T0020__serial_name_object.json")
}
it("Can generate the schema for object with generic property") {
jsonSchemaTest<GenericObject<TestSimpleRequest>>("T0024__generic_object.json")
}
}
describe("Enums") {
it("Can generate the schema for a simple enum") {
@ -88,7 +95,13 @@ class SchemaGeneratorTest : DescribeSpec({
jsonSchemaTest<Map<String, Int>>("T0012__scalar_map.json")
}
it("Throws an error when map keys are not strings") {
shouldThrow<IllegalArgumentException> { SchemaGenerator.fromTypeToSchema<Map<Int, Int>>() }
shouldThrow<IllegalArgumentException> {
SchemaGenerator.fromTypeToSchema(
typeOf<Map<Int, Int>>(),
cache = mutableMapOf(),
schemaConfigurator = KotlinXSchemaConfigurator()
)
}
}
it("Can generate the schema for a map of objects") {
jsonSchemaTest<Map<String, TestResponse>>("T0013__object_map.json")
@ -97,6 +110,54 @@ class SchemaGeneratorTest : DescribeSpec({
jsonSchemaTest<Map<String, Int>?>("T0014__nullable_map.json")
}
}
describe("Enrichment") {
it("Can attach an enrichment to a simple type") {
jsonSchemaTest<TestSimpleRequest>(
snapshotName = "T0022__enriched_simple_object.json",
enrichment = TypeEnrichment("simple") {
TestSimpleRequest::a {
description = "This is a simple description"
}
TestSimpleRequest::b {
deprecated = true
}
}
)
}
it("Can properly assign a reference to a nested enrichment") {
jsonSchemaTest<ComplexRequest>(
snapshotName = "T0023__enriched_nested_reference.json",
enrichment = TypeEnrichment("example") {
ComplexRequest::tables {
description = "Collection of important items"
typeEnrichment = TypeEnrichment("table") {
NestedComplexItem::name {
description = "The name of the table"
}
}
}
}
)
}
it("Can properly assign a reference to a generic object") {
jsonSchemaTest<GenericObject<TestSimpleRequest>>(
snapshotName = "T0025__enrichment_generic_object.json",
enrichment = TypeEnrichment("generic") {
GenericObject<TestSimpleRequest>::data {
description = "This is a generic param"
typeEnrichment = TypeEnrichment("simple") {
TestSimpleRequest::a {
description = "This is a simple description"
}
TestSimpleRequest::b {
deprecated = true
}
}
}
}
)
}
}
}) {
companion object {
private val json = Json {
@ -107,11 +168,14 @@ class SchemaGeneratorTest : DescribeSpec({
private fun JsonSchema.serialize() = json.encodeToString(JsonSchema.serializer(), this)
private inline fun <reified T> jsonSchemaTest(snapshotName: String) {
private inline fun <reified T> jsonSchemaTest(snapshotName: String, enrichment: TypeEnrichment<*>? = null) {
// act
val schema = SchemaGenerator.fromTypeToSchema<T>(schemaConfigurator = KotlinXSchemaConfigurator())
// todo add cache assertions!!!
val schema = SchemaGenerator.fromTypeToSchema(
type = typeOf<T>(),
cache = mutableMapOf(),
schemaConfigurator = KotlinXSchemaConfigurator(),
enrichment = enrichment,
)
// assert
schema.serialize() shouldEqualJson getFileSnapshot(snapshotName)

View File

@ -0,0 +1,18 @@
{
"type": "object",
"properties": {
"a": {
"type": "string",
"description": "This is a simple description"
},
"b": {
"type": "number",
"format": "int32",
"deprecated": true
}
},
"required": [
"a",
"b"
]
}

View File

@ -0,0 +1,23 @@
{
"type": "object",
"properties": {
"amazingField": {
"type": "string"
},
"org": {
"type": "string"
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem-table"
},
"description": "Collection of important items",
"type": "array"
}
},
"required": [
"amazingField",
"org",
"tables"
]
}

Some files were not shown because too many files have changed in this diff Show More