From 113a0ffcafb5489cbcf6ae2a64c24157f478f41e Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 26 Nov 2025 07:28:45 -0300 Subject: [PATCH 01/17] prepare for next development cycle --- jooby/pom.xml | 2 +- modules/jooby-apt/pom.xml | 2 +- modules/jooby-avaje-inject/pom.xml | 2 +- modules/jooby-avaje-jsonb/pom.xml | 2 +- modules/jooby-avaje-validator/pom.xml | 2 +- modules/jooby-awssdk-v1/pom.xml | 2 +- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-bom/pom.xml | 4 ++-- modules/jooby-caffeine/pom.xml | 2 +- modules/jooby-camel/pom.xml | 2 +- modules/jooby-cli/pom.xml | 2 +- modules/jooby-commons-email/pom.xml | 2 +- modules/jooby-conscrypt/pom.xml | 2 +- modules/jooby-db-scheduler/pom.xml | 2 +- modules/jooby-distribution/pom.xml | 2 +- modules/jooby-ebean/pom.xml | 2 +- modules/jooby-flyway/pom.xml | 2 +- modules/jooby-freemarker/pom.xml | 2 +- modules/jooby-gradle-setup/pom.xml | 2 +- modules/jooby-graphiql/pom.xml | 2 +- modules/jooby-graphql/pom.xml | 2 +- modules/jooby-gson/pom.xml | 2 +- modules/jooby-guice/pom.xml | 2 +- modules/jooby-handlebars/pom.xml | 2 +- modules/jooby-hibernate-validator/pom.xml | 2 +- modules/jooby-hibernate/pom.xml | 2 +- modules/jooby-hikari/pom.xml | 2 +- modules/jooby-jackson/pom.xml | 2 +- modules/jooby-jasypt/pom.xml | 2 +- modules/jooby-jdbi/pom.xml | 2 +- modules/jooby-jetty/pom.xml | 2 +- modules/jooby-jstachio/pom.xml | 2 +- modules/jooby-jte/pom.xml | 2 +- modules/jooby-jwt/pom.xml | 2 +- modules/jooby-kafka/pom.xml | 2 +- modules/jooby-kotlin/pom.xml | 2 +- modules/jooby-log4j/pom.xml | 2 +- modules/jooby-logback/pom.xml | 2 +- modules/jooby-maven-plugin/pom.xml | 2 +- modules/jooby-metrics/pom.xml | 2 +- modules/jooby-mutiny/pom.xml | 2 +- modules/jooby-netty/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 2 +- modules/jooby-pac4j/pom.xml | 2 +- modules/jooby-pebble/pom.xml | 2 +- modules/jooby-quartz/pom.xml | 2 +- modules/jooby-reactor/pom.xml | 2 +- modules/jooby-redis/pom.xml | 2 +- modules/jooby-redoc/pom.xml | 2 +- modules/jooby-rocker/pom.xml | 2 +- modules/jooby-run/pom.xml | 2 +- modules/jooby-rxjava3/pom.xml | 2 +- modules/jooby-stork/pom.xml | 2 +- modules/jooby-swagger-ui/pom.xml | 2 +- modules/jooby-test/pom.xml | 2 +- modules/jooby-thymeleaf/pom.xml | 2 +- modules/jooby-undertow/pom.xml | 2 +- modules/jooby-vertx-mysql-client/pom.xml | 2 +- modules/jooby-vertx-pg-client/pom.xml | 2 +- modules/jooby-vertx-sql-client/pom.xml | 2 +- modules/jooby-vertx/pom.xml | 2 +- modules/jooby-whoops/pom.xml | 2 +- modules/jooby-yasson/pom.xml | 2 +- modules/pom.xml | 2 +- pom.xml | 4 ++-- tests/pom.xml | 2 +- 66 files changed, 68 insertions(+), 68 deletions(-) diff --git a/jooby/pom.xml b/jooby/pom.xml index 18708edc3c..424eca7e39 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.0.12 + 4.0.13-SNAPSHOT jooby jooby diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml index 1f915929a4..32ee21294f 100644 --- a/modules/jooby-apt/pom.xml +++ b/modules/jooby-apt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-apt jooby-apt diff --git a/modules/jooby-avaje-inject/pom.xml b/modules/jooby-avaje-inject/pom.xml index 745fd62a3d..73dfa8be4a 100644 --- a/modules/jooby-avaje-inject/pom.xml +++ b/modules/jooby-avaje-inject/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-avaje-inject jooby-avaje-inject diff --git a/modules/jooby-avaje-jsonb/pom.xml b/modules/jooby-avaje-jsonb/pom.xml index af71b2c191..37a365331e 100644 --- a/modules/jooby-avaje-jsonb/pom.xml +++ b/modules/jooby-avaje-jsonb/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-avaje-jsonb jooby-avaje-jsonb diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml index 486f7f4ba2..a02eed303e 100644 --- a/modules/jooby-avaje-validator/pom.xml +++ b/modules/jooby-avaje-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-avaje-validator jooby-avaje-validator diff --git a/modules/jooby-awssdk-v1/pom.xml b/modules/jooby-awssdk-v1/pom.xml index 24be785861..c0211f5c49 100644 --- a/modules/jooby-awssdk-v1/pom.xml +++ b/modules/jooby-awssdk-v1/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-awssdk-v1 jooby-awssdk-v1 diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 43158df9c9..1554c1ca45 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-awssdk-v2 jooby-awssdk-v2 diff --git a/modules/jooby-bom/pom.xml b/modules/jooby-bom/pom.xml index 789cadf64d..0937de6b44 100644 --- a/modules/jooby-bom/pom.xml +++ b/modules/jooby-bom/pom.xml @@ -7,14 +7,14 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT io.jooby jooby-bom jooby-bom pom - 4.0.12 + 4.0.13-SNAPSHOT Jooby (Bill of Materials) https://jooby.io diff --git a/modules/jooby-caffeine/pom.xml b/modules/jooby-caffeine/pom.xml index dc4e51a678..88e09da439 100644 --- a/modules/jooby-caffeine/pom.xml +++ b/modules/jooby-caffeine/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-caffeine jooby-caffeine diff --git a/modules/jooby-camel/pom.xml b/modules/jooby-camel/pom.xml index ed3c28c7e4..815bc8a3ea 100644 --- a/modules/jooby-camel/pom.xml +++ b/modules/jooby-camel/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-camel jooby-camel diff --git a/modules/jooby-cli/pom.xml b/modules/jooby-cli/pom.xml index 62e314ae43..9d5d477d03 100644 --- a/modules/jooby-cli/pom.xml +++ b/modules/jooby-cli/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-cli jooby-cli diff --git a/modules/jooby-commons-email/pom.xml b/modules/jooby-commons-email/pom.xml index 6ea7af57e1..f40d993059 100644 --- a/modules/jooby-commons-email/pom.xml +++ b/modules/jooby-commons-email/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-commons-email jooby-commons-email diff --git a/modules/jooby-conscrypt/pom.xml b/modules/jooby-conscrypt/pom.xml index 5d2c9704c0..895c17033d 100644 --- a/modules/jooby-conscrypt/pom.xml +++ b/modules/jooby-conscrypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-conscrypt jooby-conscrypt diff --git a/modules/jooby-db-scheduler/pom.xml b/modules/jooby-db-scheduler/pom.xml index 7c34b401fe..e5c04015e8 100644 --- a/modules/jooby-db-scheduler/pom.xml +++ b/modules/jooby-db-scheduler/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-db-scheduler jooby-db-scheduler diff --git a/modules/jooby-distribution/pom.xml b/modules/jooby-distribution/pom.xml index 78eae74a90..0b8f5d13f4 100644 --- a/modules/jooby-distribution/pom.xml +++ b/modules/jooby-distribution/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-distribution jooby-distribution diff --git a/modules/jooby-ebean/pom.xml b/modules/jooby-ebean/pom.xml index 57ad711b86..c9fbb8c74e 100644 --- a/modules/jooby-ebean/pom.xml +++ b/modules/jooby-ebean/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-ebean jooby-ebean diff --git a/modules/jooby-flyway/pom.xml b/modules/jooby-flyway/pom.xml index a25f41d3d8..32097e0270 100644 --- a/modules/jooby-flyway/pom.xml +++ b/modules/jooby-flyway/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-flyway jooby-flyway diff --git a/modules/jooby-freemarker/pom.xml b/modules/jooby-freemarker/pom.xml index e129883023..cd9369b01c 100644 --- a/modules/jooby-freemarker/pom.xml +++ b/modules/jooby-freemarker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-freemarker jooby-freemarker diff --git a/modules/jooby-gradle-setup/pom.xml b/modules/jooby-gradle-setup/pom.xml index 4d07020f20..e796c048dd 100644 --- a/modules/jooby-gradle-setup/pom.xml +++ b/modules/jooby-gradle-setup/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-gradle-setup jooby-gradle-setup diff --git a/modules/jooby-graphiql/pom.xml b/modules/jooby-graphiql/pom.xml index 1effa8d9fc..415c7739c5 100644 --- a/modules/jooby-graphiql/pom.xml +++ b/modules/jooby-graphiql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-graphiql jooby-graphiql diff --git a/modules/jooby-graphql/pom.xml b/modules/jooby-graphql/pom.xml index 83d9fc6d50..96121bbb98 100644 --- a/modules/jooby-graphql/pom.xml +++ b/modules/jooby-graphql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-graphql jooby-graphql diff --git a/modules/jooby-gson/pom.xml b/modules/jooby-gson/pom.xml index 4e2b5b3096..b0f586b23b 100644 --- a/modules/jooby-gson/pom.xml +++ b/modules/jooby-gson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-gson jooby-gson diff --git a/modules/jooby-guice/pom.xml b/modules/jooby-guice/pom.xml index e658ca9cd8..fb54623494 100644 --- a/modules/jooby-guice/pom.xml +++ b/modules/jooby-guice/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-guice jooby-guice diff --git a/modules/jooby-handlebars/pom.xml b/modules/jooby-handlebars/pom.xml index 6d54689d22..30b4ffb841 100644 --- a/modules/jooby-handlebars/pom.xml +++ b/modules/jooby-handlebars/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-handlebars jooby-handlebars diff --git a/modules/jooby-hibernate-validator/pom.xml b/modules/jooby-hibernate-validator/pom.xml index e0b2cb99cd..75f1e198ec 100644 --- a/modules/jooby-hibernate-validator/pom.xml +++ b/modules/jooby-hibernate-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-hibernate-validator jooby-hibernate-validator diff --git a/modules/jooby-hibernate/pom.xml b/modules/jooby-hibernate/pom.xml index f9ce71759b..2c157021cd 100644 --- a/modules/jooby-hibernate/pom.xml +++ b/modules/jooby-hibernate/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-hibernate jooby-hibernate diff --git a/modules/jooby-hikari/pom.xml b/modules/jooby-hikari/pom.xml index c761840a16..9387eaa182 100644 --- a/modules/jooby-hikari/pom.xml +++ b/modules/jooby-hikari/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-hikari jooby-hikari diff --git a/modules/jooby-jackson/pom.xml b/modules/jooby-jackson/pom.xml index bc712f27ad..a780eeedc7 100644 --- a/modules/jooby-jackson/pom.xml +++ b/modules/jooby-jackson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-jackson jooby-jackson diff --git a/modules/jooby-jasypt/pom.xml b/modules/jooby-jasypt/pom.xml index 1b221dd97e..0903d8de60 100644 --- a/modules/jooby-jasypt/pom.xml +++ b/modules/jooby-jasypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-jasypt jooby-jasypt diff --git a/modules/jooby-jdbi/pom.xml b/modules/jooby-jdbi/pom.xml index 95d8ff6fad..ff4cc752bd 100644 --- a/modules/jooby-jdbi/pom.xml +++ b/modules/jooby-jdbi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-jdbi jooby-jdbi diff --git a/modules/jooby-jetty/pom.xml b/modules/jooby-jetty/pom.xml index 7622a7d639..c7a66a43c8 100644 --- a/modules/jooby-jetty/pom.xml +++ b/modules/jooby-jetty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-jetty jooby-jetty diff --git a/modules/jooby-jstachio/pom.xml b/modules/jooby-jstachio/pom.xml index 9e58ee3a66..14a3c27fa9 100644 --- a/modules/jooby-jstachio/pom.xml +++ b/modules/jooby-jstachio/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-jstachio jooby-jstachio diff --git a/modules/jooby-jte/pom.xml b/modules/jooby-jte/pom.xml index 31060b4c42..a794fbb846 100644 --- a/modules/jooby-jte/pom.xml +++ b/modules/jooby-jte/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-jte jooby-jte diff --git a/modules/jooby-jwt/pom.xml b/modules/jooby-jwt/pom.xml index 1ffc912f27..23baf4a67d 100644 --- a/modules/jooby-jwt/pom.xml +++ b/modules/jooby-jwt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-jwt jooby-jwt diff --git a/modules/jooby-kafka/pom.xml b/modules/jooby-kafka/pom.xml index b6043d3e7b..0203249609 100644 --- a/modules/jooby-kafka/pom.xml +++ b/modules/jooby-kafka/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-kafka jooby-kafka diff --git a/modules/jooby-kotlin/pom.xml b/modules/jooby-kotlin/pom.xml index 22c9a51b2b..32efb7d334 100644 --- a/modules/jooby-kotlin/pom.xml +++ b/modules/jooby-kotlin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-kotlin jooby-kotlin diff --git a/modules/jooby-log4j/pom.xml b/modules/jooby-log4j/pom.xml index dad66ad22f..ed4d2c42c6 100644 --- a/modules/jooby-log4j/pom.xml +++ b/modules/jooby-log4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-log4j jooby-log4j diff --git a/modules/jooby-logback/pom.xml b/modules/jooby-logback/pom.xml index e089dce1ea..12397ed3db 100644 --- a/modules/jooby-logback/pom.xml +++ b/modules/jooby-logback/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-logback jooby-logback diff --git a/modules/jooby-maven-plugin/pom.xml b/modules/jooby-maven-plugin/pom.xml index 23df2352bb..2cb9798d81 100644 --- a/modules/jooby-maven-plugin/pom.xml +++ b/modules/jooby-maven-plugin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-maven-plugin jooby-maven-plugin diff --git a/modules/jooby-metrics/pom.xml b/modules/jooby-metrics/pom.xml index a1badc1028..0474ccd62c 100644 --- a/modules/jooby-metrics/pom.xml +++ b/modules/jooby-metrics/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-metrics jooby-metrics diff --git a/modules/jooby-mutiny/pom.xml b/modules/jooby-mutiny/pom.xml index 8a9c65eca3..583f26c5df 100644 --- a/modules/jooby-mutiny/pom.xml +++ b/modules/jooby-mutiny/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-mutiny jooby-mutiny diff --git a/modules/jooby-netty/pom.xml b/modules/jooby-netty/pom.xml index 3e09bcd3c3..cfa0a5f334 100644 --- a/modules/jooby-netty/pom.xml +++ b/modules/jooby-netty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-netty jooby-netty diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index e597101a37..e270bdc694 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-openapi jooby-openapi diff --git a/modules/jooby-pac4j/pom.xml b/modules/jooby-pac4j/pom.xml index 7fc4fa537c..01f361e151 100644 --- a/modules/jooby-pac4j/pom.xml +++ b/modules/jooby-pac4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-pac4j jooby-pac4j diff --git a/modules/jooby-pebble/pom.xml b/modules/jooby-pebble/pom.xml index 25cec0ad71..50e8fec3fb 100644 --- a/modules/jooby-pebble/pom.xml +++ b/modules/jooby-pebble/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-pebble jooby-pebble diff --git a/modules/jooby-quartz/pom.xml b/modules/jooby-quartz/pom.xml index eee715368f..4da9a37f53 100644 --- a/modules/jooby-quartz/pom.xml +++ b/modules/jooby-quartz/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-quartz jooby-quartz diff --git a/modules/jooby-reactor/pom.xml b/modules/jooby-reactor/pom.xml index ce5b28b5ce..be79a3a238 100644 --- a/modules/jooby-reactor/pom.xml +++ b/modules/jooby-reactor/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-reactor jooby-reactor diff --git a/modules/jooby-redis/pom.xml b/modules/jooby-redis/pom.xml index e7c5dbf4dd..51d2011202 100644 --- a/modules/jooby-redis/pom.xml +++ b/modules/jooby-redis/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-redis jooby-redis diff --git a/modules/jooby-redoc/pom.xml b/modules/jooby-redoc/pom.xml index 5d113cd9f1..094ae17dc2 100644 --- a/modules/jooby-redoc/pom.xml +++ b/modules/jooby-redoc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-redoc jooby-redoc diff --git a/modules/jooby-rocker/pom.xml b/modules/jooby-rocker/pom.xml index 6f0e4b3078..0c9e1c3718 100644 --- a/modules/jooby-rocker/pom.xml +++ b/modules/jooby-rocker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-rocker jooby-rocker diff --git a/modules/jooby-run/pom.xml b/modules/jooby-run/pom.xml index db2932ae03..03420649c7 100644 --- a/modules/jooby-run/pom.xml +++ b/modules/jooby-run/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-run jooby-run diff --git a/modules/jooby-rxjava3/pom.xml b/modules/jooby-rxjava3/pom.xml index 2754e706bd..94e018d5eb 100644 --- a/modules/jooby-rxjava3/pom.xml +++ b/modules/jooby-rxjava3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-rxjava3 jooby-rxjava3 diff --git a/modules/jooby-stork/pom.xml b/modules/jooby-stork/pom.xml index 442b7e8e6a..f1b9a5b7b9 100644 --- a/modules/jooby-stork/pom.xml +++ b/modules/jooby-stork/pom.xml @@ -4,7 +4,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-stork diff --git a/modules/jooby-swagger-ui/pom.xml b/modules/jooby-swagger-ui/pom.xml index 954b655da1..49abea4b73 100644 --- a/modules/jooby-swagger-ui/pom.xml +++ b/modules/jooby-swagger-ui/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-swagger-ui jooby-swagger-ui diff --git a/modules/jooby-test/pom.xml b/modules/jooby-test/pom.xml index dadbf84030..368f743642 100644 --- a/modules/jooby-test/pom.xml +++ b/modules/jooby-test/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-test jooby-test diff --git a/modules/jooby-thymeleaf/pom.xml b/modules/jooby-thymeleaf/pom.xml index c3c43c8bfc..cb160fe460 100644 --- a/modules/jooby-thymeleaf/pom.xml +++ b/modules/jooby-thymeleaf/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-thymeleaf jooby-thymeleaf diff --git a/modules/jooby-undertow/pom.xml b/modules/jooby-undertow/pom.xml index b9f37b90ae..ffb82911e9 100644 --- a/modules/jooby-undertow/pom.xml +++ b/modules/jooby-undertow/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-undertow jooby-undertow diff --git a/modules/jooby-vertx-mysql-client/pom.xml b/modules/jooby-vertx-mysql-client/pom.xml index 46006b294a..f94ec98050 100644 --- a/modules/jooby-vertx-mysql-client/pom.xml +++ b/modules/jooby-vertx-mysql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-vertx-mysql-client jooby-vertx-mysql-client diff --git a/modules/jooby-vertx-pg-client/pom.xml b/modules/jooby-vertx-pg-client/pom.xml index 149033a795..23c4957834 100644 --- a/modules/jooby-vertx-pg-client/pom.xml +++ b/modules/jooby-vertx-pg-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-vertx-pg-client jooby-vertx-pg-client diff --git a/modules/jooby-vertx-sql-client/pom.xml b/modules/jooby-vertx-sql-client/pom.xml index d3a8709f59..2c35774643 100644 --- a/modules/jooby-vertx-sql-client/pom.xml +++ b/modules/jooby-vertx-sql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-vertx-sql-client jooby-vertx-sql-client diff --git a/modules/jooby-vertx/pom.xml b/modules/jooby-vertx/pom.xml index 5f1d263723..b98a7ed507 100644 --- a/modules/jooby-vertx/pom.xml +++ b/modules/jooby-vertx/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-vertx jooby-vertx diff --git a/modules/jooby-whoops/pom.xml b/modules/jooby-whoops/pom.xml index 458b961fa3..2d49951015 100644 --- a/modules/jooby-whoops/pom.xml +++ b/modules/jooby-whoops/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-whoops jooby-whoops diff --git a/modules/jooby-yasson/pom.xml b/modules/jooby-yasson/pom.xml index 9e003dc174..894b7ad878 100644 --- a/modules/jooby-yasson/pom.xml +++ b/modules/jooby-yasson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.12 + 4.0.13-SNAPSHOT jooby-yasson jooby-yasson diff --git a/modules/pom.xml b/modules/pom.xml index 1283af5671..d40ce5473a 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -4,7 +4,7 @@ io.jooby jooby-project - 4.0.12 + 4.0.13-SNAPSHOT modules diff --git a/pom.xml b/pom.xml index 2a16d8c8ea..20b430a7f7 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.jooby jooby-project - 4.0.12 + 4.0.13-SNAPSHOT pom jooby-project @@ -210,7 +210,7 @@ 21 21 yyyy-MM-dd HH:mm:ssa - 2025-11-26T09:57:25Z + 2025-11-26T10:28:11Z UTF-8 etc${file.separator}source${file.separator}formatter.sh diff --git a/tests/pom.xml b/tests/pom.xml index ad56f56df5..12e9a82396 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.0.12 + 4.0.13-SNAPSHOT tests tests From a84f6e1958ecddd703cbc2346b64bd898cc7b8fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 00:02:02 +0000 Subject: [PATCH 02/17] build(deps): bump swagger-ui-dist in /modules/jooby-swagger-ui Bumps [swagger-ui-dist](https://github.com/swagger-api/swagger-ui) from 5.30.2 to 5.30.3. - [Release notes](https://github.com/swagger-api/swagger-ui/releases) - [Commits](https://github.com/swagger-api/swagger-ui/compare/v5.30.2...v5.30.3) --- updated-dependencies: - dependency-name: swagger-ui-dist dependency-version: 5.30.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- modules/jooby-swagger-ui/package-lock.json | 8 ++++---- modules/jooby-swagger-ui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/jooby-swagger-ui/package-lock.json b/modules/jooby-swagger-ui/package-lock.json index 58e56e29c1..b0e88805c1 100644 --- a/modules/jooby-swagger-ui/package-lock.json +++ b/modules/jooby-swagger-ui/package-lock.json @@ -9,7 +9,7 @@ "version": "4.0.0", "license": "ASF", "dependencies": { - "swagger-ui-dist": "^5.30.2" + "swagger-ui-dist": "^5.30.3" } }, "node_modules/@scarf/scarf": { @@ -20,9 +20,9 @@ "license": "Apache-2.0" }, "node_modules/swagger-ui-dist": { - "version": "5.30.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.2.tgz", - "integrity": "sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==", + "version": "5.30.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.3.tgz", + "integrity": "sha512-giQl7/ToPxCqnUAx2wpnSnDNGZtGzw1LyUw6ZitIpTmdrvpxKFY/94v1hihm0zYNpgp1/VY0jTDk//R0BBgnRQ==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" diff --git a/modules/jooby-swagger-ui/package.json b/modules/jooby-swagger-ui/package.json index 652d484467..0e25ff59ce 100644 --- a/modules/jooby-swagger-ui/package.json +++ b/modules/jooby-swagger-ui/package.json @@ -4,7 +4,7 @@ "private": true, "license": "ASF", "dependencies": { - "swagger-ui-dist": "^5.30.2" + "swagger-ui-dist": "^5.30.3" }, "scarfSettings": { "enabled": false From fd5543ac2653c6d0e6d3de0135f27c6a2c55b707 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 00:07:59 +0000 Subject: [PATCH 03/17] build(deps): bump the dependencies group with 12 updates Bumps the dependencies group with 12 updates: | Package | From | To | | --- | --- | --- | | [com.amazonaws:aws-java-sdk-bom](https://github.com/aws/aws-sdk-java) | `1.12.793` | `1.12.794` | | io.swagger.core.v3:swagger-annotations | `2.2.40` | `2.2.41` | | io.swagger.core.v3:swagger-models | `2.2.40` | `2.2.41` | | [io.swagger.parser.v3:swagger-parser](https://github.com/swagger-api/swagger-parser) | `2.1.35` | `2.1.36` | | [org.apache.maven.plugins:maven-assembly-plugin](https://github.com/apache/maven-assembly-plugin) | `3.7.1` | `3.8.0` | | [org.codehaus.mojo:versions-maven-plugin](https://github.com/mojohaus/versions) | `2.20.0` | `2.20.1` | | [org.apache.maven.plugins:maven-resources-plugin](https://github.com/apache/maven-resources-plugin) | `3.3.1` | `3.4.0` | | [org.apache.maven.plugins:maven-source-plugin](https://github.com/apache/maven-source-plugin) | `3.3.1` | `3.4.0` | | [com.puppycrawl.tools:checkstyle](https://github.com/checkstyle/checkstyle) | `12.1.2` | `12.2.0` | | [net.bytebuddy:byte-buddy](https://github.com/raphw/byte-buddy) | `1.18.1` | `1.18.2` | | software.amazon.awssdk:bom | `2.39.2` | `2.39.5` | | [io.smallrye.reactive:mutiny](https://github.com/smallrye/smallrye-mutiny) | `3.0.1` | `3.0.3` | Updates `com.amazonaws:aws-java-sdk-bom` from 1.12.793 to 1.12.794 - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.793...1.12.794) Updates `io.swagger.core.v3:swagger-annotations` from 2.2.40 to 2.2.41 Updates `io.swagger.core.v3:swagger-models` from 2.2.40 to 2.2.41 Updates `io.swagger.core.v3:swagger-models` from 2.2.40 to 2.2.41 Updates `io.swagger.parser.v3:swagger-parser` from 2.1.35 to 2.1.36 - [Release notes](https://github.com/swagger-api/swagger-parser/releases) - [Commits](https://github.com/swagger-api/swagger-parser/compare/v2.1.35...v2.1.36) Updates `org.apache.maven.plugins:maven-assembly-plugin` from 3.7.1 to 3.8.0 - [Release notes](https://github.com/apache/maven-assembly-plugin/releases) - [Commits](https://github.com/apache/maven-assembly-plugin/compare/maven-assembly-plugin-3.7.1...v3.8.0) Updates `org.codehaus.mojo:versions-maven-plugin` from 2.20.0 to 2.20.1 - [Release notes](https://github.com/mojohaus/versions/releases) - [Changelog](https://github.com/mojohaus/versions/blob/master/ReleaseNotes.md) - [Commits](https://github.com/mojohaus/versions/compare/2.20.0...2.20.1) Updates `org.apache.maven.plugins:maven-resources-plugin` from 3.3.1 to 3.4.0 - [Release notes](https://github.com/apache/maven-resources-plugin/releases) - [Commits](https://github.com/apache/maven-resources-plugin/compare/maven-resources-plugin-3.3.1...v3.4.0) Updates `org.apache.maven.plugins:maven-source-plugin` from 3.3.1 to 3.4.0 - [Release notes](https://github.com/apache/maven-source-plugin/releases) - [Commits](https://github.com/apache/maven-source-plugin/compare/maven-source-plugin-3.3.1...maven-source-plugin-3.4.0) Updates `com.puppycrawl.tools:checkstyle` from 12.1.2 to 12.2.0 - [Release notes](https://github.com/checkstyle/checkstyle/releases) - [Commits](https://github.com/checkstyle/checkstyle/compare/checkstyle-12.1.2...checkstyle-12.2.0) Updates `net.bytebuddy:byte-buddy` from 1.18.1 to 1.18.2 - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.1...byte-buddy-1.18.2) Updates `software.amazon.awssdk:bom` from 2.39.2 to 2.39.5 Updates `io.smallrye.reactive:mutiny` from 3.0.1 to 3.0.3 - [Release notes](https://github.com/smallrye/smallrye-mutiny/releases) - [Commits](https://github.com/smallrye/smallrye-mutiny/compare/3.0.1...3.0.3) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-bom dependency-version: 1.12.794 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-annotations dependency-version: 2.2.41 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-models dependency-version: 2.2.41 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-models dependency-version: 2.2.41 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.parser.v3:swagger-parser dependency-version: 2.1.36 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.apache.maven.plugins:maven-assembly-plugin dependency-version: 3.8.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.codehaus.mojo:versions-maven-plugin dependency-version: 2.20.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.apache.maven.plugins:maven-resources-plugin dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.apache.maven.plugins:maven-source-plugin dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: com.puppycrawl.tools:checkstyle dependency-version: 12.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: net.bytebuddy:byte-buddy dependency-version: 1.18.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: software.amazon.awssdk:bom dependency-version: 2.39.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.smallrye.reactive:mutiny dependency-version: 3.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-mutiny/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 4 ++-- pom.xml | 16 ++++++++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 1554c1ca45..d2496f57a4 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -12,7 +12,7 @@ jooby-awssdk-v2 - 2.39.2 + 2.39.5 diff --git a/modules/jooby-mutiny/pom.xml b/modules/jooby-mutiny/pom.xml index 583f26c5df..87070dbbfd 100644 --- a/modules/jooby-mutiny/pom.xml +++ b/modules/jooby-mutiny/pom.xml @@ -26,7 +26,7 @@ io.smallrye.reactive mutiny - 3.0.1 + 3.0.3 diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index e270bdc694..25a5057632 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -48,7 +48,7 @@ com.puppycrawl.tools checkstyle - 12.1.2 + 12.2.0 @@ -125,7 +125,7 @@ net.bytebuddy byte-buddy - 1.18.1 + 1.18.2 test diff --git a/pom.xml b/pom.xml index 20b430a7f7..d1bb75d60d 100644 --- a/pom.xml +++ b/pom.xml @@ -111,8 +111,8 @@ 5.0.5 - 2.2.40 - 2.1.35 + 2.2.41 + 2.1.36 2.0.0-rc.20 @@ -136,7 +136,7 @@ 2.5.1 9.2.1 8.15.0 - 1.12.793 + 1.12.794 4.15.0 1.9.3 @@ -170,7 +170,7 @@ ${jacoco.version} 3.2.0 3.2.0 - 3.7.1 + 3.8.0 2.41 3.14.1 3.9.11 @@ -183,15 +183,15 @@ 3.9.11 3.15.2 2.2.1 - 3.3.1 + 3.4.0 3.6.1 3.8.2 - 3.3.1 + 3.4.0 3.5.4 2.3.1 4.0.2 3.2.0 - 2.20.0 + 2.20.1 3.6.2 3.1.2 1.15.4 @@ -1654,7 +1654,7 @@ org.codehaus.mojo versions-maven-plugin - 2.20.0 + 2.20.1 From 9ce066caae9968e29582d04eb43433531bed6e11 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 1 Dec 2025 04:39:31 -0500 Subject: [PATCH 04/17] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 75f7963694..e44c538c65 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,6 @@ sponsors | Logo | Sponsor | |----------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------| -| Mercedes Benz | [@mercedes-benz](https://github.com/mercedes-benz) | | Premium Minds | [@premium-minds](https://github.com/premium-minds) | | Adam Gent | [@agentgt](https://github.com/agentgt) | | David | [@tipsy](https://github.com/tipsy) | From 577b4081adeb416f2e2eb4ca1f6b4a9bdb24c4ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:56:47 +0000 Subject: [PATCH 05/17] build(deps): bump the dependencies group across 1 directory with 31 updates Bumps the dependencies group with 31 updates in the / directory: | Package | From | To | | --- | --- | --- | | [io.netty:netty-bom](https://github.com/netty/netty) | `4.2.7.Final` | `4.2.8.Final` | | [io.netty:netty-codec-http2](https://github.com/netty/netty) | `4.2.7.Final` | `4.2.8.Final` | | [io.netty:netty-transport-native-epoll](https://github.com/netty/netty) | `4.2.7.Final` | `4.2.8.Final` | | [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) | `4.2.7.Final` | `4.2.8.Final` | | [io.netty:netty-transport-native-io_uring](https://github.com/netty/netty) | `4.2.7.Final` | `4.2.8.Final` | | [com.amazonaws:aws-java-sdk-bom](https://github.com/aws/aws-sdk-java) | `1.12.794` | `1.12.795` | | [io.avaje:avaje-inject](https://github.com/avaje/avaje-inject) | `12.0` | `12.1` | | io.avaje:avaje-inject-generator | `12.0` | `12.1` | | [io.avaje:avaje-jsonb](https://github.com/avaje/avaje-jsonb) | `3.8` | `3.9` | | io.avaje:avaje-jsonb-generator | `3.8` | `3.9` | | [io.avaje:avaje-validator](https://github.com/avaje/avaje-validator) | `2.14` | `2.15` | | io.avaje:avaje-validator-generator | `2.14` | `2.15` | | [io.pebbletemplates:pebble](https://github.com/PebbleTemplates/pebble) | `4.0.0` | `4.1.0` | | [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) | `1.5.21` | `1.5.22` | | [org.quartz-scheduler:quartz](https://github.com/quartz-scheduler/quartz) | `2.5.1` | `2.5.2` | | [org.jdbi:jdbi3-core](https://github.com/jdbi/jdbi) | `3.50.0` | `3.51.0` | | org.eclipse.jetty:jetty-server | `12.1.4` | `12.1.5` | | org.eclipse.jetty.websocket:jetty-websocket-core-server | `12.1.4` | `12.1.5` | | org.eclipse.jetty.websocket:jetty-websocket-jetty-api | `12.1.4` | `12.1.5` | | org.eclipse.jetty.websocket:jetty-websocket-jetty-server | `12.1.4` | `12.1.5` | | org.eclipse.jetty.http2:jetty-http2-server | `12.1.4` | `12.1.5` | | org.eclipse.jetty:jetty-alpn-java-server | `12.1.4` | `12.1.5` | | org.eclipse.jetty.http2:jetty-http2-client | `12.1.4` | `12.1.5` | | [io.rest-assured:rest-assured](https://github.com/rest-assured/rest-assured) | `5.5.6` | `6.0.0` | | [org.mockito:mockito-core](https://github.com/mockito/mockito) | `5.20.0` | `5.21.0` | | [org.mockito:mockito-junit-jupiter](https://github.com/mockito/mockito) | `5.20.0` | `5.21.0` | | [io.lettuce:lettuce-core](https://github.com/redis/lettuce) | `7.1.0.RELEASE` | `7.2.0.RELEASE` | | org.apache.commons:commons-pool2 | `2.12.1` | `2.13.0` | | software.amazon.awssdk:bom | `2.39.5` | `2.40.8` | | [io.projectreactor:reactor-core](https://github.com/reactor/reactor-core) | `3.8.0` | `3.8.1` | | [io.smallrye.reactive:mutiny](https://github.com/smallrye/smallrye-mutiny) | `3.0.3` | `3.1.0` | Updates `io.netty:netty-bom` from 4.2.7.Final to 4.2.8.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.7.Final...netty-4.2.8.Final) Updates `io.netty:netty-codec-http2` from 4.2.7.Final to 4.2.8.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.7.Final...netty-4.2.8.Final) Updates `io.netty:netty-transport-native-epoll` from 4.2.7.Final to 4.2.8.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.7.Final...netty-4.2.8.Final) Updates `io.netty:netty-transport-native-kqueue` from 4.2.7.Final to 4.2.8.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.7.Final...netty-4.2.8.Final) Updates `io.netty:netty-transport-native-io_uring` from 4.2.7.Final to 4.2.8.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.7.Final...netty-4.2.8.Final) Updates `com.amazonaws:aws-java-sdk-bom` from 1.12.794 to 1.12.795 - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.794...1.12.795) Updates `io.avaje:avaje-inject` from 12.0 to 12.1 - [Release notes](https://github.com/avaje/avaje-inject/releases) - [Commits](https://github.com/avaje/avaje-inject/compare/12.0...12.1) Updates `io.avaje:avaje-inject-generator` from 12.0 to 12.1 Updates `io.avaje:avaje-inject-generator` from 12.0 to 12.1 Updates `io.avaje:avaje-jsonb` from 3.8 to 3.9 - [Release notes](https://github.com/avaje/avaje-jsonb/releases) - [Commits](https://github.com/avaje/avaje-jsonb/compare/3.8...3.9) Updates `io.avaje:avaje-jsonb-generator` from 3.8 to 3.9 Updates `io.avaje:avaje-jsonb-generator` from 3.8 to 3.9 Updates `io.avaje:avaje-validator` from 2.14 to 2.15 - [Release notes](https://github.com/avaje/avaje-validator/releases) - [Commits](https://github.com/avaje/avaje-validator/compare/2.14...2.15) Updates `io.avaje:avaje-validator-generator` from 2.14 to 2.15 Updates `io.avaje:avaje-validator-generator` from 2.14 to 2.15 Updates `io.pebbletemplates:pebble` from 4.0.0 to 4.1.0 - [Release notes](https://github.com/PebbleTemplates/pebble/releases) - [Changelog](https://github.com/PebbleTemplates/pebble/blob/master/release.properties) - [Commits](https://github.com/PebbleTemplates/pebble/compare/4.0.0...4.1.0) Updates `ch.qos.logback:logback-classic` from 1.5.21 to 1.5.22 - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.21...v_1.5.22) Updates `org.quartz-scheduler:quartz` from 2.5.1 to 2.5.2 - [Release notes](https://github.com/quartz-scheduler/quartz/releases) - [Commits](https://github.com/quartz-scheduler/quartz/compare/v2.5.1...v2.5.2) Updates `org.jdbi:jdbi3-core` from 3.50.0 to 3.51.0 - [Release notes](https://github.com/jdbi/jdbi/releases) - [Changelog](https://github.com/jdbi/jdbi/blob/master/RELEASE_NOTES.md) - [Commits](https://github.com/jdbi/jdbi/compare/v3.50.0...v3.51.0) Updates `org.eclipse.jetty:jetty-server` from 12.1.4 to 12.1.5 Updates `org.eclipse.jetty.websocket:jetty-websocket-core-server` from 12.1.4 to 12.1.5 Updates `org.eclipse.jetty.websocket:jetty-websocket-jetty-api` from 12.1.4 to 12.1.5 Updates `org.eclipse.jetty.websocket:jetty-websocket-jetty-server` from 12.1.4 to 12.1.5 Updates `org.eclipse.jetty.http2:jetty-http2-server` from 12.1.4 to 12.1.5 Updates `org.eclipse.jetty:jetty-alpn-java-server` from 12.1.4 to 12.1.5 Updates `org.eclipse.jetty.http2:jetty-http2-client` from 12.1.4 to 12.1.5 Updates `org.eclipse.jetty.websocket:jetty-websocket-core-server` from 12.1.4 to 12.1.5 Updates `io.rest-assured:rest-assured` from 5.5.6 to 6.0.0 - [Changelog](https://github.com/rest-assured/rest-assured/blob/master/changelog.txt) - [Commits](https://github.com/rest-assured/rest-assured/compare/rest-assured-5.5.6...rest-assured-6.0.0) Updates `org.mockito:mockito-core` from 5.20.0 to 5.21.0 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.20.0...v5.21.0) Updates `org.mockito:mockito-junit-jupiter` from 5.20.0 to 5.21.0 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.20.0...v5.21.0) Updates `org.mockito:mockito-junit-jupiter` from 5.20.0 to 5.21.0 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.20.0...v5.21.0) Updates `io.lettuce:lettuce-core` from 7.1.0.RELEASE to 7.2.0.RELEASE - [Release notes](https://github.com/redis/lettuce/releases) - [Changelog](https://github.com/redis/lettuce/blob/main/RELEASE-NOTES.md) - [Commits](https://github.com/redis/lettuce/compare/7.1.0.RELEASE...7.2.0.RELEASE) Updates `org.apache.commons:commons-pool2` from 2.12.1 to 2.13.0 Updates `org.eclipse.jetty.websocket:jetty-websocket-jetty-api` from 12.1.4 to 12.1.5 Updates `org.eclipse.jetty.websocket:jetty-websocket-jetty-server` from 12.1.4 to 12.1.5 Updates `org.eclipse.jetty.http2:jetty-http2-server` from 12.1.4 to 12.1.5 Updates `org.eclipse.jetty:jetty-alpn-java-server` from 12.1.4 to 12.1.5 Updates `io.netty:netty-codec-http2` from 4.2.7.Final to 4.2.8.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.7.Final...netty-4.2.8.Final) Updates `io.netty:netty-transport-native-epoll` from 4.2.7.Final to 4.2.8.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.7.Final...netty-4.2.8.Final) Updates `io.netty:netty-transport-native-kqueue` from 4.2.7.Final to 4.2.8.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.7.Final...netty-4.2.8.Final) Updates `io.netty:netty-transport-native-io_uring` from 4.2.7.Final to 4.2.8.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.7.Final...netty-4.2.8.Final) Updates `software.amazon.awssdk:bom` from 2.39.5 to 2.40.8 Updates `io.projectreactor:reactor-core` from 3.8.0 to 3.8.1 - [Release notes](https://github.com/reactor/reactor-core/releases) - [Commits](https://github.com/reactor/reactor-core/compare/v3.8.0...v3.8.1) Updates `io.smallrye.reactive:mutiny` from 3.0.3 to 3.1.0 - [Release notes](https://github.com/smallrye/smallrye-mutiny/releases) - [Commits](https://github.com/smallrye/smallrye-mutiny/compare/3.0.3...3.1.0) Updates `org.eclipse.jetty.http2:jetty-http2-client` from 12.1.4 to 12.1.5 --- updated-dependencies: - dependency-name: io.netty:netty-bom dependency-version: 4.2.8.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-codec-http2 dependency-version: 4.2.8.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-epoll dependency-version: 4.2.8.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-kqueue dependency-version: 4.2.8.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-io_uring dependency-version: 4.2.8.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: com.amazonaws:aws-java-sdk-bom dependency-version: 1.12.795 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.avaje:avaje-inject dependency-version: '12.1' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-inject-generator dependency-version: '12.1' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-inject-generator dependency-version: '12.1' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb dependency-version: '3.9' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb-generator dependency-version: '3.9' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb-generator dependency-version: '3.9' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-validator dependency-version: '2.15' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-validator-generator dependency-version: '2.15' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-validator-generator dependency-version: '2.15' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.pebbletemplates:pebble dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: ch.qos.logback:logback-classic dependency-version: 1.5.22 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.quartz-scheduler:quartz dependency-version: 2.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.jdbi:jdbi3-core dependency-version: 3.51.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.eclipse.jetty:jetty-server dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-core-server dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-jetty-api dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-jetty-server dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.http2:jetty-http2-server dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty:jetty-alpn-java-server dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.http2:jetty-http2-client dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-core-server dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.rest-assured:rest-assured dependency-version: 6.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: dependencies - dependency-name: org.mockito:mockito-core dependency-version: 5.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.mockito:mockito-junit-jupiter dependency-version: 5.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.mockito:mockito-junit-jupiter dependency-version: 5.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.lettuce:lettuce-core dependency-version: 7.2.0.RELEASE dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.apache.commons:commons-pool2 dependency-version: 2.13.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-jetty-api dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-jetty-server dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.http2:jetty-http2-server dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty:jetty-alpn-java-server dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-codec-http2 dependency-version: 4.2.8.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-epoll dependency-version: 4.2.8.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-kqueue dependency-version: 4.2.8.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-io_uring dependency-version: 4.2.8.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: software.amazon.awssdk:bom dependency-version: 2.40.8 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.projectreactor:reactor-core dependency-version: 3.8.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.smallrye.reactive:mutiny dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.eclipse.jetty.http2:jetty-http2-client dependency-version: 12.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-mutiny/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 2 +- modules/jooby-reactor/pom.xml | 2 +- pom.xml | 28 ++++++++++++++-------------- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index d2496f57a4..0c1c6fc86d 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -12,7 +12,7 @@ jooby-awssdk-v2 - 2.39.5 + 2.40.8 diff --git a/modules/jooby-mutiny/pom.xml b/modules/jooby-mutiny/pom.xml index 87070dbbfd..75ecf14a15 100644 --- a/modules/jooby-mutiny/pom.xml +++ b/modules/jooby-mutiny/pom.xml @@ -26,7 +26,7 @@ io.smallrye.reactive mutiny - 3.0.3 + 3.1.0 diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 25a5057632..15ee4c1a6b 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -79,7 +79,7 @@ io.avaje avaje-inject - 12.0 + 12.1 test diff --git a/modules/jooby-reactor/pom.xml b/modules/jooby-reactor/pom.xml index be79a3a238..ee434f510f 100644 --- a/modules/jooby-reactor/pom.xml +++ b/modules/jooby-reactor/pom.xml @@ -26,7 +26,7 @@ io.projectreactor reactor-core - 3.8.0 + 3.8.1 diff --git a/pom.xml b/pom.xml index d1bb75d60d..a49c3a4363 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.3.34 4.5.0 1.3.7 - 4.0.0 + 4.1.0 2.20.1 2.13.2 3.0.1 @@ -74,11 +74,11 @@ 1.2 7.0.4.Final 17.2.0 - 3.50.0 + 3.51.0 11.15.0 25.0 - 7.1.0.RELEASE - 2.12.1 + 7.2.0.RELEASE + 2.13.0 4.1.1 3.2.3 @@ -88,7 +88,7 @@ 7.0.0 - 1.5.21 + 1.5.22 2.25.2 2.0.17 @@ -106,8 +106,8 @@ 2.3.20.Final - 12.1.4 - 4.2.7.Final + 12.1.5 + 4.2.8.Final 5.0.5 @@ -119,9 +119,9 @@ 2.5.2 - 12.0 - 3.8 - 2.14 + 12.1 + 3.9 + 2.15 2.0.1.MR @@ -133,10 +133,10 @@ 5.3.2 0.13.0 6.2.2 - 2.5.1 + 2.5.2 9.2.1 8.15.0 - 1.12.794 + 1.12.795 4.15.0 1.9.3 @@ -150,9 +150,9 @@ 0.8.14 6.0.1 - 5.5.6 + 6.0.0 3.27.6 - 5.20.0 + 5.21.0 ${user.home}${file.separator}.m2${file.separator}repository org${file.separator}mockito${file.separator}mockito-core${file.separator}${mockito.version}${file.separator}mockito-core-${mockito.version}.jar -javaagent:${maven.m2.repo}${file.separator}${mockito.agent} From c219dc5ff73c48dc7c9c22c315e41f822ab6c647 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 13 Dec 2025 09:14:29 -0300 Subject: [PATCH 06/17] build: fix pebble upgrade - FileLoader requires a path prefix --- .../src/main/java/io/jooby/pebble/PebbleModule.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleModule.java b/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleModule.java index c497c70ed0..33eadae574 100644 --- a/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleModule.java +++ b/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleModule.java @@ -235,10 +235,9 @@ DelegatingLoader getDefaultLoader(String templatesPath, String extension) { cLoader.setSuffix(extension); loaders.add(cLoader); - FileLoader fLoader = new FileLoader(); Path dir = Paths.get(System.getProperty("user.dir"), templatesPath); if (Files.exists(dir)) { - fLoader.setPrefix(dir.normalize().toString()); + FileLoader fLoader = new FileLoader(dir.normalize().toString()); fLoader.setSuffix(extension); loaders.add(fLoader); } From d695226218429be89ba591ef93f4883c351d1672 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 00:01:21 +0000 Subject: [PATCH 07/17] build(deps): bump swagger-ui-dist in /modules/jooby-swagger-ui Bumps [swagger-ui-dist](https://github.com/swagger-api/swagger-ui) from 5.30.3 to 5.31.0. - [Release notes](https://github.com/swagger-api/swagger-ui/releases) - [Commits](https://github.com/swagger-api/swagger-ui/compare/v5.30.3...v5.31.0) --- updated-dependencies: - dependency-name: swagger-ui-dist dependency-version: 5.31.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- modules/jooby-swagger-ui/package-lock.json | 8 ++++---- modules/jooby-swagger-ui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/jooby-swagger-ui/package-lock.json b/modules/jooby-swagger-ui/package-lock.json index b0e88805c1..4416d58c38 100644 --- a/modules/jooby-swagger-ui/package-lock.json +++ b/modules/jooby-swagger-ui/package-lock.json @@ -9,7 +9,7 @@ "version": "4.0.0", "license": "ASF", "dependencies": { - "swagger-ui-dist": "^5.30.3" + "swagger-ui-dist": "^5.31.0" } }, "node_modules/@scarf/scarf": { @@ -20,9 +20,9 @@ "license": "Apache-2.0" }, "node_modules/swagger-ui-dist": { - "version": "5.30.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.3.tgz", - "integrity": "sha512-giQl7/ToPxCqnUAx2wpnSnDNGZtGzw1LyUw6ZitIpTmdrvpxKFY/94v1hihm0zYNpgp1/VY0jTDk//R0BBgnRQ==", + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" diff --git a/modules/jooby-swagger-ui/package.json b/modules/jooby-swagger-ui/package.json index 0e25ff59ce..0ddbf68e46 100644 --- a/modules/jooby-swagger-ui/package.json +++ b/modules/jooby-swagger-ui/package.json @@ -4,7 +4,7 @@ "private": true, "license": "ASF", "dependencies": { - "swagger-ui-dist": "^5.30.3" + "swagger-ui-dist": "^5.31.0" }, "scarfSettings": { "enabled": false From 0e79d846afa7257329e02406e66eb6b413dba4f5 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 15 Dec 2025 17:03:36 -0500 Subject: [PATCH 08/17] openapi: asciidoc output #3820 (#3821) * v4.0.11 * prepare for next development cycle * WIP: openapi: asciidoc output #3820 - setup pebble as template engine/ascii doc pre-processor - start of `snippets` which are going to output/print routes in multiple formats * prepare for next development cycle * filter: curl - more advanced implementation - unit tests - ref #3820 * openapi: asciidoc output - curl support form parameter - curl support query parameter - added schema, json, yaml filters - ref #3820 * openapi: asciidoc output #3820 - add more snippets - code clean up * openapi: asciidoc output #3820 - attempt to control new lines * windows: attempt to find blank difference * - integrate ascii doc into openapi generator: maven/gradle plugin - fix schema data iteration to support schema $ref - make schema filter pretty print output * - better implementation for summary/description doc - Add GET adoc function shortcut for operation * - add POST, PUT,.., adoc operations - ref #3820 * - add `path` filter with replace query and path paramters - better format for table output (long description column) - ref #3820 * - enum doc: add field doc with custom implementation - add schema function to located schemas and/or schema properties - add `display` filter * pebble/ascii doc support: redo pebble function and filter all them follow the given patter: `{{find/locate | mutator/filter | display}}` it creates a clear and common pattern for final users * - implementation complete - redo entire pebble/template support - it is lot better now <3 * - cleanup, remove all implementation - cleanup classpath - ref #3820 * - remove dead template files - ref #3820 * - fix build #3820 * - build: fix new line on windows * set default error response: - allow to define custom error - add a function error - allow to resolve simple variables from error template - ref #3820 * statusCode: add way to display status code - it can be override, supported format: json, yaml, table, list * routes: add route summary - routes/operations - add support for fn alias - fix error lookup to support expression like `error(400)` * - code cleanup - dynamic doc generation * - implement dyamic display based on tags - ref #3820 * link: add link to schema filter - bug fixing - make jakarata Page fully embedded * open api: fix `type` vs `types` difference over `v30` vs `v31` - remove useless code around it * - fix sample data/schema properties for array of basic types/as well as basic types it self - fix links for array, arraylike models * - make schemas easier to navigate/iterate and display * - add extensions to http request * - document ascii doc support - fix curl bug --- docs/asciidoc/modules/openapi-ascii.adoc | 445 +++++ docs/asciidoc/modules/openapi.adoc | 157 +- .../main/java/io/jooby/adoc/DocGenerator.java | 48 +- jooby/src/main/java/io/jooby/StatusCode.java | 192 +- .../java/io/jooby/gradle/OpenAPITask.java | 35 +- .../main/java/io/jooby/maven/OpenAPIMojo.java | 25 +- .../src/main/java/io/jooby/maven/RunMojo.java | 17 +- modules/jooby-openapi/pom.xml | 37 + .../internal/openapi/AnnotationParser.java | 40 +- .../internal/openapi/ArrayLikeSchema.java | 23 + .../io/jooby/internal/openapi/EnumSchema.java | 35 + .../io/jooby/internal/openapi/MixinHook.java | 75 + .../internal/openapi/ModelConverterExt.java | 5 +- .../io/jooby/internal/openapi/OpenAPIExt.java | 15 + .../jooby/internal/openapi/OpenAPIParser.java | 2 +- .../jooby/internal/openapi/OperationExt.java | 20 +- .../jooby/internal/openapi/ParameterExt.java | 24 +- .../jooby/internal/openapi/ParserContext.java | 155 +- .../jooby/internal/openapi/RouteParser.java | 14 +- .../openapi/asciidoc/AsciiDocContext.java | 521 ++++++ .../openapi/asciidoc/AutoDataFakerMapper.java | 340 ++++ .../internal/openapi/asciidoc/Display.java | 230 +++ .../openapi/asciidoc/HttpMessage.java | 50 + .../openapi/asciidoc/HttpRequest.java | 283 +++ .../openapi/asciidoc/HttpRequestList.java | 79 + .../openapi/asciidoc/HttpResponse.java | 118 ++ .../internal/openapi/asciidoc/Lookup.java | 267 +++ .../internal/openapi/asciidoc/Mutator.java | 188 ++ .../openapi/asciidoc/ParameterList.java | 51 + .../openapi/asciidoc/StatusCodeList.java | 49 + .../internal/openapi/asciidoc/TagExt.java | 31 + .../internal/openapi/asciidoc/ToAsciiDoc.java | 14 + .../internal/openapi/asciidoc/ToSnippet.java | 12 + .../asciidoc/display/MapToAsciiDoc.java | 55 + .../asciidoc/display/OpenApiToAsciiDoc.java | 211 +++ .../asciidoc/display/RequestToCurl.java | 180 ++ .../asciidoc/display/RequestToHttp.java | 50 + .../asciidoc/display/ResponseToHttp.java | 39 + .../internal/openapi/javadoc/ClassDoc.java | 10 + .../openapi/javadoc/ContentSplitter.java | 158 ++ .../internal/openapi/javadoc/FieldDoc.java | 6 + .../internal/openapi/javadoc/JavaDocNode.java | 41 +- .../io/jooby/openapi/OpenAPIGenerator.java | 145 +- .../src/main/java/module-info.java | 12 + .../asciidoc/AutoDataFakerMapperTest.java | 157 ++ .../asciidoc/PebbleTemplateSupport.java | 47 + .../openapi/javadoc/ContentSplitterTest.java | 169 ++ .../java/io/jooby/openapi/CurrentDir.java | 43 + .../io/jooby/openapi/OpenAPIExtension.java | 9 +- .../java/io/jooby/openapi/OpenAPIResult.java | 34 + .../io/jooby/openapi/OperationBuilder.java | 166 ++ .../src/test/java/issues/Issue1582.java | 31 +- .../java/issues/i3729/api/ApiDocTest.java | 829 ++++++--- .../java/issues/i3729/api/AppDemoLibrary.java | 17 + .../src/test/java/issues/i3729/api/Book.java | 13 + .../test/java/issues/i3729/api/BookError.java | 41 + .../test/java/issues/i3729/api/BookQuery.java | 4 +- .../test/java/issues/i3729/api/BookType.java | 49 + .../java/issues/i3729/api/LibraryApi.java | 7 +- .../java/issues/i3729/api/LibraryDemoApi.java | 100 ++ .../java/issues/i3729/api/LibraryRepo.java | 87 + .../java/issues/i3729/api/ScriptLibrary.java | 6 +- .../src/test/java/issues/i3820/App3820a.java | 19 + .../src/test/java/issues/i3820/App3820b.java | 29 + .../src/test/java/issues/i3820/Issue3820.java | 45 + .../java/issues/i3820/PebbleSupportTest.java | 1559 +++++++++++++++++ .../test/java/issues/i3820/app/AppLib.java | 42 + .../test/java/issues/i3820/app/LibApi.java | 124 ++ .../test/java/issues/i3820/app/Library.java | 87 + .../test/java/issues/i3820/model/Address.java | 35 + .../test/java/issues/i3820/model/Author.java | 35 + .../test/java/issues/i3820/model/Book.java | 118 ++ .../java/issues/i3820/model/BookType.java | 49 + .../java/issues/i3820/model/Publisher.java | 46 + .../src/test/resources/adoc/library.adoc | 46 + .../src/test/resources/adoc/library.yml | 262 +++ .../test/resources/issues/i3820/schema.adoc | 1 + pom.xml | 2 +- 78 files changed, 8197 insertions(+), 615 deletions(-) create mode 100644 docs/asciidoc/modules/openapi-ascii.adoc create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ArrayLikeSchema.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/EnumSchema.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/MixinHook.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AutoDataFakerMapper.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Display.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpMessage.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpRequest.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpRequestList.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpResponse.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Lookup.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Mutator.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ParameterList.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/StatusCodeList.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/TagExt.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ToAsciiDoc.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ToSnippet.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/MapToAsciiDoc.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/OpenApiToAsciiDoc.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/RequestToCurl.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/RequestToHttp.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/ResponseToHttp.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ContentSplitter.java create mode 100644 modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/asciidoc/AutoDataFakerMapperTest.java create mode 100644 modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/asciidoc/PebbleTemplateSupport.java create mode 100644 modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/javadoc/ContentSplitterTest.java create mode 100644 modules/jooby-openapi/src/test/java/io/jooby/openapi/CurrentDir.java create mode 100644 modules/jooby-openapi/src/test/java/io/jooby/openapi/OperationBuilder.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3729/api/AppDemoLibrary.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3729/api/BookError.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3729/api/BookType.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryDemoApi.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryRepo.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/App3820a.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/App3820b.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/Issue3820.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/PebbleSupportTest.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/app/AppLib.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/app/LibApi.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/app/Library.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/model/Address.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/model/Author.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/model/Book.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/model/BookType.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3820/model/Publisher.java create mode 100644 modules/jooby-openapi/src/test/resources/adoc/library.adoc create mode 100644 modules/jooby-openapi/src/test/resources/adoc/library.yml create mode 100644 modules/jooby-openapi/src/test/resources/issues/i3820/schema.adoc diff --git a/docs/asciidoc/modules/openapi-ascii.adoc b/docs/asciidoc/modules/openapi-ascii.adoc new file mode 100644 index 0000000000..2034d6fafc --- /dev/null +++ b/docs/asciidoc/modules/openapi-ascii.adoc @@ -0,0 +1,445 @@ +==== Ascii Doc + +===== Setup + +1) create your template: `doc/library.adoc` + +[source, asciidoc] +---- += 📚 {{info.title}} Guide +:source-highlighter: highlightjs + +{{ info.description }} + +== Base URL + +All requests start with: `{{ server(0).url }}/library` + +=== Summary + +{{ routes | table(grid="rows") }} +---- + +2) add to build process + +.pom.xml +[source, xml, role = "primary", subs="verbatim,attributes"] +---- +... + + ... + + io.jooby + jooby-maven-plugin + {joobyVersion} + + + + openapi + + + + doc/library.adoc + + + + + + +---- + +.build.gradle +[source, groovy, role = "secondary", subs="verbatim,attributes"] +---- +openAPI { + adoc = ["doc/library.adoc"] +} +---- + +3) The output directory will have two files: + - library.adoc (final asciidoctor file) + - library.html (asciidoctor output) + +===== 1. Overview +The **Jooby OpenAPI Template Engine** is a tool designed to generate comprehensive **AsciiDoc (`.adoc`)** documentation directly from your Jooby application's OpenAPI model. + +It uses https://pebbletemplates.io[pebble] as a pre-processor to automate redundant tasks. Instead of manually writing repetitive documentation for every endpoint, you write a single template that pulls live data from your code (routes, schemas, javadoc). + +====== Pebble Syntax Primer +You mix standard AsciiDoc text with Pebble logic. + +* **`{{ expression }}`**: **Output.** Use this to print values to the file. ++ +_Example:_ `{{ info.title }}` prints the API title. + +* **`{% tag %}`**: **Logic.** Use this for control flow (loops, variables, if/else) without printing output. ++ +_Example:_ `{% for route in routes %} ... {% endfor %}` loops through your API routes. + +--- + +===== 2. The Pipeline Concept + +Data generation follows a flexible pipeline architecture. You start with a source and can optionally transform it before rendering. + +[source, subs="verbatim,quotes"] +---- +{{ *Source* | [*Mutator*] | *Display* }} +---- + +. **Source**: Finds an object in the OpenAPI model (e.g., a route or schema). +. **Mutator** _(Optional)_: Transforms or filters the data (e.g., extracting just the body, or filtering parameters). +. **Display**: Renders the final output (e.g., as JSON, a Table, or a cURL command). + +====== Examples +* **Simple:** Source -> Display ++ +`{{ info.description }}` + +* **Chained:** Source -> Mutator -> Display ++ +`{{ GET("/users") | request | body | example | json }}` + +--- + +===== 3. Data Sources (Lookups) +These functions are your entry points to locate objects within the OpenAPI definition. + +[cols="2m,3,3"] +|=== +|Function |Description |Example + +|operation(method, path) +|Generic lookup for an API operation. +|`{{ operation("GET", "/books") }}` + +|GET(path) +|Shorthand for `operation("GET", path)`. +|`{{ GET("/books") }}` + +|POST(path) +|Shorthand for `operation("POST", path)`. +|`{{ POST("/books") }}` + +|PUT / PATCH / DELETE +|Shorthand for respective HTTP methods. +|`{{ DELETE("/books/{id}") }}` + +|schema(name) +|Looks up a Schema/Model definition by name. +|`{{ schema("User") }}` + +|tag(name) +|Selects a specific Tag group (containing name, description, and routes). +|`{{ tag("Inventory") }}` + +|routes() +|Returns a collection of all available routes in the API. +|`{% for r in routes() %}...{% endfor %}` + +|server(index) +|Selects a server definition from the OpenAPI spec by index. +|`{{ server(0).url }}` + +|error(code) +|Generates an error response object. + +**Default:** `{statusCode, reason, message}`. + +**Custom:** Looks for a global `error` variable map and interpolates values. +|`{{ error(404) }}` + +|statusCode(input) +|Generates status code descriptions. Accepts: + +1. **Int:** Default reason. + +2. **List:** `[200, 404]` + +3. **Map:** `{200: "OK", 400: "Bad Syntax"}` (Overrides defaults). +|`{{ statusCode(200) }}` + +`{{ statusCode([200, 400]) }}` + +`{{ statusCode( {200: "OK", 400: "Bad Syntax"} ) }}` + +|=== + +--- + +===== 4. Mutators (Transformers) +Mutators modify the data stream. They are optional but powerful for drilling down into specific parts of an object. + +[cols="1m,2,2"] +|=== +|Filter |Description |Input Context + +|request +|Extracts the Request component from an operation. +|Operation + +|response(code) +|Extracts a specific Response component. + +**Default:** `200` (OK). +|Operation + +|body +|Extracts the Body payload definition. +|Operation / Request / Response + +|form +|Extracts form-data parameters specifically. +|Operation / Request + +|parameters(type) +|Extracts parameters. + +**Default:** Returns all parameters. + +**Filter Arguments:** `query`, `header`, `path`, `cookie`. +|Operation / Request + +|example +|Populates a Schema with example data. +|Schema / Body + +|truncate +|Takes a complex Schema and returns a new Schema containing **only direct fields**. Removes nested objects and deep relationships. +|Schema / Body +|=== + +--- + +===== 5. Display (Renderers) +The final step in the chain. These filters determine how the data is written to the AsciiDoc file. + +[cols="1m,3"] +|=== +|Filter |Description + +|curl +|Generates a ready-to-use **cURL** command. + +Includes method, headers, and body. + +|http +|Renders the raw **HTTP** Request/Response wire format. + +(Status line, Headers, Body). + +|path(params...) +|Renders the full relative URI. + +**Arguments:** Pass `key=value` pairs to override path variables or query parameters in the output. + +_Example:_ `path(id=123, sort="asc")` + +|json +|Renders the input object as a formatted JSON block. + +|yaml +|Renders the input object as a YAML block. + +|table +|Renders a standard AsciiDoc/Markdown table. + +Great for lists of parameters or schema fields. + +|list +|Renders a simple bulleted list. + +Used mostly for Status Codes or Enums. + +|link +|Renders an ascii doc on schema. + +Only for Schemas. + +|=== + +--- + +===== 6. Common Recipes + +====== A. Documenting a Route (The "Standard" Block) +Use this pattern to document a specific endpoint, separating path parameters from query parameters. + +[source, asciidoc] +---- +// 1. Define the route +{% set route = GET("/library/books/{isbn}") %} + +=== {{ route.summary }} + +{{ route.description }} + +// 2. Render Path Params +.Path Parameters +{{ route | parameters(path) | table }} + +// 3. Render Query Params +.Query Parameters +{{ route | parameters(query) | table }} + +// 4. Render Response + example +.Response +{{ route | response | body | example | json }} + +// 5. Render Response and Override Status Code +.Created(201) Response +{{ route | response(201) | http }} + +// 6. Render Response 400 +.Bad Request Response +{{ route | response(400) | http }} + +{{ route | response(400) | json }} +---- + +====== B. The "Try It" Button (cURL) +Provide a copy-paste command for developers. + +[source] +---- +{{ POST("/items") | curl }} +---- + +.Passing curl options +[source] +---- +{{ POST("/items") | curl("-i", "-H", "'Accept: application/xml'") }} +---- + +.Generate a bash source +[source, bash] +---- +{{ POST("/items") | curl(language="bash") }} +---- + +====== C. Simulating specific scenarios +Use the `path` filter to inject specific values into the URL, making the documentation more realistic. + +[source, pebble] +---- +// Scenario: Searching for Sci-Fi books on page 2 +GET {{ GET("/search") | path(q="Sci-Fi", page=2) }} +---- + +_Output:_ `GET /search?q=Sci-Fi&page=2` + +====== D. Simplifying Complex Objects +If your database entities have deep nesting (e.g., Book -> Author -> Address -> Country), use `truncate` to show a summary view. + +[source, pebble] +---- +// Full Graph (Huge JSON) +{{ schema("Book") | example | json }} + +// Summary (Flat JSON) +{{ schema("Book") | truncate | example | json }} +---- + +====== E. Error Response Reference +You can generate error examples using the standard format or a custom structure. + +**1. Default Structure** +Generates a JSON with `statusCode`, `reason`, and `message`. + +[source, pebble] +---- +.404 Not Found +{{ error(404) | json }} +---- + +**2. Custom Error Structure** +Define a variable named `error` with a map containing your fields. Use `{{code}}`, `{{message}}`, and `{{reason}}` placeholders which the engine will automatically populate. + +[source, pebble] +---- +// Define the custom error shape once +{%- set error = { + "code": "{{code}}", + "message": "{{message}}", + "reason": "{{reason}}", + "timestamp": "2025-01-01T12:00:00Z", + "support": "help@example.com" +} -%} + +// Now generate the error. It will use the map above. +.400 Bad Request +{{ error(400) | json }} +---- + +_Output:_ +[source, json] +---- +{ + "code": 400, + "message": "Bad Request", + "reason": "Bad Request", + "timestamp": "2025-01-01T12:00:00Z", + "support": "help@example.com" +} +---- + +====== F. Dynamic Tag Loop +Automatically document your API by iterating over tags defined in Java. + +[source, pebble] +---- +{% for tag in tags %} +== {{ tag.name }} + {% for route in tag.routes %} + === {{ route.summary }} + {{ route.method }} {{ route.path }} + {% endfor %} +{% endfor %} +---- + +--- + +===== 7. Advanced Patterns + +====== G. Reusable Macros (DRY) +As your template grows, use macros to create reusable UI components (like warning blocks or deprecated notices) to keep the main logic clean. + +[source, pebble] +---- +{# 1. Define the Macro at the top of your file #} +{% macro deprecationWarning(since) %} +[WARNING] +==== +This endpoint is deprecated since version {{ since }}. +Please use the newer version instead. +==== +{% endmacro %} + +{# 2. Use it inside your route loop #} +{% if route.deprecated %} + {{ deprecationWarning("v2.1") }} +{% endif %} +---- + +====== H. Security & Permissions +If your API uses authentication (OAuth2, API Keys), the `security` property on the route contains the requirements. + +[source, pebble] +---- +{% if route.security %} +.Required Permissions +[cols="1,3"] +|=== +|Type | Scopes + +{# Iterate through security schemes #} +{% for scheme in route.security %} + {% for req in scheme %} +| *{{ loop.key }}* | {{ req | join(", ") }} + {% endfor %} +{% endfor %} +|=== +{% endif %} +---- + +====== I. Linking to Schema Definitions +AsciiDoc supports internal anchors. You can automatically link a route's return type to its full Schema definition elsewhere in the document. + +[source, pebble] +---- +{# 1. Create Anchors in your Schema Loop #} +{% for s in schemas %} +[id="{{ s.name }}"] +== {{ s.name }} +{{ s | table }} +{% endfor %} + +{# 2. Link to them in your Route Loop #} +.Response Type +Returns a {{ route | response | link }} object. +---- diff --git a/docs/asciidoc/modules/openapi.adoc b/docs/asciidoc/modules/openapi.adoc index 773c3af54c..cd8125b2df 100644 --- a/docs/asciidoc/modules/openapi.adoc +++ b/docs/asciidoc/modules/openapi.adoc @@ -30,6 +30,12 @@ This library supports: openapi + + ... + + ... + + @@ -193,7 +199,9 @@ class Pets { The Maven plugin and Gradle task provide two filter properties `includes` and `excludes`. These properties filter routes by their path pattern. The filter is a regular expression. -=== JavaDoc comments +=== Documenting your API + +==== JavaDoc comments JavaDoc comments are supported on Java in script and MVC routes. @@ -384,6 +392,49 @@ Whitespaces (including new lines) are ignored. To introduce a new line, you must | | +|@securityScheme.name +|[x] +| +| +| + +|@securityScheme.in +|[x] +| +| +| + +|@securityScheme.paramName +|[x] +| +| +| + +|@securityScheme.flows.implicit.authorizationUrl +|[x] +| +| +| + +|@securityScheme.flows.implicit.scopes.name +|[x] +| +| +| + +|@securityScheme.flows.implicit.scopes.description +|[x] +| +| +| + +|@securityRequirement +| +|[x] +|[x] +|Name of the `securityScheme` and optionally scopes. Example: `myOauth2Security read:pets` + + |@server.description |[x] | @@ -443,7 +494,56 @@ Whitespaces (including new lines) are ignored. To introduce a new line, you must This feature is only available for Java routes. Kotlin source code is not supported. -=== Annotations +==== Documentation Template + +The OpenAPI output generates some default values for `info` and `server` section. It generates +the necessary to follow the specification and produces a valid output. These sections can be override +with better information/metadata. + +To do so just write an `openapi.yaml` file inside the `conf` directory to use it as template. + +.conf/openapi.yaml +[source, yaml] +---- +openapi: 3.0.1 +info: + title: My Super API + description: | + Nunc commodo ipsum vitae dignissim congue. Quisque convallis malesuada tortor, non + lacinia quam malesuada id. Curabitur nisi mi, lobortis non tempus vel, vestibulum et neque. + + ... + version: "1.0" + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + paths: + /api/pets: + get: + operationId: listPets + description: List and sort pets. + parameters: + name: page + descripton: Page number. + +---- + +All sections from template file are merged into the final output. + +The extension property: `x-merge-policy` controls how merge must be done: + +- ignore: Silently ignore a path or operation present in template but not found in generated output. This is the default value. +- keep: Add a path or operation to final output. Must be valid path or operation. +- fail: Throw an error when path or operation is present in template but not found in generated output. + +The extension property can be added at root/global level, paths, pathItem, operation or parameter level. + +[NOTE] +==== +Keep in mind that any section found here in the template overrides existing metadata. +==== + +=== Swagger Annotations Optionally this plugin depends on some OpenAPI annotations. To use them, you need to add a dependency to your project: @@ -679,56 +779,11 @@ You need the `ApiResponse` annotation: }) ---- -=== Documentation Template - -The OpenAPI output generates some default values for `info` and `server` section. It generates -the necessary to follow the specification and produces a valid output. These sections can be override -with better information/metadata. - -To do so just write an `openapi.yaml` file inside the `conf` directory to use it as template. - -.conf/openapi.yaml -[source, yaml] ----- -openapi: 3.0.1 -info: - title: My Super API - description: | - Nunc commodo ipsum vitae dignissim congue. Quisque convallis malesuada tortor, non - lacinia quam malesuada id. Curabitur nisi mi, lobortis non tempus vel, vestibulum et neque. - - ... - version: "1.0" - license: - name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0.html - paths: - /api/pets: - get: - operationId: listPets - description: List and sort pets. - parameters: - name: page - descripton: Page number. - ----- - -All sections from template file are merged into the final output. - -The extension property: `x-merge-policy` controls how merge must be done: - -- ignore: Silently ignore a path or operation present in template but not found in generated output. This is the default value. -- keep: Add a path or operation to final output. Must be valid path or operation. -- fail: Throw an error when path or operation is present in template but not found in generated output. - -The extension property can be added at root/global level, paths, pathItem, operation or parameter level. +=== Output/Display -[NOTE] -==== -Keep in mind that any section found here in the template overrides existing metadata. -==== +include::modules/openapi-ascii.adoc[] -=== Swagger UI +==== Swagger UI To use swagger-ui just add the dependency to your project: @@ -737,7 +792,7 @@ To use swagger-ui just add the dependency to your project: The swagger-ui application will be available at `/swagger`. To modify the default path, just call javadoc:OpenAPIModule[swaggerUI, java.lang.String] -=== Redoc +==== Redoc To use redoc just add the dependency to your project: diff --git a/docs/src/main/java/io/jooby/adoc/DocGenerator.java b/docs/src/main/java/io/jooby/adoc/DocGenerator.java index c2e0fa0d6c..2aa68f2be2 100644 --- a/docs/src/main/java/io/jooby/adoc/DocGenerator.java +++ b/docs/src/main/java/io/jooby/adoc/DocGenerator.java @@ -87,29 +87,29 @@ public static void generate(Path basedir, boolean publish, boolean v1, boolean d pb.step(); if (doAscii) { - Asciidoctor asciidoctor = Asciidoctor.Factory.create(); - - asciidoctor.convertFile( - asciidoc.resolve("index.adoc").toFile(), - createOptions(asciidoc, outdir, version, null, asciidoc.resolve("index.adoc"))); - var index = outdir.resolve("index.html"); - Files.writeString(index, hljs(Files.readString(index))); - pb.step(); - - Stream.of(treeDirs) - .forEach( - throwingConsumer( - name -> { - Path modules = outdir.resolve(name); - Files.createDirectories(modules); - Files.walk(asciidoc.resolve(name)) - .filter(Files::isRegularFile) - .forEach( - module -> { - processModule(asciidoctor, asciidoc, module, outdir, name, version); - pb.step(); - }); - })); + try (var asciidoctor = Asciidoctor.Factory.create()) { + asciidoctor.convertFile( + asciidoc.resolve("index.adoc").toFile(), + createOptions(asciidoc, outdir, version, null, asciidoc.resolve("index.adoc"))); + var index = outdir.resolve("index.html"); + Files.writeString(index, hljs(Files.readString(index))); + pb.step(); + + Stream.of(treeDirs) + .forEach( + throwingConsumer( + name -> { + Path modules = outdir.resolve(name); + Files.createDirectories(modules); + Files.walk(asciidoc.resolve(name)) + .filter(Files::isRegularFile) + .forEach( + module -> { + processModule(asciidoctor, asciidoc, module, outdir, name, version); + pb.step(); + }); + })); + } } // LICENSE @@ -280,7 +280,7 @@ private static Options createOptions(Path basedir, Path outdir, String version, attributes.attribute("date", DateTimeFormatter.ISO_INSTANT.format(Instant.now())); OptionsBuilder options = Options.builder(); - options.backend("html"); + options.backend("html5"); options.attributes(attributes.build()); options.baseDir(basedir.toAbsolutePath().toFile()); diff --git a/jooby/src/main/java/io/jooby/StatusCode.java b/jooby/src/main/java/io/jooby/StatusCode.java index 1dd1e9b1c5..0292bcf902 100644 --- a/jooby/src/main/java/io/jooby/StatusCode.java +++ b/jooby/src/main/java/io/jooby/StatusCode.java @@ -922,6 +922,11 @@ private StatusCode(final int value, final String reason) { this.reason = reason; } + private StatusCode(final int value) { + this.value = value; + this.reason = Integer.toString(value); + } + /** * Return the integer value of this status code. * @@ -971,129 +976,68 @@ public int hashCode() { * @throws IllegalArgumentException if this enum has no constant for the specified numeric value */ public static StatusCode valueOf(final int statusCode) { - switch (statusCode) { - case CONTINUE_CODE: - return CONTINUE; - case SWITCHING_PROTOCOLS_CODE: - return SWITCHING_PROTOCOLS; - case PROCESSING_CODE: - return PROCESSING; - case CHECKPOINT_CODE: - return CHECKPOINT; - case OK_CODE: - return OK; - case CREATED_CODE: - return CREATED; - case ACCEPTED_CODE: - return ACCEPTED; - case NON_AUTHORITATIVE_INFORMATION_CODE: - return NON_AUTHORITATIVE_INFORMATION; - case NO_CONTENT_CODE: - return NO_CONTENT; - case RESET_CONTENT_CODE: - return RESET_CONTENT; - case PARTIAL_CONTENT_CODE: - return PARTIAL_CONTENT; - case MULTI_STATUS_CODE: - return MULTI_STATUS; - case ALREADY_REPORTED_CODE: - return ALREADY_REPORTED; - case IM_USED_CODE: - return IM_USED; - case MULTIPLE_CHOICES_CODE: - return MULTIPLE_CHOICES; - case MOVED_PERMANENTLY_CODE: - return MOVED_PERMANENTLY; - case FOUND_CODE: - return FOUND; - case SEE_OTHER_CODE: - return SEE_OTHER; - case NOT_MODIFIED_CODE: - return NOT_MODIFIED; - case USE_PROXY_CODE: - return USE_PROXY; - case TEMPORARY_REDIRECT_CODE: - return TEMPORARY_REDIRECT; - case RESUME_INCOMPLETE_CODE: - return RESUME_INCOMPLETE; - case BAD_REQUEST_CODE: - return BAD_REQUEST; - case UNAUTHORIZED_CODE: - return UNAUTHORIZED; - case PAYMENT_REQUIRED_CODE: - return PAYMENT_REQUIRED; - case FORBIDDEN_CODE: - return FORBIDDEN; - case NOT_FOUND_CODE: - return NOT_FOUND; - case METHOD_NOT_ALLOWED_CODE: - return METHOD_NOT_ALLOWED; - case NOT_ACCEPTABLE_CODE: - return NOT_ACCEPTABLE; - case PROXY_AUTHENTICATION_REQUIRED_CODE: - return PROXY_AUTHENTICATION_REQUIRED; - case REQUEST_TIMEOUT_CODE: - return REQUEST_TIMEOUT; - case CONFLICT_CODE: - return CONFLICT; - case GONE_CODE: - return GONE; - case LENGTH_REQUIRED_CODE: - return LENGTH_REQUIRED; - case PRECONDITION_FAILED_CODE: - return PRECONDITION_FAILED; - case REQUEST_ENTITY_TOO_LARGE_CODE: - return REQUEST_ENTITY_TOO_LARGE; - case REQUEST_URI_TOO_LONG_CODE: - return REQUEST_URI_TOO_LONG; - case UNSUPPORTED_MEDIA_TYPE_CODE: - return UNSUPPORTED_MEDIA_TYPE; - case REQUESTED_RANGE_NOT_SATISFIABLE_CODE: - return REQUESTED_RANGE_NOT_SATISFIABLE; - case EXPECTATION_FAILED_CODE: - return EXPECTATION_FAILED; - case I_AM_A_TEAPOT_CODE: - return I_AM_A_TEAPOT; - case UNPROCESSABLE_ENTITY_CODE: - return UNPROCESSABLE_ENTITY; - case LOCKED_CODE: - return LOCKED; - case FAILED_DEPENDENCY_CODE: - return FAILED_DEPENDENCY; - case UPGRADE_REQUIRED_CODE: - return UPGRADE_REQUIRED; - case PRECONDITION_REQUIRED_CODE: - return PRECONDITION_REQUIRED; - case TOO_MANY_REQUESTS_CODE: - return TOO_MANY_REQUESTS; - case REQUEST_HEADER_FIELDS_TOO_LARGE_CODE: - return REQUEST_HEADER_FIELDS_TOO_LARGE; - case SERVER_ERROR_CODE: - return SERVER_ERROR; - case NOT_IMPLEMENTED_CODE: - return NOT_IMPLEMENTED; - case BAD_GATEWAY_CODE: - return BAD_GATEWAY; - case SERVICE_UNAVAILABLE_CODE: - return SERVICE_UNAVAILABLE; - case GATEWAY_TIMEOUT_CODE: - return GATEWAY_TIMEOUT; - case HTTP_VERSION_NOT_SUPPORTED_CODE: - return HTTP_VERSION_NOT_SUPPORTED; - case VARIANT_ALSO_NEGOTIATES_CODE: - return VARIANT_ALSO_NEGOTIATES; - case INSUFFICIENT_STORAGE_CODE: - return INSUFFICIENT_STORAGE; - case LOOP_DETECTED_CODE: - return LOOP_DETECTED; - case BANDWIDTH_LIMIT_EXCEEDED_CODE: - return BANDWIDTH_LIMIT_EXCEEDED; - case NOT_EXTENDED_CODE: - return NOT_EXTENDED; - case NETWORK_AUTHENTICATION_REQUIRED_CODE: - return NETWORK_AUTHENTICATION_REQUIRED; - default: - return new StatusCode(statusCode, Integer.toString(statusCode)); - } + return switch (statusCode) { + case CONTINUE_CODE -> CONTINUE; + case SWITCHING_PROTOCOLS_CODE -> SWITCHING_PROTOCOLS; + case PROCESSING_CODE -> PROCESSING; + case CHECKPOINT_CODE -> CHECKPOINT; + case OK_CODE -> OK; + case CREATED_CODE -> CREATED; + case ACCEPTED_CODE -> ACCEPTED; + case NON_AUTHORITATIVE_INFORMATION_CODE -> NON_AUTHORITATIVE_INFORMATION; + case NO_CONTENT_CODE -> NO_CONTENT; + case RESET_CONTENT_CODE -> RESET_CONTENT; + case PARTIAL_CONTENT_CODE -> PARTIAL_CONTENT; + case MULTI_STATUS_CODE -> MULTI_STATUS; + case ALREADY_REPORTED_CODE -> ALREADY_REPORTED; + case IM_USED_CODE -> IM_USED; + case MULTIPLE_CHOICES_CODE -> MULTIPLE_CHOICES; + case MOVED_PERMANENTLY_CODE -> MOVED_PERMANENTLY; + case FOUND_CODE -> FOUND; + case SEE_OTHER_CODE -> SEE_OTHER; + case NOT_MODIFIED_CODE -> NOT_MODIFIED; + case USE_PROXY_CODE -> USE_PROXY; + case TEMPORARY_REDIRECT_CODE -> TEMPORARY_REDIRECT; + case RESUME_INCOMPLETE_CODE -> RESUME_INCOMPLETE; + case BAD_REQUEST_CODE -> BAD_REQUEST; + case UNAUTHORIZED_CODE -> UNAUTHORIZED; + case PAYMENT_REQUIRED_CODE -> PAYMENT_REQUIRED; + case FORBIDDEN_CODE -> FORBIDDEN; + case NOT_FOUND_CODE -> NOT_FOUND; + case METHOD_NOT_ALLOWED_CODE -> METHOD_NOT_ALLOWED; + case NOT_ACCEPTABLE_CODE -> NOT_ACCEPTABLE; + case PROXY_AUTHENTICATION_REQUIRED_CODE -> PROXY_AUTHENTICATION_REQUIRED; + case REQUEST_TIMEOUT_CODE -> REQUEST_TIMEOUT; + case CONFLICT_CODE -> CONFLICT; + case GONE_CODE -> GONE; + case LENGTH_REQUIRED_CODE -> LENGTH_REQUIRED; + case PRECONDITION_FAILED_CODE -> PRECONDITION_FAILED; + case REQUEST_ENTITY_TOO_LARGE_CODE -> REQUEST_ENTITY_TOO_LARGE; + case REQUEST_URI_TOO_LONG_CODE -> REQUEST_URI_TOO_LONG; + case UNSUPPORTED_MEDIA_TYPE_CODE -> UNSUPPORTED_MEDIA_TYPE; + case REQUESTED_RANGE_NOT_SATISFIABLE_CODE -> REQUESTED_RANGE_NOT_SATISFIABLE; + case EXPECTATION_FAILED_CODE -> EXPECTATION_FAILED; + case I_AM_A_TEAPOT_CODE -> I_AM_A_TEAPOT; + case UNPROCESSABLE_ENTITY_CODE -> UNPROCESSABLE_ENTITY; + case LOCKED_CODE -> LOCKED; + case FAILED_DEPENDENCY_CODE -> FAILED_DEPENDENCY; + case UPGRADE_REQUIRED_CODE -> UPGRADE_REQUIRED; + case PRECONDITION_REQUIRED_CODE -> PRECONDITION_REQUIRED; + case TOO_MANY_REQUESTS_CODE -> TOO_MANY_REQUESTS; + case REQUEST_HEADER_FIELDS_TOO_LARGE_CODE -> REQUEST_HEADER_FIELDS_TOO_LARGE; + case SERVER_ERROR_CODE -> SERVER_ERROR; + case NOT_IMPLEMENTED_CODE -> NOT_IMPLEMENTED; + case BAD_GATEWAY_CODE -> BAD_GATEWAY; + case SERVICE_UNAVAILABLE_CODE -> SERVICE_UNAVAILABLE; + case GATEWAY_TIMEOUT_CODE -> GATEWAY_TIMEOUT; + case HTTP_VERSION_NOT_SUPPORTED_CODE -> HTTP_VERSION_NOT_SUPPORTED; + case VARIANT_ALSO_NEGOTIATES_CODE -> VARIANT_ALSO_NEGOTIATES; + case INSUFFICIENT_STORAGE_CODE -> INSUFFICIENT_STORAGE; + case LOOP_DETECTED_CODE -> LOOP_DETECTED; + case BANDWIDTH_LIMIT_EXCEEDED_CODE -> BANDWIDTH_LIMIT_EXCEEDED; + case NOT_EXTENDED_CODE -> NOT_EXTENDED; + case NETWORK_AUTHENTICATION_REQUIRED_CODE -> NETWORK_AUTHENTICATION_REQUIRED; + default -> new StatusCode(statusCode); + }; } } diff --git a/modules/jooby-gradle-plugin/src/main/java/io/jooby/gradle/OpenAPITask.java b/modules/jooby-gradle-plugin/src/main/java/io/jooby/gradle/OpenAPITask.java index 6940c6a5ad..9dc4ba6ba4 100644 --- a/modules/jooby-gradle-plugin/src/main/java/io/jooby/gradle/OpenAPITask.java +++ b/modules/jooby-gradle-plugin/src/main/java/io/jooby/gradle/OpenAPITask.java @@ -17,8 +17,11 @@ import java.io.File; import java.nio.file.Path; import java.util.List; +import java.util.Map; import java.util.Optional; +import static java.util.Optional.ofNullable; + /** * Generate an OpenAPI file from a jooby application. * @@ -37,6 +40,8 @@ public class OpenAPITask extends BaseTask { private String specVersion; + private List adoc; + /** * Creates an OpenAPI task. */ @@ -61,7 +66,8 @@ public void generate() throws Throwable { .map(File::toPath); }) .distinct() - .toList(); Path outputDir = classes(getProject(), false); + .toList(); + Path outputDir = classes(getProject(), false); ClassLoader classLoader = createClassLoader(projects); @@ -82,9 +88,10 @@ public void generate() throws Throwable { OpenAPI result = tool.generate(mainClass); - for (OpenAPIGenerator.Format format : OpenAPIGenerator.Format.values()) { - Path output = tool.export(result, format); - getLogger().info(" writing: " + output); + var adocPath = ofNullable(adoc).orElse(List.of()).stream().map(File::toPath).toList(); + for (var format : OpenAPIGenerator.Format.values()) { + tool.export(result, format, Map.of("adoc", adocPath)) + .forEach(output -> getLogger().info(" writing: " + output)); } } @@ -188,6 +195,26 @@ public void setSpecVersion(String specVersion) { this.specVersion = specVersion; } + /** + * Optionally generates adoc output. + * + * @return List of adoc templates. + */ + @Input + @org.gradle.api.tasks.Optional + public List getAdoc() { + return adoc; + } + + /** + * Set adoc templates to build. + * + * @param adoc Adoc templates to build. + */ + public void setAdoc(List adoc) { + this.adoc = adoc; + } + private Optional trim(String value) { if (value == null || value.trim().isEmpty()) { return Optional.empty(); diff --git a/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java b/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java index 6a2db33c23..35f8d41a1d 100644 --- a/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java +++ b/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java @@ -5,12 +5,15 @@ */ package io.jooby.maven; +import static java.util.Optional.ofNullable; import static org.apache.maven.plugins.annotations.LifecyclePhase.PROCESS_CLASSES; import static org.apache.maven.plugins.annotations.ResolutionScope.COMPILE_PLUS_RUNTIME; +import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.Map; import java.util.Optional; import org.apache.maven.plugins.annotations.Mojo; @@ -20,7 +23,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.openapi.OpenAPIGenerator; -import io.swagger.v3.oas.models.OpenAPI; /** * Generate an OpenAPI file from a jooby application. @@ -47,6 +49,8 @@ public class OpenAPIMojo extends BaseMojo { @Parameter(property = "openAPI.specVersion") private String specVersion; + @Parameter private List adoc; + @Override protected void doExecute(@NonNull List projects, @NonNull String mainClass) throws Exception { @@ -73,16 +77,17 @@ protected void doExecute(@NonNull List projects, @NonNull String m trim(includes).ifPresent(tool::setIncludes); trim(excludes).ifPresent(tool::setExcludes); - OpenAPI result = tool.generate(mainClass); + var result = tool.generate(mainClass); - for (OpenAPIGenerator.Format format : OpenAPIGenerator.Format.values()) { - Path output = tool.export(result, format); - getLog().info(" writing: " + output); + var adocPath = ofNullable(adoc).orElse(List.of()).stream().map(File::toPath).toList(); + for (var format : OpenAPIGenerator.Format.values()) { + tool.export(result, format, Map.of("adoc", adocPath)) + .forEach(output -> getLog().info(" writing: " + output)); } } private Optional trim(String value) { - if (value == null || value.trim().length() == 0) { + if (value == null || value.trim().isEmpty()) { return Optional.empty(); } return Optional.of(value.trim()); @@ -131,4 +136,12 @@ public String getSpecVersion() { public void setSpecVersion(String specVersion) { this.specVersion = specVersion; } + + public List getAdoc() { + return adoc; + } + + public void setAdoc(List adoc) { + this.adoc = adoc; + } } diff --git a/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/RunMojo.java b/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/RunMojo.java index 924e23cc07..a535bf8e1b 100644 --- a/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/RunMojo.java +++ b/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/RunMojo.java @@ -24,6 +24,8 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.jooby.run.JoobyRun; import io.jooby.run.JoobyRunOptions; @@ -42,8 +44,10 @@ @Execute(phase = PROCESS_CLASSES) public class RunMojo extends BaseMojo { + private static final Logger log = LoggerFactory.getLogger(RunMojo.class); + static { - /** Turn off shutdown hook on Server. */ + /* Turn off shutdown hook on Server. */ System.setProperty("jooby.useShutdownHook", "false"); } @@ -97,7 +101,13 @@ protected void doExecute(List projects, String mainClass) throws T var error = result.hasExceptions(); // Success? if (error) { - getLog().debug("Compilation error found: " + path); + var filename = path.getFileName().toFile().toString(); + var isSource = filename.endsWith(".java") || filename.endsWith(".kt"); + for (Throwable exception : result.getExceptions()) { + if (!isSource) { + getLog().error(exception); + } + } } return !error; }); @@ -213,8 +223,7 @@ protected void setUseTestScope(boolean useTestScope) { * @return Request. */ private MavenExecutionRequest mavenRequest(String goal) { - return DefaultMavenExecutionRequest.copy(session.getRequest()) - .setGoals(Collections.singletonList(goal)); + return DefaultMavenExecutionRequest.copy(session.getRequest()).setGoals(List.of(goal)); } private Set sourceDirectories(MavenProject project, boolean useTestScope) { diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 15ee4c1a6b..a4eeae25bc 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -51,6 +51,21 @@ 12.2.0 + + org.asciidoctor + asciidoctorj + 3.0.1 + + + io.pebbletemplates + pebble + + + + net.datafaker + datafaker + 2.5.3 + commons-codec commons-codec @@ -62,6 +77,17 @@ swagger-parser + + com.google.guava + guava + + + + jakarta.data + jakarta.data-api + 1.0.1 + + org.junit.jupiter @@ -128,6 +154,17 @@ 1.18.2 test + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + 3.27.6 + test + diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java index ae33cb8d56..349a47521d 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java @@ -21,14 +21,7 @@ import org.objectweb.asm.tree.*; import io.jooby.*; -import io.jooby.annotation.ContextParam; -import io.jooby.annotation.CookieParam; -import io.jooby.annotation.FormParam; -import io.jooby.annotation.GET; -import io.jooby.annotation.HeaderParam; -import io.jooby.annotation.Path; -import io.jooby.annotation.PathParam; -import io.jooby.annotation.QueryParam; +import io.jooby.annotation.*; import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; @@ -314,6 +307,7 @@ private static Map methods(ParserContext ctx, ClassNode node return methods; } + @SuppressWarnings("unchecked") private static List routerMethod( ParserContext ctx, String prefix, ClassNode classNode, MethodNode method) { @@ -330,6 +324,9 @@ private static List routerMethod( operation.setOperationId(method.name); Optional.ofNullable(requestBody.get()).ifPresent(operation::setRequestBody); + mediaType(classNode, method, produces(), operation::addProduces); + mediaType(classNode, method, consumes(), operation::addConsumes); + result.add(operation); } } @@ -337,6 +334,25 @@ private static List routerMethod( return result; } + @SuppressWarnings("unchecked") + public static void mediaType( + ClassNode classNode, MethodNode method, List types, Consumer consumer) { + mediaType(classNode, method, types).stream() + .map(AsmUtils::toMap) + .map(it -> it.get("value")) + .filter(Objects::nonNull) + .map(List.class::cast) + .flatMap(List::stream) + .distinct() + .forEach(it -> consumer.accept(it.toString())); + } + + public static List mediaType( + ClassNode classNode, MethodNode method, List types) { + var result = findAnnotationByType(method.visibleAnnotations, types); + return result.isEmpty() ? findAnnotationByType(classNode.visibleAnnotations, types) : result; + } + private static ResponseExt returnTypes(MethodNode method) { Signature signature = Signature.create(method); String desc = Optional.ofNullable(method.signature).orElse(method.desc); @@ -604,6 +620,14 @@ private static List httpMethods() { return annotationTypes; } + private static List produces() { + return List.of(Produces.class.getName(), jakarta.ws.rs.Produces.class.getName()); + } + + private static List consumes() { + return List.of(Consumes.class.getName(), jakarta.ws.rs.Consumes.class.getName()); + } + private static List httpMethod(String pkg, Class pathType) { List annotationTypes = Router.METHODS.stream().map(m -> pkg + "." + m).collect(Collectors.toList()); diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ArrayLikeSchema.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ArrayLikeSchema.java new file mode 100644 index 0000000000..e853acf5b3 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ArrayLikeSchema.java @@ -0,0 +1,23 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.models.media.Schema; + +@JsonIgnoreProperties({"items"}) +public class ArrayLikeSchema extends Schema { + + public static ArrayLikeSchema create(Schema schema, Schema items) { + var arrayLikeSchema = new ArrayLikeSchema<>(); + arrayLikeSchema.setItems(items); + arrayLikeSchema.setProperties(schema.getProperties()); + arrayLikeSchema.setType(schema.getType()); + arrayLikeSchema.setTypes(schema.getTypes()); + arrayLikeSchema.setName(schema.getName()); + return arrayLikeSchema; + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/EnumSchema.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/EnumSchema.java new file mode 100644 index 0000000000..b9555d7102 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/EnumSchema.java @@ -0,0 +1,35 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.models.media.StringSchema; + +public class EnumSchema extends StringSchema { + @JsonIgnore private final Map fields = new HashMap<>(); + @JsonIgnore private String summary; + + public EnumSchema() {} + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public void setDescription(String name, String description) { + fields.put(name, description); + } + + public String getDescription(String name) { + return fields.get(name); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/MixinHook.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/MixinHook.java new file mode 100644 index 0000000000..099080a5ad --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/MixinHook.java @@ -0,0 +1,75 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.annotations.ApiModelProperty; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; + +public class MixinHook { + + @JsonPropertyOrder({ + "content", + "numberOfElements", + "totalElements", + "totalPages", + "pageRequest", + "nextPageRequest", + "previousPageRequest" + }) + public abstract static class PageMixin implements Page { + @JsonProperty("content") + @Override + public abstract List content(); + + @JsonProperty("numberOfElements") + @Override + public abstract int numberOfElements(); + + @Override + @JsonProperty("pageRequest") + public abstract PageRequest pageRequest(); + + @Override + @JsonProperty("nextPageRequest") + public abstract PageRequest nextPageRequest(); + + @Override + @JsonProperty("previousPageRequest") + public abstract PageRequest previousPageRequest(); + + @Override + @JsonProperty("totalElements") + public abstract long totalElements(); + + @Override + @JsonProperty("totalPages") + public abstract long totalPages(); + } + + @JsonPropertyOrder({"page", "size"}) + public abstract static class PageRequestMixin implements PageRequest { + @JsonProperty("page") + @Override + @ApiModelProperty("The page to be returned") + public abstract long page(); + + @JsonProperty("size") + @Override + @ApiModelProperty("The requested size of each page") + public abstract int size(); + } + + public static void mixin(ObjectMapper mapper) { + mapper.addMixIn(jakarta.data.page.Page.class, PageMixin.class); + mapper.addMixIn(jakarta.data.page.PageRequest.class, PageRequestMixin.class); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ModelConverterExt.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ModelConverterExt.java index 32bead9525..96b5351bfa 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ModelConverterExt.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ModelConverterExt.java @@ -11,10 +11,7 @@ import java.util.Set; import com.fasterxml.jackson.databind.ObjectMapper; -import io.jooby.FileUpload; -import io.jooby.Jooby; -import io.jooby.Router; -import io.jooby.ServiceRegistry; +import io.jooby.*; import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverter; import io.swagger.v3.core.converter.ModelConverterContext; diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIExt.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIExt.java index 43f5d38a81..0004a40ab1 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIExt.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIExt.java @@ -9,6 +9,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import com.fasterxml.jackson.annotation.JsonIgnore; import io.jooby.Router; @@ -233,4 +234,18 @@ private void setProperty(S src, Function getter, S target, BiConsum } } } + + public OperationExt findOperation(String method, String pattern) { + Predicate filter = op -> op.getPath().equals(pattern); + filter = filter.and(op -> op.getMethod().equals(method)); + return getOperations().stream() + .filter(filter) + .findFirst() + .orElseThrow( + () -> new IllegalArgumentException("Operation not found: " + method + " " + pattern)); + } + + public List findOperationByTag(String tag) { + return getOperations().stream().filter(it -> it.isOnTag(tag)).toList(); + } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIParser.java index 5eed7d7b73..58dc070599 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIParser.java @@ -604,7 +604,7 @@ private static void operationResponse( String name = stringValue(value, "name"); stringValue(value, "description", header::setDescription); - io.swagger.v3.oas.models.media.Schema schema = + var schema = annotationValue(value, "schema") .map(schemaMap -> toSchema(ctx, schemaMap).orElseGet(StringSchema::new)) .orElseGet(StringSchema::new); diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java index 50426437dc..f04c8d3ab2 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java @@ -27,7 +27,7 @@ public class OperationExt extends io.swagger.v3.oas.models.Operation { @JsonIgnore private final MethodNode node; @JsonIgnore private String method; - @JsonIgnore private final String pattern; + @JsonIgnore private final String path; @JsonIgnore private Boolean hidden; @JsonIgnore private LinkedList produces = new LinkedList<>(); @JsonIgnore private LinkedList consumes = new LinkedList<>(); @@ -41,10 +41,10 @@ public class OperationExt extends io.swagger.v3.oas.models.Operation { @JsonIgnore private ClassNode controller; public OperationExt( - MethodNode node, String method, String pattern, List arguments, ResponseExt response) { + MethodNode node, String method, String path, List arguments, ResponseExt response) { this.node = node; this.method = method.toUpperCase(); - this.pattern = pattern; + this.path = path; setParameters(arguments); this.defaultResponse = response; setResponses(apiResponses(Collections.singletonList(response))); @@ -87,8 +87,8 @@ public void setMethod(String method) { this.method = method; } - public String getPattern() { - return pattern; + public String getPath() { + return path; } public List getProduces() { @@ -120,7 +120,7 @@ public void setHidden(Boolean hidden) { } public String toString() { - return getMethod() + " " + getPattern(); + return getMethod() + " " + getPath(); } public Parameter getParameter(int i) { @@ -181,6 +181,10 @@ public void addTag(Tag tag) { addTagsItem(tag.getName()); } + public boolean isOnTag(String tag) { + return globalTags.stream().map(Tag::getName).anyMatch(tag::equals); + } + public List getGlobalTags() { return globalTags; } @@ -267,4 +271,8 @@ public OperationExt copy(String pattern) { copy.setPathExtensions(getPathExtensions()); return copy; } + + public String getPath(Map pathParams) { + return Router.reverse(getPath(), pathParams); + } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParameterExt.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParameterExt.java index 16c05e9a1e..b8782d79d7 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParameterExt.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParameterExt.java @@ -8,8 +8,12 @@ import java.util.Objects; import com.fasterxml.jackson.annotation.JsonIgnore; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; -public class ParameterExt extends io.swagger.v3.oas.models.parameters.Parameter { +public class ParameterExt extends Parameter { @JsonIgnore private String javaType; @JsonIgnore private Object defaultValue; @@ -54,4 +58,22 @@ public void setRequired(Boolean required) { public String toString() { return javaType + " " + getName(); } + + public static Parameter header(@NonNull String name, @Nullable String value) { + return basic(name, "header", value); + } + + public static Parameter cookie(@NonNull String name, @Nullable String value) { + return basic(name, "cookie", value); + } + + public static Parameter basic(@NonNull String name, @NonNull String in, @Nullable String value) { + ParameterExt param = new ParameterExt(); + param.setName(name); + param.setIn(in); + param.setDefaultValue(value); + param.setSchema(new StringSchema()); + param.setJavaType(String.class.getName()); + return param; + } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java index fb1e47cfae..ecaa165cbb 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java @@ -30,20 +30,7 @@ import java.time.OffsetDateTime; import java.time.Period; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Currency; -import java.util.Date; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; @@ -75,20 +62,9 @@ import io.jooby.openapi.DebugOption; import io.swagger.v3.core.util.RefUtils; import io.swagger.v3.oas.models.SpecVersion; -import io.swagger.v3.oas.models.media.ArraySchema; -import io.swagger.v3.oas.models.media.BinarySchema; -import io.swagger.v3.oas.models.media.BooleanSchema; -import io.swagger.v3.oas.models.media.ByteArraySchema; -import io.swagger.v3.oas.models.media.DateSchema; -import io.swagger.v3.oas.models.media.DateTimeSchema; -import io.swagger.v3.oas.models.media.FileSchema; -import io.swagger.v3.oas.models.media.IntegerSchema; -import io.swagger.v3.oas.models.media.MapSchema; -import io.swagger.v3.oas.models.media.NumberSchema; -import io.swagger.v3.oas.models.media.ObjectSchema; -import io.swagger.v3.oas.models.media.Schema; -import io.swagger.v3.oas.models.media.StringSchema; -import io.swagger.v3.oas.models.media.UUIDSchema; +import io.swagger.v3.oas.models.media.*; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; public class ParserContext { @@ -147,7 +123,7 @@ private ParserContext( } private void jacksonModules(ClassLoader classLoader, List mappers) { - /** Kotlin module? */ + /* Kotlin module? */ List modules = new ArrayList<>(2); try { var kotlinModuleClass = @@ -162,7 +138,7 @@ private void jacksonModules(ClassLoader classLoader, List mappers) | InvocationTargetException x) { // Sshhhhh } - /** Ignore some conflictive setter in Jooby API: */ + /* Ignore some conflictive setter in Jooby API: */ modules.add( new SimpleModule("jooby-openapi") { @Override @@ -171,13 +147,14 @@ public void setupModule(SetupContext context) { context.insertAnnotationIntrospector(new ConflictiveSetter()); } }); - /** Java8/Optional: */ + /* Java8/Optional: */ modules.add(new Jdk8Module()); modules.forEach(module -> mappers.forEach(mapper -> mapper.registerModule(module))); - /** Set class loader: */ - mappers.stream() - .forEach( - mapper -> mapper.setTypeFactory(mapper.getTypeFactory().withClassLoader(classLoader))); + /* Set class loader: */ + mappers.forEach( + mapper -> mapper.setTypeFactory(mapper.getTypeFactory().withClassLoader(classLoader))); + /* Mixin */ + mappers.forEach(MixinHook::mixin); } public Collection schemas() { @@ -280,7 +257,7 @@ public Schema schema(Class type) { return new ObjectSchema(); } if (type.isEnum()) { - StringSchema schema = new StringSchema(); + var schema = new EnumSchema(); EnumSet.allOf(type).forEach(e -> schema.addEnumItem(((Enum) e).name())); return schema; } @@ -330,36 +307,57 @@ private void document(Class typeName, Schema schema, ResolvedSchemaExt resolvedS .ifPresent( javadoc -> { Optional.ofNullable(javadoc.getText()).ifPresent(schema::setDescription); + // make a copy Map properties = schema.getProperties(); if (properties != null) { - properties.forEach( - (key, value) -> { - var text = javadoc.getPropertyDoc(key); - var propertyType = getPropertyType(typeName, key); - var isEnum = - propertyType != null - && propertyType.isEnum() - && resolvedSchema.referencedSchemasByType.keySet().stream() - .map(this::toClass) - .anyMatch(it -> !it.equals(propertyType)); - if (isEnum) { - javadocParser - .parse(propertyType.getName()) - .ifPresent( - enumDoc -> { - var enumDesc = enumDoc.getEnumDescription(text); - if (enumDesc != null) { - value.setDescription(enumDesc); - } - }); - } else { - value.setDescription(text); - var example = javadoc.getPropertyExample(key); - if (example != null) { - value.setExample(example); - } - } - }); + new LinkedHashMap<>(properties) + .forEach( + (key, value) -> { + var text = javadoc.getPropertyDoc(key); + var propertyType = getPropertyType(typeName, key); + var isEnum = + propertyType != null + && propertyType.isEnum() + && resolvedSchema.referencedSchemasByType.keySet().stream() + .map(this::toClass) + .anyMatch(it -> !it.equals(propertyType)); + if (isEnum) { + javadocParser + .parse(propertyType.getName()) + .ifPresent( + enumDoc -> { + var enumDesc = enumDoc.getEnumDescription(text); + if (enumDesc != null) { + EnumSchema enumSchema; + if (!(value instanceof EnumSchema)) { + enumSchema = new EnumSchema(); + value.getEnum().stream() + .forEach( + enumValue -> + enumSchema.addEnumItemObject( + enumValue.toString())); + properties.put(key, enumSchema); + } else { + enumSchema = (EnumSchema) value; + } + for (var field : enumSchema.getEnum()) { + var enumItemDesc = enumDoc.getEnumItemDescription(field); + if (enumItemDesc != null) { + enumSchema.setDescription(field, enumItemDesc); + } + } + enumSchema.setSummary(enumDoc.getSummary()); + enumSchema.setDescription(enumDesc); + } + }); + } else { + value.setDescription(text); + var example = javadoc.getPropertyExample(key); + if (example != null) { + value.setExample(example); + } + } + }); } }); } @@ -403,12 +401,8 @@ public Optional schemaRef(String type) { } public Schema schema(Type type) { - if (isArray(type)) { - // For array we need internal name :S - return schema(type.getInternalName()); - } else { - return schema(type.getClassName()); - } + // For array we need internal name :S + return schema(isArray(type) ? type.getInternalName() : type.getClassName()); } private boolean isArray(Type type) { @@ -452,6 +446,27 @@ private Schema schema(JavaType type) { MapSchema mapSchema = new MapSchema(); mapSchema.setAdditionalProperties(schema(type.getContentType())); return mapSchema; + } else if (type.getRawClass() == Page.class) { + // must be embedded it mimics a List. This is bc it might have a different item type + // per operation. + var pageSchema = converters.read(type.getRawClass()).get("Page"); + + var pageRequestSchema = converters.read(PageRequest.class).get("PageRequest"); + pageSchema.getProperties().put("pageRequest", pageRequestSchema); + pageSchema.getProperties().put("nextPageRequest", pageRequestSchema); + pageSchema.getProperties().put("previousPageRequest", pageRequestSchema); + + var params = type.getBindings().getTypeParameters(); + Schema element; + if (params != null && !params.isEmpty()) { + element = schema(params.getFirst()); + Schema contentSchema = (Schema) pageSchema.getProperties().get("content"); + contentSchema.setItems(element); + } else { + element = new Schema<>(); + element.setType("object"); + } + return ArrayLikeSchema.create(pageSchema, element); } return schema(type.getRawClass()); } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java index e66514b93e..bfc05fb7f3 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java @@ -63,7 +63,6 @@ public List parse(ParserContext ctx, OpenAPIExt openapi) { String applicationName = Optional.ofNullable(ctx.getMainClass()).orElse(ctx.getRouter().getClassName()); ClassNode application = ctx.classNode(Type.getObjectType(applicationName.replace(".", "/"))); - // JavaDoc addJavaDoc(ctx, ctx.getRouter().getClassName(), "", operations); @@ -75,7 +74,7 @@ public List parse(ParserContext ctx, OpenAPIExt openapi) { List result = new ArrayList<>(); for (OperationExt operation : operations) { - List patterns = Router.expandOptionalVariables(operation.getPattern()); + List patterns = Router.expandOptionalVariables(operation.getPath()); if (patterns.size() == 1) { result.add(operation); } else { @@ -112,8 +111,7 @@ private static void addJavaDoc( if (operation.getController() == null) { javaDoc .flatMap( - doc -> - doc.getScript(operation.getMethod(), operation.getPattern().substring(offset))) + doc -> doc.getScript(operation.getMethod(), operation.getPath().substring(offset))) .ifPresent( scriptDoc -> { if (scriptDoc.getPath() != null) { @@ -164,12 +162,11 @@ private void checkRequestBody(ParserContext ctx, OperationExt operation) { if (requestBody != null) { if (requestBody.getContent() == null) { // default content - io.swagger.v3.oas.models.media.MediaType mediaType = - new io.swagger.v3.oas.models.media.MediaType(); + var mediaType = new io.swagger.v3.oas.models.media.MediaType(); mediaType.setSchema(ctx.schema(requestBody.getJavaType())); String mediaTypeName = operation.getConsumes().stream().findFirst().orElseGet(requestBody::getContentType); - Content content = new Content(); + var content = new Content(); content.addMediaType(mediaTypeName, mediaType); requestBody.setContent(content); } @@ -250,8 +247,7 @@ private void uniqueOperationId(List operations) { private String operationId(OperationExt operation) { return Optional.ofNullable(operation.getOperationId()) .orElseGet( - () -> - operation.getMethod().toLowerCase() + patternToOperationId(operation.getPattern())); + () -> operation.getMethod().toLowerCase() + patternToOperationId(operation.getPath())); } private String patternToOperationId(String pattern) { diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java new file mode 100644 index 0000000000..c599382a7d --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java @@ -0,0 +1,521 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import static io.swagger.v3.oas.models.Components.COMPONENTS_SCHEMAS_REF; +import static java.util.Optional.ofNullable; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Options; +import org.asciidoctor.SafeMode; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import io.jooby.SneakyThrows; +import io.jooby.StatusCode; +import io.jooby.internal.openapi.OpenAPIExt; +import io.pebbletemplates.pebble.PebbleEngine; +import io.pebbletemplates.pebble.error.PebbleException; +import io.pebbletemplates.pebble.extension.AbstractExtension; +import io.pebbletemplates.pebble.extension.Filter; +import io.pebbletemplates.pebble.extension.Function; +import io.pebbletemplates.pebble.lexer.Syntax; +import io.pebbletemplates.pebble.loader.ClasspathLoader; +import io.pebbletemplates.pebble.loader.DelegatingLoader; +import io.pebbletemplates.pebble.loader.FileLoader; +import io.pebbletemplates.pebble.loader.Loader; +import io.pebbletemplates.pebble.template.EvaluationContext; +import io.pebbletemplates.pebble.template.PebbleTemplate; +import io.swagger.v3.oas.models.media.BooleanSchema; +import io.swagger.v3.oas.models.media.NumberSchema; +import io.swagger.v3.oas.models.media.Schema; + +public class AsciiDocContext { + public static final BiConsumer> NOOP = (name, schema) -> {}; + + private ObjectMapper json; + + private ObjectMapper yamlOpenApi; + + private ObjectMapper yamlOutput; + + private PebbleEngine engine; + + private OpenAPIExt openapi; + + private final AutoDataFakerMapper faker = new AutoDataFakerMapper(); + + private final Map, Map> examples = new HashMap<>(); + + private final Instant now = Instant.now(); + + static { + // type vs types difference in v30 vs v31 + System.setProperty(Schema.BIND_TYPE_AND_TYPES, Boolean.TRUE.toString()); + } + + public AsciiDocContext(Path baseDir, ObjectMapper json, ObjectMapper yaml, OpenAPIExt openapi) { + this.json = json; + this.yamlOpenApi = yaml; + this.yamlOutput = newYamlOutput(); + this.openapi = openapi; + this.engine = createEngine(baseDir, json, this); + } + + public String generate(Path index) throws IOException { + var template = engine.getTemplate(index.getFileName().toString()); + var writer = new StringWriter(); + var context = new HashMap(); + template.evaluate(writer, context); + return writer.toString().trim(); + } + + public void export(Path input, Path outputDir) { + try (var asciidoctor = Asciidoctor.Factory.create()) { + + var options = + Options.builder() + .backend("html5") + .baseDir(input.getParent().toFile()) + .toDir(outputDir.toFile()) + .mkDirs(true) + .safe(SafeMode.UNSAFE) + .build(); + + // Perform the conversion + asciidoctor.convertFile(input.toFile(), options); + } + } + + public Instant getNow() { + return now; + } + + private ObjectMapper newYamlOutput() { + var factory = new YAMLFactory(); + factory.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES); + factory.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER); + return new ObjectMapper(factory); + } + + private static PebbleEngine createEngine( + Path baseDir, ObjectMapper json, AsciiDocContext context) { + List> loaders = + List.of(new FileLoader(baseDir.toAbsolutePath().toString()), new ClasspathLoader()); + return new PebbleEngine.Builder() + .autoEscaping(false) + .loader(new DelegatingLoader(loaders)) + .syntax(new Syntax.Builder().setEnableNewLineTrimming(false).build()) + .extension( + new AbstractExtension() { + @Override + public Map getGlobalVariables() { + Map openapiRoot = json.convertValue(context.openapi, Map.class); + openapiRoot.put("openapi", context.openapi); + openapiRoot.put("now", context.now); + + // Global/Default values: + openapiRoot.put( + "error", + Map.of( + "statusCode", + "{{statusCode.code}}", + "reason", + "{{statusCode.reason}}", + "message", + "...")); + // Routes + var operations = + new HttpRequestList( + context, + Optional.of(context.openapi.getOperations()).orElse(List.of()).stream() + .map(op -> new HttpRequest(context, op, Map.of())) + .toList()); + // so we can print routes without calling function: routes() vs routes + openapiRoot.put("routes", operations); + openapiRoot.put("operations", operations); + + // Tags + var tags = + Optional.ofNullable(context.openapi.getTags()).orElse(List.of()).stream() + .map( + tag -> + new TagExt( + tag, + context.openapi.findOperationByTag(tag.getName()).stream() + .map(op -> new HttpRequest(context, op, Map.of())) + .toList())) + .toList(); + openapiRoot.put("tags", tags); + // Schemas + var components = context.openapi.getComponents(); + if (components != null && components.getSchemas() != null) { + var schemas = components.getSchemas(); + openapiRoot.put("schemas", new ArrayList<>(schemas.values())); + } + + // make in to work without literal + openapiRoot.put("query", "query"); + openapiRoot.put("path", "path"); + openapiRoot.put("header", "header"); + openapiRoot.put("cookie", "cookie"); + + openapiRoot.put("_asciidocContext", context); + return openapiRoot; + } + + @Override + public Map getFunctions() { + return Stream.of(Lookup.values()) + .flatMap(it -> it.alias().stream().map(name -> Map.entry(name, it))) + .collect(Collectors.toMap(Map.Entry::getKey, it -> wrapFn(it.getValue()))); + } + + private static Function wrapFn(Lookup lookup) { + return new Function() { + @Override + public List getArgumentNames() { + return lookup.getArgumentNames(); + } + + @Override + public Object execute( + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) { + try { + return lookup.execute(args, self, context, lineNumber); + } catch (PebbleException rethrow) { + throw rethrow; + } catch (Throwable cause) { + var path = Paths.get(self.getName()); + throw new PebbleException( + cause, + "execution of `" + lookup.name() + "()` resulted in exception:", + lineNumber, + path.getFileName().toString().trim()); + } + } + }; + } + + private static Filter wrapFilter(String filterName, Filter filter) { + return new Filter() { + @Override + public List getArgumentNames() { + return filter.getArgumentNames(); + } + + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + try { + return filter.apply(input, args, self, context, lineNumber); + } catch (PebbleException rethrow) { + throw rethrow; + } catch (Throwable cause) { + var path = Paths.get(self.getName()); + throw new PebbleException( + cause, + "execution of `" + filterName + "()` resulted in exception:", + lineNumber, + path.getFileName().toString().trim()); + } + } + }; + } + + @Override + public Map getFilters() { + return Stream.concat(Stream.of(Mutator.values()), Stream.of(Display.values())) + .collect(Collectors.toMap(Enum::name, it -> wrapFilter(it.name(), it))); + } + }) + .build(); + } + + @SuppressWarnings("unchecked") + public Map error(EvaluationContext context, Map args) { + var error = context.getVariable("error"); + if (error instanceof Map errorMap) { + var mutableMap = new TreeMap((Map) errorMap); + args.forEach( + (key, value) -> { + if (mutableMap.containsKey(key)) { + mutableMap.put(key, value); + } + }); + var statusCode = + StatusCode.valueOf( + ((Number) + args.getOrDefault( + "code", args.getOrDefault("statusCode", StatusCode.SERVER_ERROR.value()))) + .intValue()); + for (var entry : errorMap.entrySet()) { + var value = entry.getValue(); + var template = String.valueOf(value); + if (template.startsWith("{{") && template.endsWith("}}")) { + var variable = template.substring(2, template.length() - 2).trim(); + value = + switch (variable) { + case "status.reason", + "statusCodeReason", + "statusCode.reason", + "code.reason", + "codeReason", + "reason" -> + statusCode.reason(); + case "status.code", "statusCode.code", "statusCode", "code" -> statusCode.value(); + default -> + Optional.ofNullable(args.getOrDefault(variable, context.getVariable(variable))) + .orElse(template); + }; + mutableMap.put((String) entry.getKey(), value); + } + } + return mutableMap; + } + throw new ClassCastException("Global error must be a map: " + error); + } + + public String schemaType(Schema schema) { + var resolved = resolveSchema(schema); + return Optional.ofNullable(resolved.getFormat()).orElse(resolved.getType()); + } + + public Schema resolveSchema(Schema schema) { + if (schema.get$ref() != null) { + return resolveSchemaInternal(schema.get$ref()) + .orElseThrow(() -> new NoSuchElementException("Schema not found: " + schema.get$ref())); + } + return schema; + } + + public Object schemaProperties(Schema schema) { + var resolved = resolveSchema(schema); + if ("array".equals(resolved.getType())) { + var items = resolveSchema(resolved.getItems()); + if (items.getName() == null) { + return List.of(basicTypeSample(items)); + } + return List.of(traverse(resolved.getItems(), NOOP)); + } + if (resolved.getName() == null) { + return basicTypeSample(resolved); + } + return traverse(schema, NOOP); + } + + private Object basicTypeSample(Schema items) { + return switch (items) { + case NumberSchema s -> 0; + case BooleanSchema s -> true; + default -> schemaType(items); + }; + } + + @SuppressWarnings("rawtypes") + public Schema reduceSchema(Schema schema) { + var truncated = emptySchema(schema); + var properties = new LinkedHashMap(); + traverse( + schema, + (name, value) -> { + var type = value.getType(); + if ("object".equals(type)) { + var object = new Schema<>(); + object.setType(type); + properties.put(name, object); + } else if ("array".equals(type)) { + var array = new Schema<>(); + array.setType(type); + array.setItems(new Schema<>()); + properties.put(name, array); + } else { + properties.put(name, value); + } + }); + truncated.setProperties(properties); + return truncated; + } + + public Schema emptySchema(Schema schema) { + var resolved = resolveSchema(schema); + var empty = new Schema<>(); + empty.setType(resolved.getType()); + empty.setName(resolved.getName()); + empty.setTypes(resolved.getTypes()); + return empty; + } + + public Object schemaExample(Schema schema) { + var resolved = resolveSchema(schema); + var target = resolved; + if ("array".equals(resolved.getType())) { + target = resolveSchema(resolved.getItems()); + } + var result = + examples.computeIfAbsent( + target, + key -> + traverse( + new HashSet<>(), + key, + (parent, property) -> { + var enumItems = property.getEnum(); + if (enumItems == null || enumItems.isEmpty()) { + var type = schemaType(property); + var gen = + faker.getGenerator(parent.getName(), property.getName(), type, type); + return gen.get(); + } else { + return enumItems.get(new Random().nextInt(enumItems.size())).toString(); + } + }, + NOOP)); + return "array".equals(resolved.getType()) ? List.of(result) : result; + } + + public void traverseSchema(Schema schema, BiConsumer> consumer) { + traverse(schema, consumer); + } + + private Map traverse(Schema schema, BiConsumer> consumer) { + return traverse(new HashSet<>(), schema, (parent, property) -> schemaType(property), consumer); + } + + private Map traverse( + Set visited, + Schema schema, + SneakyThrows.Function2, Schema, String> valueMapper, + BiConsumer> consumer) { + if (schema == null) { + return Map.of(); + } + var resolved = resolveSchema(schema); + if (visited.add(resolved)) { + var properties = resolved.getProperties(); + if (properties != null) { + Map result = new LinkedHashMap<>(); + properties.forEach( + (name, value) -> { + var resolvedValue = resolveSchema(value); + var valueType = resolvedValue.getType(); + consumer.accept(name, resolvedValue); + if ("object".equals(valueType)) { + result.put(name, traverse(visited, resolvedValue, valueMapper, NOOP)); + } else if ("array".equals(valueType)) { + var array = + ofNullable(resolvedValue.getItems()) + .map(items -> traverse(visited, resolveSchema(items), valueMapper, NOOP)) + .map(List::of) + .orElse(List.of()); + result.put(name, array); + } else { + result.put(name, valueMapper.apply(resolved, resolvedValue)); + } + }); + return result; + } + } + return Map.of(); + } + + public Schema resolveSchema(String path) { + var segments = path.split("\\."); + var schema = + resolveSchemaInternal(segments[0]) + .orElseThrow(() -> new NoSuchElementException("Schema not found: " + path)); + + for (int i = 1; i < segments.length; i++) { + Schema inner = (Schema) schema.getProperties().get(segments[i]); + if (inner == null) { + throw new IllegalArgumentException( + "Property not found: " + Stream.of(segments).limit(i).collect(Collectors.joining("."))); + } + if (inner.get$ref() != null) { + inner = + resolveSchemaInternal(inner.get$ref()) + .orElseThrow(() -> new NoSuchElementException("Schema not found: " + path)); + } + schema = inner; + } + return schema; + } + + private Optional> resolveSchemaInternal(String name) { + var components = openapi.getComponents(); + if (components == null || components.getSchemas() == null) { + throw new NoSuchElementException("No schema found"); + } + if (name.startsWith(COMPONENTS_SCHEMAS_REF)) { + name = name.substring(COMPONENTS_SCHEMAS_REF.length()); + } + return Optional.ofNullable((Schema) components.getSchemas().get(name)); + } + + public PebbleEngine getEngine() { + return engine; + } + + public String toJson(Object input, boolean pretty) { + try { + var writer = pretty ? json.writer().withDefaultPrettyPrinter() : json.writer(); + return writer.writeValueAsString(input); + } catch (JsonProcessingException e) { + throw SneakyThrows.propagate(e); + } + } + + public String toYaml(Object input) { + try { + return cleanYaml( + input instanceof Map + ? yamlOutput.writeValueAsString(input) + : yamlOpenApi.writeValueAsString(input)); + } catch (JsonProcessingException e) { + throw SneakyThrows.propagate(e); + } + } + + private String cleanYaml(String value) { + return value.trim(); + } + + public ObjectMapper getJson() { + return json; + } + + public ObjectMapper getYaml() { + return yamlOpenApi; + } + + public OpenAPIExt getOpenApi() { + return openapi; + } + + public static AsciiDocContext from(EvaluationContext context) { + return (AsciiDocContext) context.getVariable("_asciidocContext"); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AutoDataFakerMapper.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AutoDataFakerMapper.java new file mode 100644 index 0000000000..db347b2ad0 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AutoDataFakerMapper.java @@ -0,0 +1,340 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.function.Supplier; + +import net.datafaker.Faker; +import net.datafaker.providers.base.AbstractProvider; + +public class AutoDataFakerMapper { + + private final Faker faker; + + // --- REGISTRIES (Functional) --- + private final Map> specificRegistry = new HashMap<>(); + private final Map> genericRegistry = new HashMap<>(); + private final Map> typeRegistry = new HashMap<>(); + + // --- SYNONYMS --- + private final Map synonymMap; + private static final Map DEFAULT_SYNONYMS = new HashMap<>(); + + static { + registerDefault("surname", "lastname"); + registerDefault("familyname", "lastname"); + registerDefault("login", "username"); + registerDefault("user", "username"); + registerDefault("fullname", "name"); + registerDefault("displayname", "name"); + registerDefault("social", "ssnvalid"); + registerDefault("ssn", "ssnvalid"); + registerDefault("mail", "emailaddress"); + registerDefault("email", "emailaddress"); + registerDefault("subject", "emailsubject"); + registerDefault("web", "url"); + registerDefault("homepage", "url"); + registerDefault("link", "url"); + registerDefault("uri", "url"); + registerDefault("avatar", "image"); + registerDefault("pic", "image"); + registerDefault("pwd", "password"); + registerDefault("pass", "password"); + registerDefault("cell", "cellphone"); + registerDefault("mobile", "cellphone"); + registerDefault("tel", "phonenumber"); + registerDefault("fax", "phonenumber"); + registerDefault("addr", "fulladdress"); + registerDefault("street", "streetaddress"); + registerDefault("postcode", "zipcode"); + registerDefault("postal", "zipcode"); + registerDefault("zip", "zipcode"); + registerDefault("town", "city"); + registerDefault("province", "state"); + registerDefault("region", "state"); + registerDefault("lat", "latitude"); + registerDefault("lon", "longitude"); + registerDefault("lng", "longitude"); + registerDefault("qty", "quantity"); + registerDefault("cost", "price"); + registerDefault("amount", "price"); + registerDefault("desc", "sentence"); + registerDefault("description", "paragraph"); + registerDefault("dept", "industry"); + registerDefault("role", "title"); + registerDefault("position", "title"); + registerDefault("dob", "birthday"); + registerDefault("born", "birthday"); + registerDefault("created", "date"); + registerDefault("modified", "date"); + registerDefault("timestamp", "date"); + registerDefault("guid", "uuid"); + } + + private static void registerDefault(String key, String value) { + DEFAULT_SYNONYMS.put(key, value); + } + + // --- CONSTRUCTORS --- + + public AutoDataFakerMapper() { + this.faker = new Faker(); + this.synonymMap = new HashMap<>(DEFAULT_SYNONYMS); + + initializeReflectionRegistry(); + initializeTypeRegistry(); + } + + public void synonyms(Map synonyms) { + synonyms.forEach((k, v) -> this.synonymMap.put(normalize(k), normalize(v))); + } + + private void initializeReflectionRegistry() { + Arrays.stream(Faker.class.getMethods()) + .filter(this::isProviderMethod) + .forEach(this::registerProvider); + } + + private void registerType(String type, Supplier supplier, String description) { + String cleanType = normalize(type); + typeRegistry.put(cleanType, fakeSupplier(supplier, description)); + } + + private static Supplier fakeSupplier(Supplier supplier, String signature) { + return new Supplier<>() { + @Override + public String get() { + return supplier.get(); + } + + @Override + public String toString() { + return signature; + } + }; + } + + private void initializeTypeRegistry() { + // domains + specificRegistry.put( + "book.isbn", fakeSupplier(() -> faker.code().isbn13(), "faker.code().isbn13()")); + + // We now register the Description alongside the Supplier + registerType("uuid", () -> faker.internet().uuid(), "faker.internet().uuid()"); + registerType("email", () -> faker.internet().emailAddress(), "faker.internet().emailAddress()"); + registerType( + "password", () -> faker.credentials().password(), "faker.credentials().password()"); + registerType("ipv4", () -> faker.internet().ipV4Address(), "faker.internet().ipV4Address()"); + registerType("ipv6", () -> faker.internet().ipV6Address(), "faker.internet().ipV6Address()"); + registerType("uri", () -> faker.internet().url(), "faker.internet().url()"); + registerType("url", () -> faker.internet().url(), "faker.internet().url()"); + registerType("hostname", () -> faker.internet().domainName(), "faker.internet().domainName()"); + + registerType( + "date", () -> faker.timeAndDate().birthday().toString(), "faker.timeAndDate().birthday()"); + registerType( + "datetime", + () -> faker.timeAndDate().past(365, java.util.concurrent.TimeUnit.DAYS).toString(), + "faker.timeAndDate().past()"); + registerType( + "time", + () -> faker.timeAndDate().birthday().toString().split(" ")[1], + "faker.timeAndDate().birthday() (time-part)"); + + registerType( + "integer", + () -> String.valueOf(faker.number().numberBetween(1, 100)), + "faker.number().numberBetween(1, 100)"); + registerType( + "int32", + () -> String.valueOf(faker.number().numberBetween(1, 100)), + "faker.number().numberBetween(1, 100)"); + registerType( + "int64", + () -> String.valueOf(faker.number().numberBetween(1, 100)), + "faker.number().numberBetween(1, 100)"); + registerType( + "float", + () -> String.valueOf(faker.number().randomDouble(2, 0, 100)), + "faker.number().randomDouble()"); + registerType( + "double", + () -> String.valueOf(faker.number().randomDouble(4, 0, 100)), + "faker.number().randomDouble()"); + registerType( + "number", + () -> String.valueOf(faker.number().numberBetween(0, 100)), + "faker.number().numberBetween(1, 100)"); + + registerType("boolean", () -> String.valueOf(faker.bool().bool()), "faker.bool().bool()"); + + registerType("string", () -> "string", "string"); + } + + private void registerProvider(Method providerMethod) { + try { + Object providerInstance = providerMethod.invoke(faker); + String domainName = normalize(providerMethod.getName()); + + Arrays.stream(providerInstance.getClass().getMethods()) + .filter(this::isValidGeneratorMethod) + .forEach(method -> registerMethod(domainName, providerInstance, method)); + + } catch (Exception ignored) { + } + } + + private void registerMethod(String domainName, Object providerInstance, Method method) { + String fieldName = normalize(method.getName()); + String signature = "faker.%s().%s()".formatted(domainName, method.getName()); + + Supplier rawGenerator = fakerSupplier(providerInstance, method, signature); + + // 1. Specific Registry + specificRegistry.put(domainName + "." + fieldName, rawGenerator); + + // add base generic only + if (method.getDeclaringClass().getPackage().equals(AbstractProvider.class.getPackage())) { + // 2. Generic Registry (First one wins) + genericRegistry.putIfAbsent(fieldName, rawGenerator); + } + } + + // --- CORE LOGIC (Unchanged) --- + public Supplier getGenerator( + String className, String fieldName, String fieldType, String defaultValue) { + var cleanClass = normalize(className); + var cleanField = normalize(fieldName); + var cleanType = normalize(fieldType); + + String resolvedField = synonymMap.getOrDefault(cleanField, cleanField); + + var specific = specificRegistry.get(cleanClass + "." + resolvedField); + if (specific != null) return wrap(specific, defaultValue); + + var generic = genericRegistry.get(resolvedField); + if (generic != null) return wrap(generic, defaultValue); + + var fuzzy = + genericRegistry.entrySet().stream() + .filter(entry -> resolvedField.contains(entry.getKey()) && entry.getKey().length() > 3) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + + if (fuzzy != null) return wrap(fuzzy, defaultValue); + + if (!cleanType.isEmpty()) { + var typeGen = typeRegistry.get(cleanType); + if (typeGen != null) return wrap(typeGen, defaultValue); + } + + return () -> defaultValue; + } + + // --- CAPABILITY MAP (IMPROVED) --- + + /** Returns a structured map of all available capabilities with Source details. */ + public Map getCapabilityMap() { + // 1. Domains Tree: "book" -> { "title": "faker.book().title()" } + Map> domainsTree = new TreeMap<>(); + specificRegistry.forEach( + (key, signature) -> { + int dotIndex = key.indexOf('.'); + if (dotIndex > 0) { + String domain = key.substring(0, dotIndex); + String field = key.substring(dotIndex + 1); + domainsTree + .computeIfAbsent(domain, k -> new TreeMap<>()) + .put(field, signature.toString()); + } + }); + + // 2. Generics Map: "title" -> "faker.book().title()" + // (Using TreeMap for sorting) + Map genericsMap = new TreeMap<>(); + genericRegistry.forEach( + (key, signature) -> { + genericsMap.put(key, signature.toString()); + }); + + // 3. Types Map: "uuid" -> "faker.internet().uuid()" + Map typesMap = new TreeMap<>(); + typeRegistry.forEach( + (key, signature) -> { + typesMap.put(key, signature.toString()); + }); + + // 4. Synonyms Copy + Map synonymsCopy = new TreeMap<>(synonymMap); + + return Map.of( + "domains", domainsTree, + "generics", genericsMap, + "types", typesMap, + "synonyms", synonymsCopy); + } + + private static Supplier fakerSupplier(Object instance, Method method, String signature) { + return new Supplier<>() { + @Override + public String get() { + try { + return (String) method.invoke(instance); + } catch (Exception ignored) { + return null; + } + } + + @Override + public String toString() { + return signature; + } + }; + } + + private static Supplier wrap(Supplier supplier, String defaultValue) { + return new Supplier<>() { + @Override + public String get() { + try { + String res = supplier.get(); + return res != null ? res : defaultValue; + } catch (Exception e) { + return defaultValue; + } + } + + @Override + public String toString() { + return supplier.toString(); + } + }; + } + + private boolean isProviderMethod(Method m) { + return m.getParameterCount() == 0 && AbstractProvider.class.isAssignableFrom(m.getReturnType()); + } + + private boolean isValidGeneratorMethod(Method m) { + return Modifier.isPublic(m.getModifiers()) + && m.getParameterCount() == 0 + && m.getReturnType().equals(String.class) + && !isStandardMethod(m.getName()); + } + + private boolean isStandardMethod(String name) { + return "toString".equals(name); + } + + private String normalize(String input) { + if (input == null || input.isBlank()) return ""; + return input.toLowerCase().trim().replaceAll("[^a-z0-9]", ""); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Display.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Display.java new file mode 100644 index 0000000000..d47f7c0e7e --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Display.java @@ -0,0 +1,230 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.*; + +import io.jooby.internal.openapi.OperationExt; +import io.jooby.internal.openapi.asciidoc.display.*; +import io.pebbletemplates.pebble.error.PebbleException; +import io.pebbletemplates.pebble.extension.Filter; +import io.pebbletemplates.pebble.extension.escaper.SafeString; +import io.pebbletemplates.pebble.template.EvaluationContext; +import io.pebbletemplates.pebble.template.PebbleTemplate; +import io.swagger.v3.oas.models.media.Schema; + +public enum Display implements Filter { + json { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + var asciidoc = AsciiDocContext.from(context); + var pretty = args.getOrDefault("pretty", true) == Boolean.TRUE; + return wrap( + asciidoc.toJson(toJson(asciidoc, input), pretty), + args.getOrDefault("wrap", Boolean.TRUE) == Boolean.TRUE, + "[source, json]\n----\n", + "\n----"); + } + + @Override + public List getArgumentNames() { + return List.of("wrap"); + } + }, + yaml { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + var asciidoc = AsciiDocContext.from(context); + return wrap( + asciidoc.toYaml(toJson(asciidoc, input)), + args.getOrDefault("wrap", Boolean.TRUE) == Boolean.TRUE, + "[source, yaml]\n----\n", + "\n----"); + } + + @Override + public List getArgumentNames() { + return List.of("wrap"); + } + }, + table { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + var asciidoc = AsciiDocContext.from(context); + return new SafeString(toAsciidoc(asciidoc, input).table(new TreeMap<>(args))); + } + }, + list { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + var asciidoc = AsciiDocContext.from(context); + return new SafeString(toAsciidoc(asciidoc, input).list(new TreeMap<>(args))); + } + }, + link { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + var schema = + switch (input) { + case Schema s -> s; + case HttpMessage msg -> msg.getBody(); + default -> throw new IllegalArgumentException("Can't render: " + input); + }; + var asciidoc = AsciiDocContext.from(context); + var resolved = asciidoc.resolveSchema(schema); + if (resolved.getItems() == null) { + if (resolved.getName() == null) { + return resolved.getType(); + } + return new SafeString("<<" + resolved.getName() + ">>"); + } else { + var item = asciidoc.resolveSchema(resolved.getItems()); + if (item.getName() == null) { + // primitives + return new SafeString(item.getType() + "[]"); + } else { + if ("array".equals(resolved.getType())) { + return new SafeString("<<" + item.getName() + ">>[]"); + } + return new SafeString(resolved.getName() + "[<<" + item.getName() + ">>]"); + } + } + } + }, + curl { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + var asciidoc = AsciiDocContext.from(context); + var curl = + switch (input) { + case OperationExt op -> + new RequestToCurl(asciidoc, new HttpRequest(asciidoc, op, args)); + case HttpRequest req -> new RequestToCurl(asciidoc, req); + default -> throw new IllegalArgumentException("Can't render: " + input); + }; + return curl.render(args); + } + }, + path { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + var asciidoc = AsciiDocContext.from(context); + var request = + switch (input) { + case OperationExt op -> new HttpRequest(asciidoc, op, args); + case HttpRequest req -> req; + default -> throw new IllegalArgumentException("Can't render: " + input); + }; + var pathParams = new HashMap(); + request + .getParameters(List.of("path"), List.of()) + .forEach( + p -> { + pathParams.put( + p.getName(), args.getOrDefault(p.getName(), "{" + p.getName() + "}")); + }); + // QueryString + pathParams.keySet().forEach(args::remove); + var queryString = request.getQueryString(args); + return request.operation().getPath(pathParams) + queryString; + } + }, + http { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + var asciidoc = AsciiDocContext.from(context); + return toHttp(asciidoc, input, args).render(args); + } + + private ToSnippet toHttp(AsciiDocContext context, Object input, Map options) { + return switch (input) { + case OperationExt op -> new RequestToHttp(context, new HttpRequest(context, op, options)); + case HttpRequest req -> new RequestToHttp(context, req); + case HttpResponse rsp -> new ResponseToHttp(context, rsp); + default -> throw new IllegalArgumentException("Can't render: " + input); + }; + } + }; + + protected ToAsciiDoc toAsciidoc(AsciiDocContext context, Object input) { + return switch (input) { + case HttpRequest req -> OpenApiToAsciiDoc.parameters(context, req.getAllParameters()); + case HttpResponse rsp -> OpenApiToAsciiDoc.schema(context, rsp.getBody()); + case Schema schema -> OpenApiToAsciiDoc.schema(context, schema); + case ParameterList paramList -> OpenApiToAsciiDoc.parameters(context, paramList); + case ToAsciiDoc asciiDoc -> asciiDoc; + case Map map -> new MapToAsciiDoc(List.of(map)); + default -> throw new IllegalArgumentException("Can't render: " + input); + }; + } + + protected Object toJson(AsciiDocContext context, Object input) { + return switch (input) { + case Schema schema -> context.schemaProperties(schema); + case HttpResponse rsp -> toJson(context, rsp.getSucessOrError()); + case StatusCodeList codeList -> + codeList.codes().size() == 1 ? codeList.codes().getFirst() : codeList.codes(); + default -> input; + }; + } + + protected SafeString wrap(String content, boolean wrap, String prefix, String suffix) { + return new SafeString(wrap ? prefix + content + suffix : content); + } + + @Override + public List getArgumentNames() { + return List.of(); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpMessage.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpMessage.java new file mode 100644 index 0000000000..3fa39d3c08 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpMessage.java @@ -0,0 +1,50 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.List; + +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.Schema; + +public interface HttpMessage { + + ParameterList getHeaders(); + + ParameterList getCookies(); + + Schema getBody(); + + AsciiDocContext context(); + + default Schema selectBody(Schema body, String modifier) { + if (body != null) { + return switch (modifier) { + case "full" -> body; + case "simple" -> context().reduceSchema(body); + default -> context().emptySchema(body); + }; + } + return body; + } + + default Schema toSchema(Content content, List contentType) { + if (content == null || content.isEmpty()) { + return null; + } + if (contentType.isEmpty()) { + // first response + return content.values().iterator().next().getSchema(); + } + for (var key : contentType) { + var mediaType = content.get(key); + if (mediaType != null) { + return mediaType.getSchema(); + } + } + return null; + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpRequest.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpRequest.java new file mode 100644 index 0000000000..ac2ce2b8fd --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpRequest.java @@ -0,0 +1,283 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Predicate; + +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.net.UrlEscapers; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Router; +import io.jooby.internal.openapi.OperationExt; +import io.jooby.internal.openapi.ParameterExt; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.security.SecurityRequirement; + +@JsonIncludeProperties({"path", "method"}) +public record HttpRequest( + AsciiDocContext context, OperationExt operation, Map options) + implements HttpMessage { + + private static final Predicate NOOP = p -> true; + + private List allParameters() { + var parameters = new ArrayList<>(getImplicitHeaders()); + parameters.addAll(Optional.ofNullable(operation.getParameters()).orElse(List.of())); + return parameters; + } + + private List getImplicitHeaders() { + var implicitHeaders = new ArrayList(); + operation + .getProduces() + .forEach(value -> implicitHeaders.add(ParameterExt.header("Accept", value))); + if (Set.of(Router.PATCH, Router.PUT, Router.POST, Router.DELETE) + .contains(operation.getMethod())) { + operation + .getConsumes() + .forEach(value -> implicitHeaders.add(ParameterExt.header("Content-Type", value))); + } + return implicitHeaders; + } + + public String getMethod() { + return operation.getMethod(); + } + + public String getPath() { + return operation.getPath(); + } + + public String getDescription() { + return operation.getDescription(); + } + + public String getSummary() { + return operation.getSummary(); + } + + public List getProduces() { + return operation.getProduces(); + } + + public List getConsumes() { + return operation.getConsumes(); + } + + public Map getExtensions() { + return operation.getExtensions(); + } + + @Override + public ParameterList getHeaders() { + return new ParameterList( + allParameters().stream().filter(inFilter("header")).toList(), ParameterList.NAME_DESC); + } + + @Override + public ParameterList getCookies() { + return new ParameterList( + allParameters().stream().filter(inFilter("cookie")).toList(), ParameterList.NAME_DESC); + } + + public ParameterList getQuery() { + return new ParameterList( + allParameters().stream().filter(inFilter("query")).toList(), ParameterList.NAME_TYPE_DESC); + } + + public ParameterList getParameters() { + return getParameterList(NOOP, ParameterList.PARAM); + } + + public ParameterList getParameters(List in, List includes) { + var show = + in.isEmpty() || in.contains("*") + ? ParameterList.PARAM + : (in.size() == 1 && in.contains("cookie") || in.contains("header")) + ? ParameterList.NAME_DESC + : ParameterList.NAME_TYPE_DESC; + return getParameterList(toFilter(in, includes), show); + } + + private Predicate toFilter(List in, List includes) { + Predicate inFilter; + if (in.isEmpty()) { + inFilter = NOOP; + } else { + inFilter = null; + for (var type : in) { + var itFilter = inFilter(type); + if (inFilter == null) { + inFilter = itFilter; + } else { + inFilter = inFilter.or(itFilter); + } + } + } + Predicate paramFilter = NOOP; + if (!includes.isEmpty()) { + paramFilter = p -> includes.contains(p.getName()); + } + return inFilter.and(paramFilter); + } + + public String getQueryString() { + return getQueryString(Map.of()); + } + + public String getQueryString(Map filter) { + var sb = new StringBuilder("?"); + + for (var param : getParameters(List.of("query"), filter.keySet().stream().toList())) { + encode( + param.getName(), + param.getSchema(), + (schema, e) -> + Map.entry( + e.getKey(), + UrlEscapers.urlFragmentEscaper() + .escape(filter.getOrDefault(e.getKey(), e.getValue()).toString())), + (name, value) -> sb.append(name).append("=").append(value).append("&")); + } + if (sb.length() > 1) { + sb.setLength(sb.length() - 1); + return sb.toString(); + } + return ""; + } + + private Schema getBody(List contentType) { + var body = + Optional.ofNullable(operation.getRequestBody()) + .map(it -> toSchema(it.getContent(), contentType)) + .map(context::resolveSchema) + .orElse(null); + + return selectBody(body, options.getOrDefault("body", "full").toString()); + } + + public Schema getForm() { + return getBody(List.of("application/x-www-form-urlencoded)", "multipart/form-data")); + } + + @NonNull public ListMultimap formUrlEncoded( + BiFunction, Map.Entry, Map.Entry> formatter) { + var output = ArrayListMultimap.create(); + var form = getForm(); + if (form != null) { + traverseSchema(null, form, formatter, output::put); + } + return output; + } + + private void traverseSchema( + String path, + Schema schema, + BiFunction, Map.Entry, Map.Entry> formatter, + BiConsumer consumer) { + context.traverseSchema( + schema, + (propertyName, value) -> { + var propertyPath = path == null ? propertyName : path + "." + propertyName; + if (value.getType().equals("object")) { + traverseSchema(propertyPath, value, formatter, consumer); + } else if (value.getType().equals("array")) { + traverseSchema(propertyPath + "[0]", value.getItems(), formatter, consumer); + } else { + encode(propertyPath, value, formatter, consumer); + } + }); + } + + private void encode( + String propertyName, + Schema schema, + BiFunction, Map.Entry, Map.Entry> formatter, + BiConsumer consumer) { + var names = List.of(propertyName); + var index = new AtomicInteger(0); + if (schema.getType().equals("array")) { + schema = schema.getItems(); + // shows 3 examples + names = List.of(propertyName, propertyName, propertyName); + index.set(1); + } + var schemaType = context.schemaType(schema); + if ("binary".equals(schema.getFormat())) { + schemaType = "file"; + } + var value = schemaType + "%1$s"; + for (String name : names) { + var formattedPair = + formatter.apply( + schema, + Map.entry( + name, String.format(value, (index.get() == 0 ? "" : index.getAndIncrement())))); + consumer.accept(formattedPair.getKey(), formattedPair.getValue()); + } + } + + public boolean isDeprecated() { + return operation.getDeprecated() == Boolean.TRUE; + } + + public List getSecurity() { + return operation.getSecurity(); + } + + @Override + public Schema getBody() { + return getBody(List.of()); + } + + public ParameterList getAllParameters() { + var parameters = allParameters(); + var body = getForm(); + var bodyType = "form"; + if (body == null) { + body = getBody(); + bodyType = "body"; + } + var paramType = bodyType; + context.traverseSchema( + body, + (propertyName, schema) -> { + var p = new Parameter(); + p.setName(propertyName); + p.setSchema(schema); + p.setIn(paramType); + p.setDescription(schema.getDescription()); + parameters.add(p); + }); + return new ParameterList(parameters, ParameterList.PARAM); + } + + private ParameterList getParameterList(Predicate predicate, List includes) { + return new ParameterList(getParameters(predicate), includes); + } + + private List getParameters(Predicate predicate) { + return predicate == NOOP + ? allParameters() + : allParameters().stream().filter(predicate).toList(); + } + + private static Predicate inFilter(String in) { + return p -> "*".equals(in) || in.equals(p.getIn()); + } + + @NonNull @Override + public String toString() { + return getMethod() + " " + getPath(); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpRequestList.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpRequestList.java new file mode 100644 index 0000000000..565e82a634 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpRequestList.java @@ -0,0 +1,79 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import edu.umd.cs.findbugs.annotations.NonNull; + +@JsonIncludeProperties({"operations"}) +public record HttpRequestList(AsciiDocContext context, List operations) + implements Iterable, ToAsciiDoc { + @NonNull @Override + public Iterator iterator() { + return operations.iterator(); + } + + @NonNull @Override + public String toString() { + return operations.toString(); + } + + @Override + public String list(Map options) { + var sb = new StringBuilder(); + operations.forEach( + op -> + sb.append("* `+") + .append(op) + .append("+`") + .append( + Optional.ofNullable(op.getSummary()).map(summary -> ": " + summary).orElse("")) + .append('\n')); + if (!sb.isEmpty()) { + sb.setLength(sb.length() - 1); + } + return sb.toString(); + } + + @Override + public String table(Map options) { + var sb = new StringBuilder(); + if (options.isEmpty()) { + options.put("options", "header"); + } + options.putIfAbsent("cols", "1,1,3a"); + sb.append( + options.entrySet().stream() + .map(it -> it.getKey() + "=\"" + it.getValue() + "\"") + .collect(Collectors.joining(", ", "[", "]"))) + .append('\n'); + sb.append("|===").append('\n'); + sb.append("|").append("Method|Path|Summary").append("\n\n"); + operations.forEach( + op -> + sb.append("|`+") + .append(op.getMethod()) + .append("+`\n") + .append("|`+") + .append(op.getPath()) + .append("+`\n") + .append("|") + .append(Optional.ofNullable(op.operation().getSummary()).orElse("")) + .append("\n\n")); + if (!sb.isEmpty()) { + sb.append("\n"); + sb.setLength(sb.length() - 1); + } + sb.append("|==="); + return sb.toString(); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpResponse.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpResponse.java new file mode 100644 index 0000000000..097cd2fb9b --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/HttpResponse.java @@ -0,0 +1,118 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.StatusCode; +import io.jooby.internal.openapi.OperationExt; +import io.jooby.internal.openapi.ParameterExt; +import io.jooby.internal.openapi.ResponseExt; +import io.pebbletemplates.pebble.template.EvaluationContext; +import io.swagger.v3.oas.models.media.Schema; + +@JsonIncludeProperties({"method", "path"}) +public record HttpResponse( + EvaluationContext evaluationContext, + OperationExt operation, + Integer statusCode, + Map options) + implements HttpMessage { + @Override + public ParameterList getHeaders() { + return new ParameterList( + operation.getProduces().stream() + .map(value -> ParameterExt.header("Content-Type", value)) + .toList(), + ParameterList.NAME_DESC); + } + + @Override + public ParameterList getCookies() { + return new ParameterList(List.of(), ParameterList.NAME_DESC); + } + + @Override + public AsciiDocContext context() { + return AsciiDocContext.from(evaluationContext); + } + + public String getMethod() { + return operation.getMethod(); + } + + public String getPath() { + return operation.getPath(); + } + + @Override + public Schema getBody() { + return selectBody(getBody(response()), options.getOrDefault("body", "full").toString()); + } + + public boolean isSuccess() { + return statusCode != null && statusCode >= 200 && statusCode < 300; + } + + public Object getSucessOrError() { + var response = response(); + if (response == operation.getDefaultResponse()) { + return getBody(); + } + // massage error apply global error format + var rsp = operation.getResponses().get(Integer.toString(statusCode)); + + if (rsp == null) { + // default output + return context().error(evaluationContext, Map.of("code", statusCode)); + } + var errorContext = new LinkedHashMap(); + errorContext.put("code", statusCode); + errorContext.put("message", rsp.getDescription()); + return context().error(evaluationContext, errorContext); + } + + private ResponseExt response() { + if (statusCode == null) { + return operation.getDefaultResponse(); + } else { + var rsp = operation.getResponses().get(Integer.toString(statusCode)); + if (rsp == null) { + if (statusCode >= 200 && statusCode <= 299) { + // override default response + return operation.getDefaultResponse(); + } + } + return (ResponseExt) rsp; + } + } + + public StatusCode getStatusCode() { + if (statusCode == null) { + return Optional.ofNullable(response()) + .map(it -> StatusCode.valueOf(Integer.parseInt(it.getCode()))) + .orElse(StatusCode.OK); + } + return StatusCode.valueOf(statusCode); + } + + private Schema getBody(ResponseExt response) { + return Optional.ofNullable(response) + .map(it -> toSchema(it.getContent(), List.of())) + .map(context()::resolveSchema) + .orElse(null); + } + + @NonNull @Override + public String toString() { + return operation.getMethod() + " " + operation.getPath(); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Lookup.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Lookup.java new file mode 100644 index 0000000000..46feb364f6 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Lookup.java @@ -0,0 +1,267 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.*; +import java.util.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.StatusCode; +import io.pebbletemplates.pebble.extension.Function; +import io.pebbletemplates.pebble.template.EvaluationContext; +import io.pebbletemplates.pebble.template.PebbleTemplate; + +/** + * GET("path") | table GET("path") | parameters | table + * + *

schema("Book") | json schema("Book.type") | yaml + * + *

GET("path") | response | json + * + *

GET("path") | response(200) | json + * + *

GET("path") | request | json + * + *

GET("path") | request | http + * + *

GET("path") | request | body | http + */ +public enum Lookup implements Function { + operation { + @Override + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + var method = args.get("method").toString(); + var path = args.get("path").toString(); + var asciidoc = AsciiDocContext.from(context); + return asciidoc.getOpenApi().findOperation(method, path); + } + + @Override + public List getArgumentNames() { + return List.of("method", "path"); + } + }, + GET { + @Override + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + return operation.execute(appendMethod(args), self, context, lineNumber); + } + + @Override + public List getArgumentNames() { + return List.of("path"); + } + }, + POST { + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + return operation.execute(appendMethod(args), self, context, lineNumber); + } + + @Override + public List getArgumentNames() { + return List.of("path"); + } + }, + PUT { + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + return operation.execute(appendMethod(args), self, context, lineNumber); + } + + @Override + public List getArgumentNames() { + return List.of("path"); + } + }, + PATCH { + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + return operation.execute(appendMethod(args), self, context, lineNumber); + } + + @Override + public List getArgumentNames() { + return List.of("path"); + } + }, + DELETE { + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + return operation.execute(appendMethod(args), self, context, lineNumber); + } + + @Override + public List getArgumentNames() { + return List.of("path"); + } + }, + schema { + @Override + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + var path = args.get("path").toString(); + var asciidoc = AsciiDocContext.from(context); + return asciidoc.resolveSchema(path); + } + + @Override + public List getArgumentNames() { + return List.of("path"); + } + + @Override + public List alias() { + return List.of("schema", "model"); + } + }, + tag { + @Override + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + var asciidoc = AsciiDocContext.from(context); + var name = args.get("name").toString(); + return asciidoc.getOpenApi().getTags().stream() + .filter(tag -> tag.getName().equalsIgnoreCase(name)) + .findFirst() + .map( + it -> + new TagExt( + it, + asciidoc.getOpenApi().findOperationByTag(it.getName()).stream() + .map(op -> new HttpRequest(asciidoc, op, Map.of())) + .toList())) + .orElseThrow(() -> new NoSuchElementException("Tag not found: " + name)); + } + + @Override + public List getArgumentNames() { + return List.of("name"); + } + }, + error { + @Override + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + var asciidoc = AsciiDocContext.from(context); + return asciidoc.error(context, args); + } + + @Override + public List getArgumentNames() { + return List.of("code"); + } + }, + routes { + @Override + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + var asciidoc = AsciiDocContext.from(context); + var operations = asciidoc.getOpenApi().getOperations(); + var list = + operations.stream() + .filter( + it -> { + var includes = (String) args.get("includes"); + return includes == null || it.getPath().matches(includes); + }) + .map(it -> new HttpRequest(asciidoc, it, args)) + .toList(); + return new HttpRequestList(asciidoc, list); + } + + @Override + public List getArgumentNames() { + return List.of("includes"); + } + + @Override + public List alias() { + return List.of("routes", "operations"); + } + }, + statusCode { + @Override + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + var code = args.get("code"); + if (code instanceof List codes) { + return new StatusCodeList(codes.stream().flatMap(this::toMap).toList()); + } + return new StatusCodeList(toMap(code).toList()); + } + + @NonNull private Stream> toMap(Object candidate) { + if (candidate instanceof Number code) { + Map map = new LinkedHashMap<>(); + map.put("code", code.intValue()); + map.put("reason", StatusCode.valueOf(code.intValue()).reason()); + return Stream.of(map); + } else if (candidate instanceof Map codeMap) { + var codes = new ArrayList>(); + for (var entry : new TreeMap<>(codeMap).entrySet()) { + var value = entry.getKey(); + if (value instanceof Number code) { + Map map = new LinkedHashMap<>(); + map.put("code", code.intValue()); + map.put("reason", entry.getValue()); + codes.add(map); + } else { + throw new ClassCastException("Must be Map: " + candidate); + } + } + return codes.stream(); + } + throw new ClassCastException("Not a number: " + candidate); + } + + @Override + public List getArgumentNames() { + return List.of("code"); + } + }, + server { + @Override + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + var asciidoc = AsciiDocContext.from(context); + var servers = asciidoc.getOpenApi().getServers(); + if (servers == null || servers.isEmpty()) { + throw new NoSuchElementException("No servers"); + } + var nameOrIndex = args.get("name"); + if (nameOrIndex instanceof Number index) { + if (index.intValue() >= 0 && index.intValue() < servers.size()) { + return servers.get(index.intValue()); + } else { + throw new NoSuchElementException("Server not found: [" + nameOrIndex + "]"); + } + } else { + return servers.stream() + .filter(it -> nameOrIndex.equals(it.getDescription())) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("Server not found: " + nameOrIndex)); + } + } + + @Override + public List getArgumentNames() { + return List.of("name"); + } + }; + + public List alias() { + return List.of(name()); + } + + protected Map appendMethod(Map args) { + Map result = new LinkedHashMap<>(args); + result.put("method", name()); + return result; + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Mutator.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Mutator.java new file mode 100644 index 0000000000..3d85d4b458 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Mutator.java @@ -0,0 +1,188 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.jooby.internal.openapi.OperationExt; +import io.pebbletemplates.pebble.error.PebbleException; +import io.pebbletemplates.pebble.extension.Filter; +import io.pebbletemplates.pebble.template.EvaluationContext; +import io.pebbletemplates.pebble.template.PebbleTemplate; +import io.swagger.v3.oas.models.media.Schema; + +public enum Mutator implements Filter { + example { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + if (input instanceof Schema schema) { + var asciidoc = AsciiDocContext.from(context); + return asciidoc.schemaExample(schema); + } + return input; + } + }, + truncate { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + if (input instanceof Schema schema) { + var asciidoc = AsciiDocContext.from(context); + return asciidoc.reduceSchema(schema); + } + return input; + } + }, + request { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + return new HttpRequest(AsciiDocContext.from(context), toOperation(input), args); + } + }, + response { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + return new HttpResponse( + context, + toOperation(input), + Optional.ofNullable(args.get("code")) + .map(Number.class::cast) + .map(Number::intValue) + .orElse(null), + args); + } + + @Override + public List getArgumentNames() { + return List.of("code"); + } + }, + parameters { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + var in = normalizeList(args.getOrDefault("in", "*")); + var includes = normalizeList(args.getOrDefault("includes", List.of())); + return toHttpRequest(context, input, args).getParameters(in, includes); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private List normalizeList(Object value) { + if (value instanceof List valueList) { + return valueList; + } + return value == null ? List.of() : List.of(value.toString()); + } + + @Override + public List getArgumentNames() { + return List.of("in", "includes"); + } + }, + body { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + var bodyType = args.getOrDefault("type", "full"); + // Handle response a bit different + if (input instanceof HttpResponse rsp) { + // success or error + return rsp.getSucessOrError(); + } + return toHttpMessage(context, input, Map.of("body", bodyType)).getBody(); + } + }, + form { + @Override + public Object apply( + Object input, + Map args, + PebbleTemplate self, + EvaluationContext context, + int lineNumber) + throws PebbleException { + return toHttpRequest(context, input, args).getForm(); + } + }; + + protected OperationExt toOperation(Object input) { + return switch (input) { + case OperationExt op -> op; + case HttpRequest req -> req.operation(); + case HttpResponse rsp -> rsp.operation(); + case null -> throw new NullPointerException(name() + ": requires a request/response input"); + default -> + throw new ClassCastException( + name() + ": requires a request/response input: " + input.getClass()); + }; + } + + protected HttpMessage toHttpMessage( + EvaluationContext context, Object input, Map options) { + return switch (input) { + // default to http request + case OperationExt op -> new HttpRequest(AsciiDocContext.from(context), op, options); + case HttpMessage msg -> msg; + case null -> throw new NullPointerException(name() + ": requires a request/response input"); + default -> + throw new ClassCastException( + name() + ": requires a request/response input: " + input.getClass()); + }; + } + + protected HttpRequest toHttpRequest( + EvaluationContext context, Object input, Map options) { + return switch (input) { + // default to http request + case OperationExt op -> new HttpRequest(AsciiDocContext.from(context), op, options); + case HttpRequest msg -> msg; + case null -> throw new NullPointerException(name() + ": requires a request/response input"); + default -> + throw new ClassCastException( + name() + ": requires a request/response input: " + input.getClass()); + }; + } + + @Override + public List getArgumentNames() { + return List.of(); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ParameterList.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ParameterList.java new file mode 100644 index 0000000000..9cf432b560 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ParameterList.java @@ -0,0 +1,51 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.AbstractList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.swagger.v3.oas.models.parameters.Parameter; + +@JsonIgnoreProperties({"includes"}) +public class ParameterList extends AbstractList { + public static final List NAME_DESC = List.of("name", "description"); + public static final List NAME_TYPE_DESC = List.of("name", "type", "description"); + public static final List PARAM = List.of("name", "type", "in", "description"); + private final List parameters; + private final List includes; + + public ParameterList(List parameters, List includes) { + this.parameters = parameters; + this.includes = includes; + } + + public List parameters() { + return parameters; + } + + public List includes() { + return includes; + } + + @Override + public int size() { + return parameters.size(); + } + + @NonNull @Override + public String toString() { + return parameters.stream().map(Parameter::getName).collect(Collectors.joining(", ")); + } + + @Override + public Parameter get(int index) { + return parameters.get(index); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/StatusCodeList.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/StatusCodeList.java new file mode 100644 index 0000000000..6b4adebeb8 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/StatusCodeList.java @@ -0,0 +1,49 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.internal.openapi.asciidoc.display.MapToAsciiDoc; + +@JsonIncludeProperties({"codes"}) +public record StatusCodeList(List> codes) + implements Iterable>, ToAsciiDoc { + @NonNull @Override + public String toString() { + return codes.toString(); + } + + @NonNull @Override + public Iterator> iterator() { + return codes.iterator(); + } + + @Override + public String list(Map options) { + var sb = new StringBuilder(); + codes.forEach( + (row) -> + sb.append("* `+") + .append(row.get("code")) + .append("+`: ") + .append(row.get("reason")) + .append('\n')); + if (!sb.isEmpty()) { + sb.setLength(sb.length() - 1); + } + return sb.toString(); + } + + @Override + public String table(Map options) { + return new MapToAsciiDoc(codes).table(options); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/TagExt.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/TagExt.java new file mode 100644 index 0000000000..7655775d00 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/TagExt.java @@ -0,0 +1,31 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.List; + +import io.swagger.v3.oas.models.tags.Tag; + +public class TagExt extends Tag { + + private final List operations; + + public TagExt(Tag tag, List operations) { + setDescription(tag.getDescription()); + setName(tag.getName()); + setExternalDocs(tag.getExternalDocs()); + setExtensions(tag.getExtensions()); + this.operations = operations; + } + + public List getOperations() { + return operations; + } + + public List getRoutes() { + return operations; + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ToAsciiDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ToAsciiDoc.java new file mode 100644 index 0000000000..435ead5f3e --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ToAsciiDoc.java @@ -0,0 +1,14 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.Map; + +public interface ToAsciiDoc { + String list(Map options); + + String table(Map options); +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ToSnippet.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ToSnippet.java new file mode 100644 index 0000000000..d3e0a82fd0 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/ToSnippet.java @@ -0,0 +1,12 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import java.util.Map; + +public interface ToSnippet { + String render(Map options); +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/MapToAsciiDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/MapToAsciiDoc.java new file mode 100644 index 0000000000..2f1511fd84 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/MapToAsciiDoc.java @@ -0,0 +1,55 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc.display; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import io.jooby.internal.openapi.asciidoc.ToAsciiDoc; + +public record MapToAsciiDoc(List> rows) implements ToAsciiDoc { + + public String list(Map options) { + var sb = new StringBuilder(); + rows.forEach( + (row) -> { + row.forEach( + (name, value) -> { + sb.append("* ").append(name).append(": ").append(value).append('\n'); + }); + }); + if (!sb.isEmpty()) { + sb.setLength(sb.length() - 1); + } + return sb.toString(); + } + + public String table(Map options) { + var sb = new StringBuilder(); + if (!options.isEmpty()) { + sb.append( + options.entrySet().stream() + .map(it -> it.getKey() + "=\"" + it.getValue() + "\"") + .collect(Collectors.joining(", ", "[", "]"))) + .append('\n'); + } + sb.append("|===").append('\n'); + if (!rows.isEmpty()) { + sb.append(rows.getFirst().keySet().stream().collect(Collectors.joining("|", "|", ""))) + .append("\n\n"); + rows.forEach( + row -> { + row.values().forEach(value -> sb.append("|").append(value).append("\n")); + sb.append("\n"); + }); + sb.append("\n"); + } + sb.setLength(sb.length() - 1); + sb.append("|==="); + return sb.toString(); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/OpenApiToAsciiDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/OpenApiToAsciiDoc.java new file mode 100644 index 0000000000..22185430cc --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/OpenApiToAsciiDoc.java @@ -0,0 +1,211 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc.display; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.google.common.base.CaseFormat; +import io.jooby.internal.openapi.EnumSchema; +import io.jooby.internal.openapi.asciidoc.AsciiDocContext; +import io.jooby.internal.openapi.asciidoc.ParameterList; +import io.jooby.internal.openapi.asciidoc.ToAsciiDoc; +import io.swagger.v3.oas.models.media.Schema; + +public record OpenApiToAsciiDoc( + AsciiDocContext context, + Map> properties, + List columns, + Map additionalProperties) + implements ToAsciiDoc { + private static final String ROOT = "___root__"; + + public static OpenApiToAsciiDoc schema(AsciiDocContext context, Schema schema) { + var columns = + schema instanceof EnumSchema + ? List.of("name", "description") + : List.of("name", "type", "description"); + var properties = new LinkedHashMap>(); + properties.put(OpenApiToAsciiDoc.ROOT, schema); + context.traverseSchema(schema, properties::put); + return new OpenApiToAsciiDoc(context, properties, columns, Map.of()); + } + + public static OpenApiToAsciiDoc parameters(AsciiDocContext context, ParameterList parameters) { + var properties = new LinkedHashMap>(); + parameters.forEach(p -> properties.put(p.getName(), p.getSchema())); + Map additionalProperties = new LinkedHashMap<>(); + parameters.forEach( + p -> { + additionalProperties.put(p.getName() + ".in", p.getIn()); + additionalProperties.put(p.getName() + ".description", p.getDescription()); + }); + return new OpenApiToAsciiDoc(context, properties, parameters.includes(), additionalProperties); + } + + public String list(Map options) { + var isEnum = properties.get(ROOT) instanceof EnumSchema; + var sb = new StringBuilder(); + if (isEnum) { + var enumSchema = (EnumSchema) properties.remove(ROOT); + for (var enumName : enumSchema.getEnum()) { + sb.append(boldCell(enumName)).append("::").append('\n'); + var enumDesc = enumSchema.getDescription(enumName); + if (enumDesc != null) { + sb.append("* ").append(enumDesc); + } + sb.append('\n'); + } + } else { + properties.remove(ROOT); + properties.forEach( + (name, value) -> { + sb.append(name).append("::").append('\n'); + sb.append("* ") + .append("type") + .append(": ") + .append(monospaceCell(context.schemaType(value))) + .append('\n'); + var in = additionalProperties.get(name + ".in"); + if (in != null) { + sb.append("* ") + .append("in") + .append(": ") + .append(monospaceCell((String) in)) + .append('\n'); + } + var isEnumProperty = value instanceof EnumSchema; + var description = + isEnumProperty ? ((EnumSchema) value).getSummary() : value.getDescription(); + if (isEnumProperty) { + sb.append("* ").append("description").append(":"); + if (description != null) { + sb.append(" ").append(description); + } + sb.append('\n'); + var enumSchema = (EnumSchema) value; + for (var enumName : enumSchema.getEnum()) { + sb.append("** ").append(boldCell(enumName)); + var enumDesc = enumSchema.getDescription(enumName); + if (enumDesc != null) { + sb.append(": ").append(enumDesc); + } + sb.append('\n'); + } + } else { + if (description != null) { + sb.append("* ").append("description").append(": ").append(description).append('\n'); + } + } + }); + } + if (!sb.isEmpty()) { + sb.setLength(sb.length() - 1); + } + return sb.toString(); + } + + @SuppressWarnings({"unchecked"}) + public String table(Map options) { + var isEnum = properties.get(ROOT) instanceof EnumSchema; + var columns = (List) options.getOrDefault("columns", this.columns); + options.remove("columns"); + var colList = colList(columns); + var sb = new StringBuilder(); + sb.append("|===").append('\n'); + sb.append(header(columns)).append('\n'); + if (isEnum) { + var enumSchema = (EnumSchema) properties.remove(ROOT); + for (var enumName : enumSchema.getEnum()) { + sb.append("| ").append(boldCell(enumName)).append('\n'); + var enumDesc = enumSchema.getDescription(enumName); + if (enumDesc != null) { + sb.append("| ").append(enumDesc); + } + sb.append('\n'); + } + } else { + properties.remove(ROOT); + properties.forEach( + (name, value) -> { + var isPropertyEnum = value instanceof EnumSchema; + for (int i = 0; i < columns.size(); i++) { + var column = columns.get(i); + sb.append("|").append(row(column, name, value)).append("\n"); + if (isPropertyEnum && column.equals("description")) { + colList.set(i, colList.get(i) + "a"); + var enumSchema = (EnumSchema) value; + for (var enumValue : enumSchema.getEnum()) { + sb.append("\n* ").append(boldCell(enumValue)); + var enumDesc = enumSchema.getDescription(enumValue); + if (enumDesc != null) { + sb.append(": ").append(enumDesc); + } + } + sb.append('\n'); + } + } + sb.append('\n'); + }); + } + sb.append("|==="); + options.putIfAbsent("cols", colsToString(colList)); + if (options.size() == 1) { + options.put("options", "header"); + } + return options.entrySet().stream() + .map(e -> e.getKey() + "=\"" + e.getValue() + "\"") + .collect(Collectors.joining(", ", "[", "]")) + + "\n" + + sb; + } + + private String colsToString(List cols) { + return String.join(",", cols); + } + + private List colList(List names) { + return names.stream() + .map(it -> it.equals("description") ? "3" : "1") + .collect(Collectors.toList()); + } + + private String header(List names) { + return names.stream() + .map(it -> CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, it)) + .collect(Collectors.joining("|", "|", "")); + } + + private static String monospaceCell(String value) { + return value == null || value.trim().isEmpty() ? "" : "`+" + value + "+`"; + } + + private String boldCell(String value) { + return value == null || value.trim().isEmpty() ? "" : "*" + value + "*"; + } + + private String nullSafe(String value) { + return value == null || value.trim().isEmpty() ? "" : value; + } + + private String row(String col, String property, Schema schema) { + return nullSafe( + switch (col) { + case "name" -> monospaceCell(property); + case "type" -> monospaceCell(context.schemaType(schema)); + case "in" -> monospaceCell((String) additionalProperties.get(property + "." + col)); + case "description" -> + (schema instanceof EnumSchema enumSchema + ? enumSchema.getSummary() + : (String) + additionalProperties.getOrDefault( + property + "." + col, schema.getDescription())); + default -> throw new IllegalArgumentException("Unknown property: " + col); + }); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/RequestToCurl.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/RequestToCurl.java new file mode 100644 index 0000000000..9da8674661 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/RequestToCurl.java @@ -0,0 +1,180 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc.display; + +import java.util.*; + +import com.google.common.base.Splitter; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Router; +import io.jooby.internal.openapi.asciidoc.*; + +public class RequestToCurl implements ToSnippet { + private static final CharSequence Accept = new HeaderName("Accept"); + private static final CharSequence ContentType = new HeaderName("Content-Type"); + + private final AsciiDocContext context; + private final HttpRequest request; + + public RequestToCurl(AsciiDocContext context, HttpRequest request) { + this.context = context; + this.request = request; + } + + @Override + public String render(Map args) { + var language = (String) args.remove("language"); + var options = args(args); + var method = removeOption(options, "-X", request.getMethod()).toUpperCase(); + /* Accept/Content-Type: */ + var addAccept = true; + var addContentType = true; + if (options.containsKey("-H")) { + var headers = parseHeaders(options.get("-H")); + addAccept = !headers.containsKey(Accept); + addContentType = !headers.containsKey(ContentType); + } + if (addAccept) { + request.getProduces().forEach(value -> options.put("-H", "'Accept: " + value + "'")); + } + if (addContentType + && Set.of(Router.PATCH, Router.PUT, Router.POST, Router.DELETE).contains(method)) { + request.getConsumes().forEach(value -> options.put("-H", "'Content-Type: " + value + "'")); + } + /* Body */ + var formUrlEncoded = + request.formUrlEncoded( + (schema, field) -> { + var option = "--data-urlencode"; + var value = field.getValue(); + if ("binary".equals(schema.getFormat())) { + option = "-F"; + value = "@/file%1$s.extension"; + } + return Map.entry(option, "\"" + field.getKey() + "=" + value + "\""); + }); + if (formUrlEncoded.isEmpty()) { + var body = request.getBody(); + if (body != null) { + options.put("-d", "'" + context.toJson(context.schemaProperties(body), false) + "'"); + } + } else { + formUrlEncoded.forEach(options::put); + } + + /* Method */ + var url = request.getPath() + request.getQueryString(); + options.put("-X", method + " '" + url + "'"); + return toString(options, language); + } + + private String toString(Multimap options, String language) { + var curl = "curl"; + var sb = new StringBuilder(); + sb.append("[source"); + if (language != null) { + sb.append(", ").append(language); + } + sb.append("]\n----\n").append(curl); + var separator = "\\\n"; + var tabSize = 1; + for (var entry : options.entries()) { + var k = entry.getKey(); + var v = entry.getValue(); + sb.append(" ".repeat(tabSize)); + sb.append(k); + if (v != null && !v.isEmpty()) { + sb.append(" ").append(v); + } + sb.append(separator); + tabSize = curl.length() + 1; + } + sb.setLength(sb.length() - separator.length()); + sb.append("\n----"); + return sb.toString(); + } + + private Multimap parseHeaders(Collection headers) { + Multimap result = LinkedHashMultimap.create(); + for (var line : headers) { + if (line.startsWith("'") && line.endsWith("'")) { + line = line.substring(1, line.length() - 1); + } + var header = Splitter.on(':').trimResults().omitEmptyStrings().splitToList(line); + if (header.size() != 2) { + throw new IllegalArgumentException("Invalid header: " + line); + } + result.put(new HeaderName(header.get(0)), header.get(1)); + } + return result; + } + + @NonNull private static String removeOption( + Multimap options, String name, String defaultValue) { + return Optional.of(options.removeAll(name)) + .map(Collection::iterator) + .filter(Iterator::hasNext) + .map(Iterator::next) + .orElse(defaultValue); + } + + private Multimap args(Map args) { + Multimap result = LinkedHashMultimap.create(); + var optionList = new ArrayList<>(args.values()); + for (int i = 0; i < optionList.size(); ) { + var key = optionList.get(i).toString(); + String value = null; + if (i + 1 < optionList.size()) { + var next = optionList.get(i + 1); + if (next.toString().startsWith("-")) { + i += 1; + } else { + value = next.toString(); + i += 2; + } + } else { + i += 1; + } + result.put(key, value == null ? "" : value); + } + return result; + } + + private record HeaderName(String value) implements CharSequence { + + @Override + public int length() { + return value.length(); + } + + @Override + public boolean equals(Object obj) { + return value.equalsIgnoreCase(obj.toString()); + } + + @Override + public int hashCode() { + return value.toLowerCase().hashCode(); + } + + @Override + public char charAt(int index) { + return value.charAt(index); + } + + @NonNull @Override + public CharSequence subSequence(int start, int end) { + return value.subSequence(start, end); + } + + @Override + @NonNull public String toString() { + return value; + } + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/RequestToHttp.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/RequestToHttp.java new file mode 100644 index 0000000000..387e11de7c --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/RequestToHttp.java @@ -0,0 +1,50 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc.display; + +import java.util.Map; + +import io.jooby.SneakyThrows; +import io.jooby.internal.openapi.ParameterExt; +import io.jooby.internal.openapi.asciidoc.AsciiDocContext; +import io.jooby.internal.openapi.asciidoc.HttpRequest; +import io.jooby.internal.openapi.asciidoc.ToSnippet; + +/** + * [source,http,options="nowrap"] ---- ${method} ${path} HTTP/1.1 {% for h in headers -%} ${h.name}: + * ${h.value} {% endfor -%} ${requestBody -} ---- + * + * @param context + * @param request + */ +public record RequestToHttp(AsciiDocContext context, HttpRequest request) implements ToSnippet { + @Override + public String render(Map options) { + try { + var sb = new StringBuilder(); + sb.append("[source,http,options=\"nowrap\"]").append('\n'); + sb.append("----").append('\n'); + sb.append(request.getMethod()) + .append(" ") + .append(request.getPath()) + .append(" HTTP/1.1") + .append('\n'); + for (var header : request.getHeaders()) { + sb.append(header.getName()) + .append(": ") + .append(((ParameterExt) header).getDefaultValue()) + .append('\n'); + } + var schema = request.getBody(); + if (schema != null) { + sb.append(context.toJson(context.schemaProperties(schema), false)).append('\n'); + } + return sb.append("----").toString(); + } catch (Exception x) { + throw SneakyThrows.propagate(x); + } + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/ResponseToHttp.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/ResponseToHttp.java new file mode 100644 index 0000000000..545572c473 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/display/ResponseToHttp.java @@ -0,0 +1,39 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc.display; + +import java.util.Map; + +import io.jooby.SneakyThrows; +import io.jooby.internal.openapi.ParameterExt; +import io.jooby.internal.openapi.asciidoc.*; + +public record ResponseToHttp(AsciiDocContext context, HttpResponse response) implements ToSnippet { + @Override + public String render(Map options) { + try { + var sb = new StringBuilder(); + sb.append("[source,http,options=\"nowrap\"]").append('\n'); + sb.append("----").append('\n'); + sb.append("HTTP/1.1 ") + .append(response.getStatusCode().value()) + .append(" ") + .append(response.getStatusCode().reason()) + .append('\n'); + for (var header : response.getHeaders()) { + var value = ((ParameterExt) header).getDefaultValue(); + sb.append(header.getName()).append(": ").append(value).append('\n'); + } + var schema = response.getBody(); + if (schema != null) { + sb.append(context.toJson(context.schemaProperties(schema), false)).append('\n'); + } + return sb.append("----").toString(); + } catch (Exception x) { + throw SneakyThrows.propagate(x); + } + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ClassDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ClassDoc.java index 35d7c04bda..7fb1c6136f 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ClassDoc.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ClassDoc.java @@ -88,6 +88,16 @@ public String getEnumDescription(String text) { return text; } + public String getEnumItemDescription(String name) { + if (isEnum()) { + var field = fields.get(name); + if (field != null) { + return field.getText(); + } + } + return null; + } + private void defaultRecordMembers() { JavaDocTag.javaDocTag( javadoc, diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ContentSplitter.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ContentSplitter.java new file mode 100644 index 0000000000..12e7e80b60 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ContentSplitter.java @@ -0,0 +1,158 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.javadoc; + +public class ContentSplitter { + + public record ContentResult(String summary, String description) {} + + public static ContentResult split(String text) { + if (text == null || text.isEmpty()) { + return new ContentResult("", ""); + } + + int len = text.length(); + int splitIndex = -1; + + // State trackers + int parenDepth = 0; // ( ) + int bracketDepth = 0; // [ ] + int braceDepth = 0; // { } + boolean inHtmlDef = false; // < ... > + boolean inCodeBlock = false; //

...
or ... + + for (int i = 0; i < len; i++) { + char c = text.charAt(i); + + // 1. Handle HTML Tags start + if (c == '<') { + // Check for

(Paragraph Split - Exclusive) + if (!inCodeBlock && !inHtmlDef && isTag(text, i, "p")) { + splitIndex = i; + break; + } + // Check for Protected Blocks (

, )
+        if (!inCodeBlock && (isTag(text, i, "pre") || isTag(text, i, "code"))) {
+          inCodeBlock = true;
+        }
+        // Check for end of Protected Blocks
+        if (inCodeBlock && (isCloseTag(text, i, "pre") || isCloseTag(text, i, "code"))) {
+          inCodeBlock = false;
+        }
+        inHtmlDef = true;
+        continue;
+      }
+
+      // 2. Handle HTML Tags end
+      if (c == '>') {
+        inHtmlDef = false;
+        continue;
+      }
+
+      // 3. Handle Nesting & Split
+      if (!inHtmlDef && !inCodeBlock) {
+        if (c == '(') {
+          parenDepth++;
+        } else if (c == ')') {
+          if (parenDepth > 0) parenDepth--;
+        } else if (c == '[') {
+          bracketDepth++;
+        } else if (c == ']') {
+          if (bracketDepth > 0) bracketDepth--;
+        } else if (c == '{') {
+          braceDepth++;
+        } else if (c == '}') {
+          if (braceDepth > 0) braceDepth--;
+        }
+        // 4. Check for Period
+        else if (c == '.') {
+          if (parenDepth == 0 && bracketDepth == 0 && braceDepth == 0) {
+            splitIndex = i + 1;
+            break;
+          }
+        }
+      }
+    }
+
+    String summary;
+    String description;
+
+    if (splitIndex == -1) {
+      summary = text.trim();
+      description = "";
+    } else {
+      summary = text.substring(0, splitIndex).trim();
+      description = text.substring(splitIndex).trim();
+    }
+
+    // Clean up: Strip 

tags without using Regex + return new ContentResult(stripParagraphTags(summary), stripParagraphTags(description)); + } + + /** + * Removes + * + *

and tags (and their attributes) from the text. Keeps content inside the tags. + */ + private static String stripParagraphTags(String text) { + if (text.isEmpty()) return text; + + StringBuilder sb = new StringBuilder(text.length()); + int len = text.length(); + + for (int i = 0; i < len; i++) { + char c = text.charAt(i); + + if (c == '<') { + // Detect or + if (isTag(text, i, "p") || isCloseTag(text, i, "p")) { + // Fast-forward until we find the closing '>' + while (i < len && text.charAt(i) != '>') { + i++; + } + // We are now at '>', loop increment will move past it + continue; + } + } + sb.append(c); + } + return sb.toString().trim(); + } + + // --- Helper Methods --- + + private static boolean isTag(String text, int i, String tagName) { + int len = tagName.length(); + if (i + 1 + len > text.length()) return false; + + // Match tagName (case insensitive) + for (int k = 0; k < len; k++) { + char c = text.charAt(i + 1 + k); + if (Character.toLowerCase(c) != tagName.charAt(k)) return false; + } + + // Check delimiter (must be '>' or whitespace or end of string) + if (i + 1 + len == text.length()) return true; + char delimiter = text.charAt(i + 1 + len); + return delimiter == '>' || Character.isWhitespace(delimiter); + } + + private static boolean isCloseTag(String text, int i, String tagName) { + int len = tagName.length(); + if (i + 2 + len > text.length()) return false; + if (text.charAt(i + 1) != '/') return false; + + // Match tagName (case insensitive) + for (int k = 0; k < len; k++) { + char c = text.charAt(i + 2 + k); + if (Character.toLowerCase(c) != tagName.charAt(k)) return false; + } + + // Check delimiter + char delimiter = text.charAt(i + 2 + len); + return delimiter == '>' || Character.isWhitespace(delimiter); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/FieldDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/FieldDoc.java index 13e1ba2117..6ce38c22ce 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/FieldDoc.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/FieldDoc.java @@ -17,6 +17,12 @@ public FieldDoc(JavaDocParser ctx, DetailAST node, DetailAST javadoc) { super(ctx, node, javadoc); } + @Override + public String getText() { + var text = super.getText(); + return text == null ? null : text.replace("

", "").replace("

", "").trim(); + } + public String getName() { return JavaDocSupport.getSimpleName(node); } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocNode.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocNode.java index 6d30ff8fde..fd3459ca79 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocNode.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocNode.java @@ -5,7 +5,6 @@ */ package io.jooby.internal.openapi.javadoc; -import static io.jooby.internal.openapi.javadoc.JavaDocStream.*; import static io.jooby.internal.openapi.javadoc.JavaDocStream.javadocToken; import java.util.*; @@ -55,29 +54,8 @@ public Map getExtensions() { } public String getSummary() { - var builder = new StringBuilder(); - for (var node : forward(javadoc, JAVADOC_TAG).toList()) { - if (node.getType() == JavadocCommentsTokenTypes.TEXT) { - var text = node.getText(); - var trimmed = text.trim(); - if (trimmed.isEmpty()) { - if (!builder.isEmpty()) { - builder.append(text); - } - } else { - builder.append(text); - } - } else if (node.getType() == JavadocCommentsTokenTypes.NEWLINE && !builder.isEmpty()) { - break; - } - var index = builder.indexOf("."); - if (index > 0) { - builder.setLength(index + 1); - break; - } - } - var string = builder.toString().trim(); - return string.isEmpty() ? null : string; + var summary = ContentSplitter.split(getText()).summary(); + return summary.isEmpty() ? null : summary; } public List getTags() { @@ -85,12 +63,8 @@ public List getTags() { } public String getDescription() { - var text = getText(); - var summary = getSummary(); - if (summary == null) { - return text; - } - return summary.equals(text) ? null : text.replaceAll(summary, "").trim(); + var description = ContentSplitter.split(getText()).description(); + return description.isEmpty() ? null : description; } public String getText() { @@ -143,7 +117,12 @@ protected static String getText(List nodes, boolean stripLeading) { if (next != null && next.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK) { builder.append(next.getText()); visited.add(next); - // visited.add(next.getNextSibling()); + } + } else if (node.getType() == JavadocCommentsTokenTypes.TAG_NAME) { + //

? + if (node.getText().equals("p")) { + // keep so we can split summary from description + builder.append("

"); } } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java b/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java index a9b1ab7116..b73fe20638 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java @@ -21,6 +21,7 @@ import io.jooby.Router; import io.jooby.SneakyThrows; import io.jooby.internal.openapi.*; +import io.jooby.internal.openapi.asciidoc.AsciiDocContext; import io.jooby.internal.openapi.javadoc.JavaDocParser; import io.swagger.v3.core.util.*; import io.swagger.v3.oas.models.OpenAPI; @@ -47,7 +48,10 @@ public enum Format { /** JSON. */ JSON { @Override - public String toString(OpenAPIGenerator tool, OpenAPI result) { + @NonNull protected String toString( + @NonNull OpenAPIGenerator tool, + @NonNull OpenAPI result, + @NonNull Map options) { return tool.toJson(result); } }, @@ -55,9 +59,49 @@ public String toString(OpenAPIGenerator tool, OpenAPI result) { /** YAML. */ YAML { @Override - public String toString(OpenAPIGenerator tool, OpenAPI result) { + @NonNull protected String toString( + @NonNull OpenAPIGenerator tool, + @NonNull OpenAPI result, + @NonNull Map options) { return tool.toYaml(result); } + }, + + ADOC { + @Override + @NonNull protected String toString( + @NonNull OpenAPIGenerator tool, + @NonNull OpenAPI result, + @NonNull Map options) { + return tool.toAdoc(result, options); + } + + @SuppressWarnings("unchecked") + @NonNull @Override + public List write( + @NonNull OpenAPIGenerator tool, + @NonNull OpenAPI result, + @NonNull Map options) + throws IOException { + var files = (List) options.get("adoc"); + if (files == null || files.isEmpty()) { + // adoc generation is optional + return List.of(); + } + var outputDir = (Path) options.get("outputDir"); + var outputList = new ArrayList(); + var context = tool.createAsciidoc(files.getFirst().getParent(), (OpenAPIExt) result); + for (var file : files) { + var opts = new HashMap<>(options); + opts.put("adoc", file); + var content = toString(tool, result, opts); + var output = outputDir.resolve(file.getFileName()); + Files.write(output, List.of(content)); + context.export(output, outputDir); + outputList.add(output); + } + return outputList; + } }; /** @@ -76,8 +120,28 @@ public String toString(OpenAPIGenerator tool, OpenAPI result) { * @param result Model. * @return String (json or yaml content). */ - public abstract @NonNull String toString( - @NonNull OpenAPIGenerator tool, @NonNull OpenAPI result); + protected abstract @NonNull String toString( + @NonNull OpenAPIGenerator tool, + @NonNull OpenAPI result, + @NonNull Map options); + + /** + * Convert an {@link OpenAPI} model to the current format. + * + * @param tool Generator. + * @param result Model. + * @return String (json or yaml content). + */ + public @NonNull List write( + @NonNull OpenAPIGenerator tool, + @NonNull OpenAPI result, + @NonNull Map options) + throws IOException { + var output = (Path) options.get("output"); + var content = toString(tool, result, options); + Files.write(output, List.of(content)); + return List.of(output); + } } private Logger log = LoggerFactory.getLogger(getClass()); @@ -100,6 +164,8 @@ public String toString(OpenAPIGenerator tool, OpenAPI result) { private SpecVersion specVersion = SpecVersion.V30; + private AsciiDocContext asciidoc; + /** Default constructor. */ public OpenAPIGenerator() {} @@ -111,29 +177,31 @@ public OpenAPIGenerator() {} * @return Output file. * @throws IOException If fails to process input. */ - public @NonNull Path export(@NonNull OpenAPI openAPI, @NonNull Format format) throws IOException { + public @NonNull List export( + @NonNull OpenAPI openAPI, @NonNull Format format, @NonNull Map options) + throws IOException { Path output; if (openAPI instanceof OpenAPIExt) { - String source = ((OpenAPIExt) openAPI).getSource(); - String[] names = source.split("\\."); + var source = ((OpenAPIExt) openAPI).getSource(); + var names = source.split("\\."); output = Stream.of(names).limit(names.length - 1).reduce(outputDir, Path::resolve, Path::resolve); - String appname = names[names.length - 1]; + var appname = names[names.length - 1]; if (appname.endsWith("Kt")) { appname = appname.substring(0, appname.length() - 2); } output = output.resolve(appname + "." + format.extension()); } else { - output = outputDir.resolve("openapi." + format.extension()); + throw new ClassCastException(openAPI.getClass() + " is not a " + OpenAPIExt.class); } if (!Files.exists(output.getParent())) { Files.createDirectories(output.getParent()); } - - String content = format.toString(this, openAPI); - Files.write(output, Collections.singleton(content)); - return output; + var allOptions = new HashMap<>(options); + allOptions.put("output", output); + allOptions.put("outputDir", output.getParent()); + return format.write(this, openAPI, allOptions); } /** @@ -177,6 +245,7 @@ public OpenAPIGenerator() {} doc.getServers().forEach(openapi::addServersItem); doc.getContact().forEach(info::setContact); doc.getLicense().forEach(info::setLicense); + doc.getTags().forEach(openapi::addTagsItem); }); } @@ -201,7 +270,7 @@ public OpenAPIGenerator() {} Map globalTags = new LinkedHashMap<>(); Paths paths = new Paths(); for (OperationExt operation : operations) { - String pattern = operation.getPattern(); + String pattern = operation.getPath(); if (!includes(pattern) || excludes(pattern)) { log.debug("skipping {}", pattern); continue; @@ -310,6 +379,24 @@ private void defaults(String classname, String contextPath, OpenAPIExt openapi) } } + /** + * Generates an adoc version of the given model. + * + * @param openAPI Model. + * @return YAML content. + */ + public @NonNull String toAdoc(@NonNull OpenAPI openAPI, @NonNull Map options) { + try { + var file = (Path) options.get("adoc"); + if (file == null) { + throw new IllegalArgumentException("'adoc' file is required: " + options); + } + return createAsciidoc(file.getParent(), (OpenAPIExt) openAPI).generate(file); + } catch (IOException x) { + throw SneakyThrows.propagate(x); + } + } + /** * Generates a JSON version of the given model. * @@ -391,7 +478,7 @@ public Path getBasedir() { } /** - * Set output directory used by {@link #export(OpenAPI, Format)} operation. + * Set output directory used by {@link #export(OpenAPI, Format, Map)} operation. * *

Defaults to {@link #getBasedir()}. * @@ -438,7 +525,7 @@ public void setExcludes(@Nullable String excludes) { } /** - * Set output directory used by {@link #export(OpenAPI, Format)}. + * Set output directory used by {@link #export(OpenAPI, Format, Map)}. * * @param outputDir Output directory. */ @@ -451,7 +538,7 @@ public void setOutputDir(@NonNull Path outputDir) { * * @param specVersion One of 3.0 or 3.1. */ - public void setSpecVersion(SpecVersion specVersion) { + private void setSpecVersion(SpecVersion specVersion) { this.specVersion = specVersion; } @@ -461,19 +548,23 @@ public void setSpecVersion(SpecVersion specVersion) { * @param version One of 3.0 or 3.1. */ public void setSpecVersion(String version) { - if (specVersion != null) { - switch (version) { - case "v3.1", "v3.1.0", "3.1", "3.1.0": - setSpecVersion(SpecVersion.V31); - case "v3.0", "v3.0.0", "3.0", "3.0.0", "v3.0.1", "3.0.1": - setSpecVersion(SpecVersion.V30); - default: - throw new IllegalArgumentException( - "Invalid spec version: " + version + ". Supported version: [3.0.1, 3.1.0]"); - } + switch (version) { + case "v3.1", "v3.1.0", "3.1", "3.1.0", "V31": + setSpecVersion(SpecVersion.V31); + break; + case "v3.0", "v3.0.0", "3.0", "3.0.0", "v3.0.1", "3.0.1", "V30": + setSpecVersion(SpecVersion.V30); + break; + default: + throw new IllegalArgumentException( + "Invalid spec version: " + version + ". Supported version: [3.0.1, 3.1.0]"); } } + protected AsciiDocContext createAsciidoc(Path basedir, OpenAPIExt openapi) { + return new AsciiDocContext(basedir, jsonMapper(), yamlMapper(), openapi); + } + private String appname(String classname) { String name = classname; int i = name.lastIndexOf('.'); diff --git a/modules/jooby-openapi/src/main/java/module-info.java b/modules/jooby-openapi/src/main/java/module-info.java index deb869c94b..0606a6a0ed 100644 --- a/modules/jooby-openapi/src/main/java/module-info.java +++ b/modules/jooby-openapi/src/main/java/module-info.java @@ -17,4 +17,16 @@ requires org.objectweb.asm; requires org.objectweb.asm.tree; requires org.objectweb.asm.util; + requires io.pebbletemplates; + requires jdk.jshell; + requires com.google.common; + requires org.checkerframework.checker.qual; + requires org.asciidoctor.asciidoctorj.api; + requires jakarta.data; + requires io.swagger.annotations; + requires org.jruby; + requires net.datafaker; + requires com.fasterxml.jackson.dataformat.yaml; + requires io.swagger.models; + requires com.fasterxml.jackson.annotation; } diff --git a/modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/asciidoc/AutoDataFakerMapperTest.java b/modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/asciidoc/AutoDataFakerMapperTest.java new file mode 100644 index 0000000000..234630db51 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/asciidoc/AutoDataFakerMapperTest.java @@ -0,0 +1,157 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import com.fasterxml.jackson.core.JsonProcessingException; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AutoDataFakerMapperTest { + + private AutoDataFakerMapper mapper; + + @BeforeAll + void setup() { + // Initialize with a custom override for testing + mapper = new AutoDataFakerMapper(); + mapper.synonyms(Map.of("sku_id", "ean13")); + } + + @Test + void testExactMatchByClassAndField() { + // Book.title exists in Datafaker + Supplier generator = mapper.getGenerator("Book", "title", "string", "fail"); + String result = generator.get(); + + assertThat(result).isNotEqualTo("fail").isNotEmpty(); + } + + @Test + void testAuthorSSN() { + Supplier generator = mapper.getGenerator("Author", "ssn", "string", "fail"); + String result = generator.get(); + + assertThat(result).as("SSN format validation").matches("^\\d{3}-\\d{2}-\\d{4}$"); + } + + @Test + void testGenericMatchByField() { + // "firstName" is generic, should map to Name.firstName + Supplier generator = mapper.getGenerator("User", "firstName", "string", "fail"); + String result = generator.get(); + + assertThat(result).isNotEqualTo("fail").doesNotContain("fail"); + } + + @Test + void testSynonymHandling() { + // "dob" is a synonym for "birthday" + Supplier generator = mapper.getGenerator("User", "dob", "date", "fail"); + String result = generator.get(); + + // Datafaker birthday usually returns a date string + assertThat(result).isNotEqualTo("fail"); + } + + @Test + void testSynonymNormalization() { + // "e-mail" -> normalize -> "email" -> maps to internet().emailAddress() + Supplier generator = mapper.getGenerator("User", "e-mail", "string", "fail"); + String result = generator.get(); + + assertThat(result).contains("@"); + } + + @Test + void testFieldTypeFallback() { + // "unknown_field_xyz" does not exist in Faker. + // It should fallback to "date-time" type logic. + Supplier generator = + mapper.getGenerator("Log", "unknown_field_xyz", "date-time", "fail"); + String result = generator.get(); + + assertThat(result).isNotEqualTo("fail"); + } + + @Test + void testFieldTypeUUID() { + Supplier generator = mapper.getGenerator("Table", "pk_id", "uuid", "fail"); + String result = generator.get(); + + assertThat(result).hasSize(36); // UUID length + } + + @Test + void testCustomUserSynonym() { + // We registered "sku_id" -> "ean13" in setup() + Supplier generator = mapper.getGenerator("Product", "sku_id", "string", "fail"); + String result = generator.get(); + + // EAN13 is numbers + assertThat(result).matches("\\d+"); + } + + @Test + void testFuzzyMatching() { + // "customer_email_address" -> contains "email" -> maps to email provider + Supplier generator = + mapper.getGenerator("Customer", "customer_email_address", "string", "fail"); + String result = generator.get(); + + assertThat(result).contains("@"); + } + + @Test + void testCompleteFallback() { + // Nothing matches this + Supplier generator = + mapper.getGenerator("Alien", "warp_speed", "unknown_type", "DEFAULT_VALUE"); + String result = generator.get(); + + assertThat(result).isEqualTo("DEFAULT_VALUE"); + } + + @Test + @SuppressWarnings("unchecked") + void testCapabilityMapStructure() throws JsonProcessingException { + Map capabilities = mapper.getCapabilityMap(); + + // 1. Check Top Level Keys + assertThat(capabilities).containsKeys("domains", "generics", "types", "synonyms"); + + // 2. Check Domains: "book" -> { "title": "faker.book().title()" } + Map> domains = + (Map>) capabilities.get("domains"); + assertThat(domains).containsKey("book"); + + Map bookFields = domains.get("book"); + assertThat(bookFields).containsKey("title"); + assertThat(bookFields.get("title")).contains("faker.book().title"); + + // 3. Check Generics: "title" -> "faker.book().title()" + Map generics = (Map) capabilities.get("generics"); + assertThat(generics).containsKey("firstname"); + assertThat(generics.get("firstname")).contains("faker.name().firstName"); + + // 4. Check Types: "uuid" -> description + Map types = (Map) capabilities.get("types"); + assertThat(types).containsKey("uuid"); + assertThat(types.get("uuid")).contains("faker.internet().uuid"); + + // 5. Check Synonyms + Map synonyms = (Map) capabilities.get("synonyms"); + assertThat(synonyms).containsEntry("surname", "lastname"); + assertThat(synonyms).containsEntry("skuid", "ean13"); + } +} diff --git a/modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/asciidoc/PebbleTemplateSupport.java b/modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/asciidoc/PebbleTemplateSupport.java new file mode 100644 index 0000000000..a1decd839a --- /dev/null +++ b/modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/asciidoc/PebbleTemplateSupport.java @@ -0,0 +1,47 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.asciidoc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Path; + +import org.assertj.core.api.AbstractStringAssert; + +import io.jooby.SneakyThrows; +import io.jooby.internal.openapi.OpenAPIExt; +import io.swagger.v3.core.util.Json31; +import io.swagger.v3.core.util.Yaml31; + +public class PebbleTemplateSupport { + + private final AsciiDocContext context; + + public PebbleTemplateSupport(Path basedir, OpenAPIExt openapi) { + this.context = new AsciiDocContext(basedir, Json31.mapper(), Yaml31.mapper(), openapi); + } + + public AbstractStringAssert evaluateThat(String input) throws IOException { + return assertThat(evaluate(input)); + } + + public void evaluate(String input, SneakyThrows.Consumer consumer) throws IOException { + consumer.accept(evaluate(input)); + } + + public AsciiDocContext getContext() { + return context; + } + + public String evaluate(String input) throws IOException { + var template = context.getEngine().getLiteralTemplate(input); + var writer = new StringWriter(); + template.evaluate(writer); + return writer.toString().trim(); + } +} diff --git a/modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/javadoc/ContentSplitterTest.java b/modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/javadoc/ContentSplitterTest.java new file mode 100644 index 0000000000..39f2ff3f8f --- /dev/null +++ b/modules/jooby-openapi/src/test/java/io/jooby/internal/openapi/javadoc/ContentSplitterTest.java @@ -0,0 +1,169 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.javadoc; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ContentSplitterTest { + + @Test + void shouldHandleNullAndEmpty() { + assertSplit(null, "", ""); + assertSplit("", "", ""); + assertSplit(" ", "", ""); + } + + @Test + void shouldSplitOnSimplePeriod() { + assertSplit("Hello world. This is description.", "Hello world.", "This is description."); + } + + @Test + void shouldSplitOnParagraphTag() { + //

acts as the separator, exclusive + assertSplit("Hello world

Description

", "Hello world", "Description"); + + // Case insensitive

+ assertSplit("Hello world

Description

", "Hello world", "Description"); + + assertSplit( + "This is the Hello /endpoint\n

Operation description", + "This is the Hello /endpoint", + "Operation description"); + } + + @Test + void shouldPrioritizeWhateverComesFirst() { + // Period comes first + assertSplit("Summary first. Then

para

.", "Summary first.", "Then para."); + + // Paragraph comes first + assertSplit( + "Summary

with description containing.

periods.", + "Summary", + "with description containing. periods."); + } + + @Test + void shouldIgnorePeriodInsideParentheses() { + assertSplit("Jooby (v3.0) is great. Description.", "Jooby (v3.0) is great.", "Description."); + + // Nested parens + assertSplit("Text (outer (inner.)) done. Desc.", "Text (outer (inner.)) done.", "Desc."); + } + + @Test + void shouldIgnorePeriodInsideBrackets() { + assertSplit("Reference [fig. 1] is here. Next.", "Reference [fig. 1] is here.", "Next."); + } + + @Test + void shouldIgnorePeriodInsideHtmlAttributes() { + assertSplit( + "Check site. Done.", + "Check site.", + "Done."); + } + + @Test + void shouldHandleComplexHtmlAttributesInP() { + //

with attributes should still trigger split + assertSplit("Summary

Description

", "Summary", "Description"); + } + + @Test + void shouldNotSplitOnSimilarTags() { + //
 starts with p but is not a paragraph
+    assertSplit(
+        "Code 
val x = 1.0
is cool. End.", + "Code
val x = 1.0
is cool.", + "End."); + + // starts with p + assertSplit( + "Config ignored. Real split.", + "Config ignored.", + "Real split."); + } + + @Test + void shouldHandleUnbalancedNestingGracefully() { + // If user forgets to close (, we probably shouldn't crash, + // though behavior on period ignore depends on implementation. + // Logic: if depth > 0, we ignore periods. + assertSplit("Unbalanced ( paren. No split here.", "Unbalanced ( paren. No split here.", ""); + + // Unbalanced closed ) should not make depth negative + assertSplit("Unbalanced ) paren. Split.", "Unbalanced ) paren.", "Split."); + } + + @Test + void shouldHandleNoSeparators() { + String text = "Just a single sentence without periods or tags"; + assertSplit(text, text, ""); + } + + @Test + void shouldHandleLeadingAndTrailingSeparators() { + // Starts with

-> Empty summary + assertSplit("

Description only.

", "", "Description only."); + + // Ends with period -> Empty description + assertSplit("Only summary.", "Only summary.", ""); + } + + @Test + void shouldNotSplitInsidePreTags() { + // The period in 1.0 must be ignored because it is inside
...
+ assertSplit( + "Code
val x = 1.0
is cool. End.", + "Code
val x = 1.0
is cool.", + "End."); + } + + @Test + void shouldNotSplitInsideCodeTags() { + // The period in System.out must be ignored because it is inside ... + assertSplit( + "Use System.out.println for logging. Next.", + "Use System.out.println for logging.", + "Next."); + } + + @Test + void shouldHandleMixedNesting() { + // Parentheses + Code block + assertSplit( + "Check (e.g. var x = 1.0). Done.", + "Check (e.g. var x = 1.0).", + "Done."); + } + + @Test + void shouldIgnorePeriodInsideJavadocTags() { + // Test {@code ...} + assertSplit("Use {@code 1.0} version. Next.", "Use {@code 1.0} version.", "Next."); + + // Test {@link ...} + assertSplit("See {@link java.util.List}. End.", "See {@link java.util.List}.", "End."); + } + + @Test + void shouldIgnorePeriodInsideGeneralBraces() { + // Since we implemented brace tracking, this also supports standard JSON/Code blocks + assertSplit( + "Config { val x = 1.0; } allowed. Next.", "Config { val x = 1.0; } allowed.", "Next."); + } + + // Helper method to make tests readable + private void assertSplit(String input, String expectedSummary, String expectedDesc) { + var result = ContentSplitter.split(input); + assertEquals(expectedSummary, result.summary(), "Summary mismatch"); + assertEquals(expectedDesc, result.description(), "Description mismatch"); + } +} diff --git a/modules/jooby-openapi/src/test/java/io/jooby/openapi/CurrentDir.java b/modules/jooby-openapi/src/test/java/io/jooby/openapi/CurrentDir.java new file mode 100644 index 0000000000..ddc21c3cfe --- /dev/null +++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/CurrentDir.java @@ -0,0 +1,43 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.openapi; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Stream; + +public class CurrentDir { + public static Path basedir(String... others) { + return basedir(List.of(others)); + } + + public static Path basedir(List others) { + var baseDir = Paths.get(System.getProperty("user.dir")); + if (!baseDir.getFileName().toString().endsWith("jooby-openapi")) { + baseDir = baseDir.resolve("modules").resolve("jooby-openapi"); + } + for (var other : others) { + baseDir = baseDir.resolve(other); + } + return baseDir; + } + + public static Path testClass(Class clazz) { + var packageDir = clazz.getPackage().getName().split("\\."); + return basedir( + Stream.concat(Stream.of("src", "test", "resources"), Stream.of(packageDir)).toList()); + } + + public static Path testClass(Class clazz, String file) { + var packageDir = clazz.getPackage().getName().split("\\."); + return basedir( + Stream.concat( + Stream.concat(Stream.of("src", "test", "resources"), Stream.of(packageDir)), + Stream.of(file)) + .toList()); + } +} diff --git a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIExtension.java b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIExtension.java index b6ceddf36b..0668c56492 100644 --- a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIExtension.java +++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIExtension.java @@ -26,7 +26,9 @@ public boolean supportsParameter( ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { Parameter parameter = parameterContext.getParameter(); - return parameter.getType() == RouteIterator.class || parameter.getType() == OpenAPIResult.class; + return parameter.getType() == RouteIterator.class + || parameter.getType() == OpenAPIResult.class + || parameter.getType() == OpenAPIExt.class; } @Override @@ -43,7 +45,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte : EnumSet.copyOf(Arrays.asList(metadata.debug())); OpenAPIGenerator tool = newTool(debugOptions); - tool.setSpecVersion(metadata.version()); + tool.setSpecVersion(metadata.version().name()); String templateName = metadata.templateName(); if (templateName.isEmpty()) { templateName = classname.replace(".", "/").toLowerCase() + ".yaml"; @@ -66,6 +68,9 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte if (parameter.getType() == OpenAPIResult.class) { return result; } + if (parameter.getType() == OpenAPIExt.class) { + return result.getOpenAPI(); + } RouteIterator iterator = result.iterator(metadata.ignoreArguments()); getStore(context).put("iterator", iterator); return iterator; diff --git a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIResult.java b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIResult.java index 5913874ba9..c11fb966d4 100644 --- a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIResult.java +++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIResult.java @@ -5,12 +5,14 @@ */ package io.jooby.openapi; +import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.ObjectMapper; import io.jooby.SneakyThrows; import io.jooby.internal.openapi.OpenAPIExt; +import io.jooby.internal.openapi.asciidoc.AsciiDocContext; import io.swagger.v3.core.util.Json; import io.swagger.v3.core.util.Yaml; import io.swagger.v3.parser.OpenAPIV3Parser; @@ -35,6 +37,10 @@ public RouteIterator iterator(boolean ignoreArgs) { return new RouteIterator(openAPI == null ? List.of() : openAPI.getOperations(), ignoreArgs); } + public OpenAPIExt getOpenAPI() { + return openAPI; + } + public String toYaml() { return toYaml(true); } @@ -89,6 +95,34 @@ public String toJson(boolean validate) { } } + public String toAsciiDoc(Path index) { + return toAsciiDoc(index, false); + } + + public String toAsciiDoc(Path index, boolean validate) { + if (failure != null) { + throw failure; + } + try { + String json = this.json.writerWithDefaultPrettyPrinter().writeValueAsString(openAPI); + if (validate) { + SwaggerParseResult result = new OpenAPIV3Parser().readContents(json); + if (result.getMessages().isEmpty()) { + return json; + } + throw new IllegalStateException( + "Invalid OpenAPI specification:\n\t- " + + String.join("\n\t- ", result.getMessages()).trim() + + "\n\n" + + json); + } + var asciiDoc = new AsciiDocContext(index.getParent(), this.json, this.yaml, openAPI); + return asciiDoc.generate(index); + } catch (Exception x) { + throw SneakyThrows.propagate(x); + } + } + public static OpenAPIResult failure(RuntimeException failure) { var result = new OpenAPIResult(Json.mapper(), Yaml.mapper(), null); result.failure = failure; diff --git a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OperationBuilder.java b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OperationBuilder.java new file mode 100644 index 0000000000..a8813f219a --- /dev/null +++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OperationBuilder.java @@ -0,0 +1,166 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.openapi; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; + +import io.jooby.Router; +import io.jooby.StatusCode; +import io.jooby.internal.openapi.*; +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.responses.ApiResponses; + +public class OperationBuilder { + + private ApiResponses responses; + + private final OperationExt operation = mock(OperationExt.class); + + static { + ModelConverters.getInstance().addConverter(new ModelConverterExt(Json.mapper())); + } + + public static OperationBuilder operation(String method, String pattern) { + return new OperationBuilder().method(method).pattern(pattern); + } + + public OperationBuilder query(String... name) { + return parameter(Map.of("query", mapOf(name))); + } + + public OperationBuilder form(String... name) { + return parameter(Map.of("form", mapOf(name))); + } + + public OperationBuilder path(String... name) { + return parameter(Map.of("path", mapOf(name))); + } + + public OperationBuilder cookie(String... name) { + return parameter(Map.of("cookie", mapOf(name))); + } + + private static Map mapOf(String... values) { + Map map = new LinkedHashMap<>(); + for (var value : values) { + map.put(value, "string"); + } + return map; + } + + public OperationBuilder parameter(Map> parameterSpecs) { + List parameters = new ArrayList<>(); + for (var parameterSpec : parameterSpecs.entrySet()) { + var in = parameterSpec.getKey(); + for (var entry : parameterSpec.getValue().entrySet()) { + var schema = mock(Schema.class); + var type = entry.getValue(); + if (type.equals("binary")) { + when(schema.getFormat()).thenReturn(type); + type = "string"; + } + when(schema.getType()).thenReturn(type); + var parameter = mock(ParameterExt.class); + when(parameter.getName()).thenReturn(entry.getKey()); + when(parameter.getIn()).thenReturn(in); + when(parameter.getSchema()).thenReturn(schema); + parameters.add(parameter); + } + } + when(operation.getParameters()).thenReturn(parameters); + return this; + } + + public OperationBuilder produces(String... produces) { + return produces(List.of(produces)); + } + + public OperationBuilder produces(List produces) { + when(operation.getProduces()).thenReturn(produces); + return this; + } + + public OperationBuilder consumes(String... consumes) { + return consumes(List.of(consumes)); + } + + public OperationBuilder consumes(List consumes) { + when(operation.getConsumes()).thenReturn(consumes); + return this; + } + + public OperationBuilder method(String method) { + when(operation.getMethod()).thenReturn(method); + return this; + } + + @SuppressWarnings("unchecked") + public OperationBuilder pattern(String pattern) { + when(operation.getPath()).thenReturn(pattern); + ArgumentCaptor> args = ArgumentCaptor.forClass(Map.class); + when(operation.getPath(args.capture())) + .thenAnswer((Answer) invocationOnMock -> Router.reverse(pattern, args.getValue())); + return this; + } + + public OperationBuilder response(Object body, StatusCode code, String contentType) { + var schemas = ModelConvertersExt.getInstance().read(body.getClass()); + var mediaType = new io.swagger.v3.oas.models.media.MediaType(); + mediaType.schema(schemas.get(body.getClass().getSimpleName())); + + var content = new Content(); + content.addMediaType(contentType, mediaType); + + ResponseExt response = mock(ResponseExt.class); + when(response.getContent()).thenReturn(content); + when(response.getCode()).thenReturn(Integer.toString(code.value())); + + if (responses == null) { + responses = mock(ApiResponses.class); + when(operation.getResponses()).thenReturn(responses); + when(operation.getDefaultResponse()).thenReturn(response); + } + when(responses.get(Integer.toString(code.value()))).thenReturn(response); + return this; + } + + public OperationBuilder defaultResponse() { + return response(Map.of(), StatusCode.OK, "application/json"); + } + + public OperationBuilder body(Object body, String contentType) { + consumes(contentType); + var schemas = ModelConverters.getInstance().read(body.getClass()); + var mediaType = new io.swagger.v3.oas.models.media.MediaType(); + mediaType.schema(schemas.get(body.getClass().getSimpleName())); + + var content = new Content(); + content.addMediaType(contentType, mediaType); + + var requestBodyExt = mock(RequestBodyExt.class); + when(requestBodyExt.getContent()).thenReturn(content); + when(requestBodyExt.getJavaType()).thenReturn(body.getClass().getName()); + when(operation.getRequestBody()).thenReturn(requestBodyExt); + return this; + } + + public OperationExt build() { + return operation; + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/Issue1582.java b/modules/jooby-openapi/src/test/java/issues/Issue1582.java index b6a4f04eb8..9b4cbf04ab 100644 --- a/modules/jooby-openapi/src/test/java/issues/Issue1582.java +++ b/modules/jooby-openapi/src/test/java/issues/Issue1582.java @@ -12,6 +12,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -24,35 +26,36 @@ public class Issue1582 { @Test public void shouldGenerateOnOneLvelPackageLocation() throws IOException { - Path output = export("com.App"); - assertTrue(Files.exists(output)); - assertEquals(outDir.resolve("com").resolve("App.yaml"), output); + var output = export("com.App"); + output.forEach(it -> assertTrue(Files.exists(it))); + assertEquals(List.of(outDir.resolve("com").resolve("App.yaml")), output); } @Test public void shouldGenerateOnPackageLocation() throws IOException { - Path output = export("com.myapp.App"); - assertTrue(Files.exists(output)); - assertEquals(outDir.resolve("com").resolve("myapp").resolve("App.yaml"), output); + var output = export("com.myapp.App"); + output.forEach(it -> assertTrue(Files.exists(it))); + assertEquals(List.of(outDir.resolve("com").resolve("myapp").resolve("App.yaml")), output); } @Test public void shouldGenerateOnDeepPackageLocation() throws IOException { - Path output = export("com.foo.bar.app.App"); - assertTrue(Files.exists(output)); + var output = export("com.foo.bar.app.App"); + output.forEach(it -> assertTrue(Files.exists(it))); assertEquals( - outDir.resolve("com").resolve("foo").resolve("bar").resolve("app").resolve("App.yaml"), + List.of( + outDir.resolve("com").resolve("foo").resolve("bar").resolve("app").resolve("App.yaml")), output); } @Test public void shouldGenerateOnRootLocation() throws IOException { - Path output = export("App"); - assertTrue(Files.exists(output)); - assertEquals(outDir.resolve("App.yaml"), output); + var output = export("App"); + output.forEach(it -> assertTrue(Files.exists(it))); + assertEquals(List.of(outDir.resolve("App.yaml")), output); } - private Path export(String source) throws IOException { + private List export(String source) throws IOException { Info info = new Info(); info.setTitle("API"); info.setVersion("1.0"); @@ -64,6 +67,6 @@ private Path export(String source) throws IOException { OpenAPIGenerator generator = new OpenAPIGenerator(); generator.setOutputDir(outDir); - return generator.export(openAPI, OpenAPIGenerator.Format.YAML); + return generator.export(openAPI, OpenAPIGenerator.Format.YAML, Map.of()); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java index 8260168217..d8bc609cc7 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java @@ -5,8 +5,10 @@ */ package issues.i3729.api; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import io.jooby.openapi.CurrentDir; import io.jooby.openapi.OpenAPIResult; import io.jooby.openapi.OpenAPITest; @@ -17,6 +19,385 @@ public void shouldGenerateMvcDoc(OpenAPIResult result) { checkResult(result); } + @OpenAPITest(value = AppDemoLibrary.class) + public void shouldGenerateGoodDoc(OpenAPIResult result) { + assertThat(result.toYaml()) + .isEqualToIgnoringNewLines( + """ + openapi: 3.0.1 + info: + title: DemoLibrary API + description: DemoLibrary API description + version: "1.0" + paths: + /library/books/{isbn}: + summary: The Public Front Desk of the library. + get: + tags: + - Library + summary: Get Specific Book Details + description: View the full information for a single specific book using its + unique ISBN. + operationId: getBook + parameters: + - name: isbn + in: path + description: "The unique ID from the URL (e.g., /books/978-3-16-148410-0)" + required: true + schema: + type: string + responses: + "200": + description: The book data + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + "404": + description: "Not Found: error if it doesn't exist." + /library/search: + summary: The Public Front Desk of the library. + get: + tags: + - Library + summary: Quick Search + description: "Find books by a partial title (e.g., searching \\"Harry\\" finds\\ + \\ \\"Harry Potter\\")." + operationId: searchBooks + parameters: + - name: q + in: query + description: The word or phrase to search for. + schema: + type: string + responses: + "200": + description: A list of books matching that term. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Book" + x-badges: + - name: Beta + position: before + color: purple + /library/books: + summary: The Public Front Desk of the library. + get: + tags: + - Library + summary: Browse Books (Paginated) + description: "Look up a specific book title where there might be many editions\\ + \\ or copies, splitting the results into manageable pages." + operationId: getBooksByTitle + parameters: + - name: title + in: query + description: The exact book title to filter by. + schema: + type: string + - name: page + in: query + description: Which page number to load (defaults to 1). + required: true + schema: + type: integer + format: int32 + - name: size + in: query + description: How many books to show per page (defaults to 20). + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: "A \\"Page\\" object containing the books and info like \\"Total\\ + \\ Pages: 5\\"." + content: + application/json: + schema: + type: object + properties: + content: + type: array + items: + $ref: "#/components/schemas/Book" + numberOfElements: + type: integer + format: int32 + totalElements: + type: integer + format: int64 + totalPages: + type: integer + format: int64 + pageRequest: + type: object + properties: + page: + type: integer + format: int64 + size: + type: integer + format: int32 + nextPageRequest: + type: object + properties: + page: + type: integer + format: int64 + size: + type: integer + format: int32 + previousPageRequest: + type: object + properties: + page: + type: integer + format: int64 + size: + type: integer + format: int32 + post: + tags: + - Inventory + summary: Add New Book + description: Register a new book in the system. + operationId: addBook + requestBody: + description: New book to add. + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + required: true + responses: + "200": + description: A text message confirming success. + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + components: + schemas: + Address: + type: object + properties: + street: + type: string + description: Street name. + city: + type: string + description: City name. + state: + type: string + description: State. + country: + type: string + description: Two digit country code. + description: Author address. + Book: + type: object + properties: + isbn: + type: string + description: Book ISBN. Method. + title: + type: string + description: Book's title. + publicationDate: + type: string + description: Publication date. Format mm-dd-yyyy. + format: date + text: + type: string + description: Book's content. + type: + type: string + description: |- + Book type. + - Fiction: Fiction books are based on imaginary characters and events, while non-fiction books are based o n real people and events. + - NonFiction: Non-fiction genres include biography, autobiography, history, self-help, and true crime. + enum: + - Fiction + - NonFiction + authors: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/Author" + image: + type: string + format: binary + description: Book model. + Author: + type: object + properties: + ssn: + type: string + description: Social security number. + name: + type: string + description: Author's name. + address: + $ref: "#/components/schemas/Address" + books: + uniqueItems: true + type: array + description: Published books. + items: + $ref: "#/components/schemas/Book"\ + """); + } + + @OpenAPITest(value = AppLibrary.class) + public void shouldGenerateAdoc(OpenAPIResult result) { + assertThat( + result.toAsciiDoc( + CurrentDir.basedir("src", "test", "resources", "adoc", "library.adoc"))) + .isEqualToIgnoringNewLines( + """ + = Library API. + Jooby Doc; + :doctype: book + :icons: font + :source-highlighter: highlightjs + :toc: left + :toclevels: 4 + :sectlinks: + + == Introduction + + Available data: Books and authors. + + == Support + + Write your questions at support@jooby.io + + [[overview_operations]] + == Operations + + === List Books + + Query books. By using advanced filters. + + Example: `/api/library?title=...` + + ==== Request Fields + + [cols="1,1,3", options="header"] + |=== + |Name|Type|Description + |`+title+` + |`+string+` + |Book's title. + + |`+author+` + |`+string+` + |Book's author. Optional. + + |`+isbn+` + |`+array+` + |Book's isbn. Optional. + + |=== + + === Find a book by ISBN + + [source] + ---- + curl -i\\ + -H 'Accept: application/json'\\ + -X GET '/api/library/{isbn}' + ---- + + .GET /api/library/{isbn} + [source, json] + ---- + { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "state" : "string", + "country" : "string" + }, + "books" : [ { } ] + } ], + "image" : "binary" + } + ---- + + .GET /api/library/{isbn} + [source, json] + ---- + { + "message" : "Bad Request: For bad ISBN code.", + "reason" : "Bad Request", + "statusCode" : 400 + } + ---- + + .GET /api/library/{isbn} + [source, json] + ---- + { + "message" : "Not Found: If a book doesn't exist.", + "reason" : "Not Found", + "statusCode" : 404 + } + ---- + + ==== Response Fields + + [cols="1,1,3a", options="header"] + |=== + |Name|Type|Description + |`+isbn+` + |`+string+` + |Book ISBN. Method. + + |`+title+` + |`+string+` + |Book's title. + + |`+publicationDate+` + |`+date+` + |Publication date. Format mm-dd-yyyy. + + |`+text+` + |`+string+` + |Book's content. + + |`+type+` + |`+string+` + |Books can be broadly categorized into fiction and non-fiction. + + * *Fiction*: Fiction books are based on imaginary characters and events, while non-fiction books are based o n real people and events. + * *NonFiction*: Non-fiction genres include biography, autobiography, history, self-help, and true crime. + + |`+authors+` + |`+array+` + | + + |`+image+` + |`+binary+` + | + + |===\ + """); + } + @OpenAPITest(value = ScriptLibrary.class) public void shouldGenerateScriptDoc(OpenAPIResult result) { checkResult(result); @@ -24,227 +405,233 @@ public void shouldGenerateScriptDoc(OpenAPIResult result) { private void checkResult(OpenAPIResult result) { assertEquals( - "openapi: 3.0.1\n" - + "info:\n" - + " title: Library API.\n" - + " description: \"Available data: Books and authors.\"\n" - + " contact:\n" - + " name: Jooby\n" - + " url: https://jooby.io\n" - + " email: support@jooby.io\n" - + " license:\n" - + " name: Apache\n" - + " url: https://jooby.io/LICENSE\n" - + " version: 4.0.0\n" - + " x-logo:\n" - + " url: https://redocly.github.io/redoc/museum-logo.png\n" - + " altText: Museum logo\n" - + "servers:\n" - + "- url: https://api.fake-museum-example.com/v1\n" - + "tags:\n" - + "- name: Library\n" - + " description: Access to all books.\n" - + "- name: Author\n" - + " description: Oxxx\n" - + "paths:\n" - + " /api/library/{isbn}:\n" - + " summary: Library API.\n" - + " description: \"Contains all operations for creating, updating and fetching" - + " books.\"\n" - + " get:\n" - + " tags:\n" - + " - Library\n" - + " - Book\n" - + " - Author\n" - + " summary: Find a book by isbn.\n" - + " operationId: bookByIsbn\n" - + " parameters:\n" - + " - name: isbn\n" - + " in: path\n" - + " description: Book isbn. Like IK-1900.\n" - + " required: true\n" - + " schema:\n" - + " type: string\n" - + " responses:\n" - + " \"200\":\n" - + " description: A matching book.\n" - + " content:\n" - + " application/json:\n" - + " schema:\n" - + " $ref: \"#/components/schemas/Book\"\n" - + " \"404\":\n" - + " description: \"Not Found: If a book doesn't exist.\"\n" - + " \"400\":\n" - + " description: \"Bad Request: For bad ISBN code.\"\n" - + " /api/library/{id}:\n" - + " summary: Library API.\n" - + " description: \"Contains all operations for creating, updating and fetching" - + " books.\"\n" - + " get:\n" - + " tags:\n" - + " - Library\n" - + " - Author\n" - + " summary: Author by Id.\n" - + " operationId: author\n" - + " parameters:\n" - + " - name: id\n" - + " in: path\n" - + " description: ID.\n" - + " required: true\n" - + " schema:\n" - + " type: string\n" - + " responses:\n" - + " \"200\":\n" - + " description: An author\n" - + " content:\n" - + " application/json:\n" - + " schema:\n" - + " $ref: \"#/components/schemas/Author\"\n" - + " /api/library:\n" - + " summary: Library API.\n" - + " description: \"Contains all operations for creating, updating and fetching" - + " books.\"\n" - + " get:\n" - + " tags:\n" - + " - Library\n" - + " summary: Query books.\n" - + " operationId: query\n" - + " parameters:\n" - + " - name: title\n" - + " in: query\n" - + " description: Book's title.\n" - + " schema:\n" - + " type: string\n" - + " - name: author\n" - + " in: query\n" - + " description: Book's author. Optional.\n" - + " schema:\n" - + " type: string\n" - + " - name: isbn\n" - + " in: query\n" - + " description: Book's isbn. Optional.\n" - + " schema:\n" - + " type: string\n" - + " responses:\n" - + " \"200\":\n" - + " description: Matching books.\n" - + " content:\n" - + " application/json:\n" - + " schema:\n" - + " type: array\n" - + " items:\n" - + " $ref: \"#/components/schemas/Book\"\n" - + " x-badges:\n" - + " - name: Beta\n" - + " position: before\n" - + " color: purple\n" - + " post:\n" - + " tags:\n" - + " - Library\n" - + " - Author\n" - + " summary: Creates a new book.\n" - + " description: Book can be created or updated.\n" - + " operationId: createBook\n" - + " requestBody:\n" - + " description: Book to create.\n" - + " content:\n" - + " application/json:\n" - + " schema:\n" - + " $ref: \"#/components/schemas/Book\"\n" - + " example:\n" - + " isbn: X01981\n" - + " title: HarryPotter\n" - + " required: true\n" - + " responses:\n" - + " \"200\":\n" - + " description: Saved book.\n" - + " content:\n" - + " application/json:\n" - + " schema:\n" - + " $ref: \"#/components/schemas/Book\"\n" - + " example:\n" - + " id: generatedId\n" - + " isbn: '...'\n" - + "components:\n" - + " schemas:\n" - + " Author:\n" - + " type: object\n" - + " properties:\n" - + " ssn:\n" - + " type: string\n" - + " description: Social security number.\n" - + " name:\n" - + " type: string\n" - + " description: Author's name.\n" - + " address:\n" - + " $ref: \"#/components/schemas/Address\"\n" - + " books:\n" - + " uniqueItems: true\n" - + " type: array\n" - + " description: Published books.\n" - + " items:\n" - + " $ref: \"#/components/schemas/Book\"\n" - + " BookQuery:\n" - + " type: object\n" - + " properties:\n" - + " title:\n" - + " type: string\n" - + " description: Book's title.\n" - + " author:\n" - + " type: string\n" - + " description: Book's author. Optional.\n" - + " isbn:\n" - + " type: string\n" - + " description: Book's isbn. Optional.\n" - + " description: Query books by complex filters.\n" - + " Address:\n" - + " type: object\n" - + " properties:\n" - + " street:\n" - + " type: string\n" - + " description: Street name.\n" - + " city:\n" - + " type: string\n" - + " description: City name.\n" - + " state:\n" - + " type: string\n" - + " description: State.\n" - + " country:\n" - + " type: string\n" - + " description: Two digit country code.\n" - + " description: Author address.\n" - + " Book:\n" - + " type: object\n" - + " properties:\n" - + " isbn:\n" - + " type: string\n" - + " description: Book ISBN. Method.\n" - + " title:\n" - + " type: string\n" - + " description: Book's title.\n" - + " publicationDate:\n" - + " type: string\n" - + " description: Publication date. Format mm-dd-yyyy.\n" - + " format: date\n" - + " text:\n" - + " type: string\n" - + " type:\n" - + " type: string\n" - + " description: |-\n" - + " Book type.\n" - + " - Fiction: Fiction books are based on imaginary characters and events," - + " while non-fiction books are based o n real people and events.\n" - + " - NonFiction: Non-fiction genres include biography, autobiography," - + " history, self-help, and true crime.\n" - + " enum:\n" - + " - Fiction\n" - + " - NonFiction\n" - + " authors:\n" - + " uniqueItems: true\n" - + " type: array\n" - + " items:\n" - + " $ref: \"#/components/schemas/Author\"\n" - + " description: Book model.\n", + """ + openapi: 3.0.1 + info: + title: Library API. + description: "Available data: Books and authors." + contact: + name: Jooby + url: https://jooby.io + email: support@jooby.io + license: + name: Apache + url: https://jooby.io/LICENSE + version: 4.0.0 + x-logo: + url: https://redocly.github.io/redoc/museum-logo.png + altText: Museum logo + servers: + - url: https://api.fake-museum-example.com/v1 + tags: + - name: Library + description: Access to all books. + - name: Author + description: Oxxx + paths: + /api/library/{isbn}: + summary: Library API. + description: "Contains all operations for creating, updating and fetching books." + get: + tags: + - Library + - Book + - Author + summary: Find a book by isbn. + operationId: bookByIsbn + parameters: + - name: isbn + in: path + description: Book isbn. Like IK-1900. + required: true + schema: + type: string + responses: + "200": + description: A matching book. + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + "404": + description: "Not Found: If a book doesn't exist." + "400": + description: "Bad Request: For bad ISBN code." + /api/library/author/{id}: + summary: Library API. + description: "Contains all operations for creating, updating and fetching books." + get: + tags: + - Library + - Author + summary: Author by Id. + operationId: author + parameters: + - name: id + in: path + description: Author ID. + required: true + schema: + type: string + responses: + "200": + description: An author + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + /api/library: + summary: Library API. + description: "Contains all operations for creating, updating and fetching books." + get: + tags: + - Library + summary: Query books. + description: By using advanced filters. + operationId: query + parameters: + - name: title + in: query + description: Book's title. + schema: + type: string + - name: author + in: query + description: Book's author. Optional. + schema: + type: string + - name: isbn + in: query + description: Book's isbn. Optional. + schema: + type: array + items: + type: string + responses: + "200": + description: Matching books. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Book" + x-badges: + - name: Beta + position: before + color: purple + post: + tags: + - Library + - Author + summary: Creates a new book. + description: Book can be created or updated. + operationId: createBook + requestBody: + description: Book to create. + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + example: + isbn: X01981 + title: HarryPotter + required: true + responses: + "200": + description: Saved book. + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + example: + id: generatedId + isbn: '...' + components: + schemas: + Author: + type: object + properties: + ssn: + type: string + description: Social security number. + name: + type: string + description: Author's name. + address: + $ref: "#/components/schemas/Address" + books: + uniqueItems: true + type: array + description: Published books. + items: + $ref: "#/components/schemas/Book" + BookQuery: + type: object + properties: + title: + type: string + description: Book's title. + author: + type: string + description: Book's author. Optional. + isbn: + type: array + description: Book's isbn. Optional. + items: + type: string + description: Query books by complex filters. + Address: + type: object + properties: + street: + type: string + description: Street name. + city: + type: string + description: City name. + state: + type: string + description: State. + country: + type: string + description: Two digit country code. + description: Author address. + Book: + type: object + properties: + isbn: + type: string + description: Book ISBN. Method. + title: + type: string + description: Book's title. + publicationDate: + type: string + description: Publication date. Format mm-dd-yyyy. + format: date + text: + type: string + description: Book's content. + type: + type: string + description: |- + Book type. + - Fiction: Fiction books are based on imaginary characters and events, while non-fiction books are based o n real people and events. + - NonFiction: Non-fiction genres include biography, autobiography, history, self-help, and true crime. + enum: + - Fiction + - NonFiction + authors: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/Author" + image: + type: string + format: binary + description: Book model. + """, result.toYaml()); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/AppDemoLibrary.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/AppDemoLibrary.java new file mode 100644 index 0000000000..16e2158d6f --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/AppDemoLibrary.java @@ -0,0 +1,17 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3729.api; + +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + +import io.jooby.Jooby; + +public class AppDemoLibrary extends Jooby { + + { + mvc(toMvcExtension(LibraryDemoApi.class)); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/Book.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/Book.java index bfc4c2bc3e..7104e464b2 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3729/api/Book.java +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/Book.java @@ -8,6 +8,8 @@ import java.time.LocalDate; import java.util.Set; +import io.jooby.FileUpload; + /** Book model. */ public class Book { /** Book ISBN. */ @@ -19,6 +21,7 @@ public class Book { /** Publication date. Format mm-dd-yyyy. */ LocalDate publicationDate; + /** Book's content. */ String text; /** Book type. */ @@ -26,6 +29,8 @@ public class Book { Set authors; + FileUpload image; + /** * Book ISBN. Method. * @@ -78,4 +83,12 @@ public Set getAuthors() { public void setAuthors(Set authors) { this.authors = authors; } + + public FileUpload getImage() { + return image; + } + + public void setImage(FileUpload image) { + this.image = image; + } } diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/BookError.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/BookError.java new file mode 100644 index 0000000000..0181bf5d54 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/BookError.java @@ -0,0 +1,41 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3729.api; + +/** Book model. */ +public class BookError { + /** Book resource path. */ + private String path; + + /** Book's error message. */ + private String message; + + private int code; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/BookQuery.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/BookQuery.java index 51cc1f1493..e03f0c814c 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3729/api/BookQuery.java +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/BookQuery.java @@ -5,6 +5,8 @@ */ package issues.i3729.api; +import java.util.List; + /** * Query books by complex filters. * @@ -12,4 +14,4 @@ * @param author Book's author. Optional. * @param isbn Book's isbn. Optional. */ -public record BookQuery(String title, String author, String isbn) {} +public record BookQuery(String title, String author, List isbn) {} diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/BookType.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/BookType.java new file mode 100644 index 0000000000..5808ec8d6e --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/BookType.java @@ -0,0 +1,49 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3729.api; + +/** Defines the format and release schedule of the item. */ +public enum BookType { + /** + * A fictional narrative story. + * + *

Examples: "Pride and Prejudice", "Harry Potter", "Dune". These are creative works meant for + * entertainment or artistic expression. + */ + NOVEL, + + /** + * A written account of a real person's life. + * + *

Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". These are + * non-fiction historical records of an individual. + */ + BIOGRAPHY, + + /** + * An educational book used for study. + * + *

Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". These are + * designed for students and are often used as reference material in academic courses. + */ + TEXTBOOK, + + /** + * A periodical publication intended for general readers. + * + *

Examples: Time, National Geographic, Vogue. These contain various articles, are published + * frequently (weekly/monthly), and often have a glossy format. + */ + MAGAZINE, + + /** + * A scholarly or professional publication. + * + *

Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic + * research or trade news and are written by experts for other experts. + */ + JOURNAL +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryApi.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryApi.java index f9e6deb147..93340cfa08 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryApi.java +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryApi.java @@ -20,6 +20,7 @@ * @tag.description Access to all books. */ @Path("/api/library") +@Produces("application/json") public class LibraryApi { /** @@ -40,17 +41,17 @@ public Book bookByIsbn(@PathParam String isbn) throws NotFoundException, BadRequ /** * Author by Id. * - * @param id ID. + * @param id Author ID. * @return An author * @tag Author. Oxxx */ - @GET("/{id}") + @GET("/author/{id}") public Author author(@PathParam String id) { return new Author(); } /** - * Query books. + * Query books. By using advanced filters. * * @param query Book's param query. * @return Matching books. diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryDemoApi.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryDemoApi.java new file mode 100644 index 0000000000..e4824f10e1 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryDemoApi.java @@ -0,0 +1,100 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3729.api; + +import java.util.List; + +import io.jooby.annotation.*; +import io.jooby.exception.NotFoundException; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.inject.Inject; + +/** The Public Front Desk of the library. */ +@Path("/library") +public class LibraryDemoApi { + + private final LibraryRepo library; + + @Inject + public LibraryDemoApi(LibraryRepo library) { + this.library = library; + } + + /** + * Get Specific Book Details + * + *

View the full information for a single specific book using its unique ISBN. + * + * @param isbn The unique ID from the URL (e.g., /books/978-3-16-148410-0) + * @return The book data + * @throws NotFoundException 404 error if it doesn't exist. + * @tag Library + */ + @GET + @Path("/books/{isbn}") + public Book getBook(@PathParam String isbn) { + return library.findBook(isbn).orElseThrow(() -> new NotFoundException(isbn)); + } + + /** + * Quick Search + * + *

Find books by a partial title (e.g., searching "Harry" finds "Harry Potter"). + * + * @param q The word or phrase to search for. + * @return A list of books matching that term. + * @x-badges [{name:Beta, position:before, color:purple}] + * @tag Library + */ + @GET + @Path("/search") + public List searchBooks(@QueryParam String q) { + var pattern = "%" + (q != null ? q : "") + "%"; + + return library.searchBooks(pattern); + } + + /** + * Browse Books (Paginated) + * + *

Look up a specific book title where there might be many editions or copies, splitting the + * results into manageable pages. + * + * @param title The exact book title to filter by. + * @param page Which page number to load (defaults to 1). + * @param size How many books to show per page (defaults to 20). + * @return A "Page" object containing the books and info like "Total Pages: 5". + * @tag Library + */ + @GET + @Path("/books") + public Page getBooksByTitle( + @QueryParam String title, @QueryParam int page, @QueryParam int size) { + // Ensure we have sensible defaults if the user sends nothing + int pageNum = page > 0 ? page : 1; + int pageSize = size > 0 ? size : 20; + + // Ask the database for just this specific slice of data + return library.findBooksByTitle(title, PageRequest.ofPage(pageNum).size(pageSize)); + } + + /** + * Add New Book + * + *

Register a new book in the system. + * + * @param book New book to add. + * @return A text message confirming success. + * @tag Inventory + */ + @POST + @Path("/books") + public Book addBook(Book book) { + // Save it + return library.add(book); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryRepo.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryRepo.java new file mode 100644 index 0000000000..30d17dd475 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryRepo.java @@ -0,0 +1,87 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3729.api; + +import java.util.List; +import java.util.Optional; + +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.*; + +/** + * The "Librarian" of our system. + * + *

This interface handles all the work of finding, saving, and removing books and authors from + * the database. You don't need to write the code for this; the system builds it automatically based + * on these method names. + */ +@Repository +public interface LibraryRepo { + + // --- Finding Items --- + + /** + * Looks up a single book using its ISBN code. + * + * @param isbn The unique code to look for. + * @return An "Optional" box that contains the book if we found it, or is empty if we didn't. + */ + @Find + Optional findBook(String isbn); + + /** Looks up an author using their ID. */ + @Find + Optional findAuthor(String ssn); + + /** + * Finds books that match a specific title. + * + *

Because there might be thousands of results, this method splits them into "pages". You ask + * for "Page 1" or "Page 5", and it gives you just that chunk. + * + * @param title The exact title to look for. + * @param pageRequest Which page of results do you want? + * @return A page containing a list of books and total count info. + */ + @Find + Page findBooksByTitle(String title, PageRequest pageRequest); + + // --- Custom Searches --- + + /** + * Search for books that have a specific word in the title. + * + *

Example: If you search for "%Harry%", it finds "Harry Potter" and "Dirty Harry". It also + * sorts the results alphabetically by title. + */ + @Query("where title like :pattern order by title") + List searchBooks(String pattern); + + /** + * A custom report that just lists the titles of new books. Useful for creating quick lists + * without loading all the book details. + * + * @param minYear The oldest year we care about (e.g., 2023). + * @return Just the names of the books. + */ + @Query("select title from Book where extract(year from publicationDate) >= :minYear") + List findRecentBookTitles(int minYear); + + // --- Saving & Deleting --- + + /** Registers a new book in the system. */ + @Insert + Book add(Book book); + + /** Saves changes made to an author's details. */ + @Update + void update(Author author); + + /** Permanently removes a book from the library. */ + @Delete + void remove(Book book); +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/ScriptLibrary.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/ScriptLibrary.java index c123db911f..f4d762decf 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3729/api/ScriptLibrary.java +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/ScriptLibrary.java @@ -45,20 +45,20 @@ public class ScriptLibrary extends Jooby { /* * Author by Id. * - * @param id ID. + * @param id Author ID. * @return An author * @tag Author. Oxxx * @operationId author */ get( - "/{id}", + "/author/{id}", ctx -> { var id = ctx.path("id").value(); return new Author(); }); /* - * Query books. + * Query books. By using advanced filters. * * @param query Book's param query. * @return Matching books. diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/App3820a.java b/modules/jooby-openapi/src/test/java/issues/i3820/App3820a.java new file mode 100644 index 0000000000..29925c495e --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/App3820a.java @@ -0,0 +1,19 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820; + +import io.jooby.Jooby; +import issues.i3820.model.Book; + +public class App3820a extends Jooby { + { + post( + "/library/books", + ctx -> { + return ctx.body(Book.class); + }); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/App3820b.java b/modules/jooby-openapi/src/test/java/issues/i3820/App3820b.java new file mode 100644 index 0000000000..6d13fc14fe --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/App3820b.java @@ -0,0 +1,29 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820; + +import java.util.ArrayList; +import java.util.List; + +import io.jooby.Jooby; + +public class App3820b extends Jooby { + { + get( + "/strings", + ctx -> { + List strings = new ArrayList<>(); + return strings; + }); + + get( + "/string", + ctx -> { + String value = ""; + return value; + }); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/Issue3820.java b/modules/jooby-openapi/src/test/java/issues/i3820/Issue3820.java new file mode 100644 index 0000000000..7c7a21d0fc --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/Issue3820.java @@ -0,0 +1,45 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.jooby.openapi.CurrentDir; +import io.jooby.openapi.OpenAPIResult; +import io.jooby.openapi.OpenAPITest; + +public class Issue3820 { + @OpenAPITest(value = App3820a.class) + public void shouldGenerateRequestBodySchema(OpenAPIResult result) { + assertThat(result.toAsciiDoc(CurrentDir.testClass(getClass(), "schema.adoc"))) + .isEqualToIgnoringNewLines( + """ + [source, json] + ---- + { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { + "id" : "int64", + "name" : "string" + }, + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } ] + } + ----\ + """); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/PebbleSupportTest.java b/modules/jooby-openapi/src/test/java/issues/i3820/PebbleSupportTest.java new file mode 100644 index 0000000000..a02643228e --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/PebbleSupportTest.java @@ -0,0 +1,1559 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import io.jooby.internal.openapi.OpenAPIExt; +import io.jooby.internal.openapi.asciidoc.PebbleTemplateSupport; +import io.jooby.openapi.CurrentDir; +import io.jooby.openapi.OpenAPITest; +import io.swagger.v3.core.util.Json31; +import io.swagger.v3.core.util.Yaml31; +import io.swagger.v3.oas.models.SpecVersion; +import issues.i3729.api.AppLibrary; +import issues.i3820.app.AppLib; + +public class PebbleSupportTest { + + @OpenAPITest(value = AppLib.class) + public void routes(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + + templates + .evaluateThat("{{ routes(\"/library/books/?.*\") | table(grid=\"rows\") }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,3a", grid="rows"] + |=== + |Method|Path|Summary + + |`+GET+` + |`+/library/books/{isbn}+` + |Get Specific Book Details + + |`+GET+` + |`+/library/books+` + |Browse Books (Paginated) + + |`+POST+` + |`+/library/books+` + |Add New Book + + |===\ + """); + + // default error map + templates + .evaluateThat("{{ routes }}") + .isEqualTo( + "[GET /library/books/{isbn}, GET /library/search, GET /library/books, POST" + + " /library/books, POST /library/authors]"); + templates + .evaluateThat("{{ routes(\"/library/books/?.*\") }}") + .isEqualTo("[GET /library/books/{isbn}, GET /library/books, POST /library/books]"); + templates.evaluate("{{ routes | json(false) }}", output -> Json31.mapper().readTree(output)); + + templates + .evaluateThat("{{ routes(\"/library/books/?.*\") | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,3a", options="header"] + |=== + |Method|Path|Summary + + |`+GET+` + |`+/library/books/{isbn}+` + |Get Specific Book Details + + |`+GET+` + |`+/library/books+` + |Browse Books (Paginated) + + |`+POST+` + |`+/library/books+` + |Add New Book + + |===\ + """); + + templates + .evaluateThat("{{ routes(\"/library/books/?.*\") | list }}") + .isEqualToIgnoringNewLines( + """ + * `+GET /library/books/{isbn}+`: Get Specific Book Details + * `+GET /library/books+`: Browse Books (Paginated) + * `+POST /library/books+`: Add New Book\ + """); + } + + @OpenAPITest(value = AppLib.class) + public void statusCode(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + // default error map + templates.evaluateThat("{{ statusCode(200) }}").isEqualTo("[{code=200, reason=Success}]"); + + templates + .evaluateThat("{{ statusCode(200) | json }}") + .isEqualToIgnoringNewLines( + """ + [source, json] + ---- + { + "code" : 200, + "reason" : "Success" + } + ----\ + """); + + templates + .evaluateThat("{{ statusCode(200) | list }}") + .isEqualToIgnoringNewLines( + """ + * `+200+`: Success\ + """); + + templates + .evaluateThat("{{ statusCode(200) | table }}") + .isEqualToIgnoringNewLines( + """ + |=== + |code|reason + + |200 + |Success + + |===\ + """); + + templates + .evaluateThat("{{ statusCode([200, 201]) | table }}") + .isEqualToIgnoringNewLines( + """ + |=== + |code|reason + + |200 + |Success + + |201 + |Created + + |===\ + """); + + templates + .evaluateThat("{{ statusCode([200, 201]) | list }}") + .isEqualToIgnoringNewLines( + """ + * `+200+`: Success + * `+201+`: Created\ + """); + + templates + .evaluateThat("{{ statusCode({200: \"OK\", 500: \"Internal Server Error\"}) | list }}") + .isEqualToIgnoringNewLines( + """ + * `+200+`: OK + * `+500+`: Internal Server Error\ + """); + } + + @OpenAPITest(value = AppLibrary.class) + public void bodyBug(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + + templates.evaluateThat("{{ GET(\"/api/library/{isbn}\") | response | body | json(false) }}"); + templates + .evaluateThat("{{ GET(\"/api/library/{isbn}\") | response | body | json(false) }}") + .isEqualToIgnoringNewLines( + """ + { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "state" : "string", + "country" : "string" + }, + "books" : [ { } ] + } ], + "image" : "binary" + }\ + """); + } + + @OpenAPITest(value = AppLib.class, version = SpecVersion.V31) + public void shouldSupportJsonSchemaInV31(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + templates + .evaluateThat("{{ POST(\"/library/books\") | request | body | json(false) }}") + .isEqualToIgnoringNewLines( + """ + { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { + "id" : "int64", + "name" : "string" + }, + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } ] + }\ + """); + } + + @OpenAPITest(value = AppLib.class) + public void errorMap(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + templates + .evaluateThat( + """ + {{ error(400) | json }} + """) + .isEqualToIgnoringNewLines( + """ + [source, json] + ---- + { + "message" : "...", + "reason" : "Bad Request", + "statusCode" : 400 + } + ----\ + """); + // default error map + templates + .evaluateThat( + """ + {{ error(code=400) | json }} + """) + .isEqualToIgnoringNewLines( + """ + [source, json] + ---- + { + "message" : "...", + "reason" : "Bad Request", + "statusCode" : 400 + } + ----\ + """); + + templates + .evaluateThat( + """ + {{ error(code=400) | list }} + """) + .isEqualToIgnoringNewLines( + """ + * message: ... + * reason: Bad Request + * statusCode: 400\ + """); + + templates + .evaluateThat( + """ + {{ error(code=400) | table }} + """) + .isEqualToIgnoringNewLines( + """ + |=== + |message|reason|statusCode + + |... + |Bad Request + |400 + + |===\ + """); + + templates + .evaluateThat( + """ + {%- set error = {"code": 500, "message": "{{code.reason}}", "time": now } -%} + {{ error(code=402) | json }} + """) + .isEqualToIgnoringNewLines( + String.format( + """ + [source, json] + ---- + { + "code" : 402, + "message" : "Payment Required", + "time" : "%s" + } + ----\ + """, + templates.getContext().getNow())); + } + + @OpenAPITest(value = AppLib.class) + public void openApi(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + templates.evaluate( + "{{openapi | json(wrap=false) }}", + output -> { + Json31.mapper().readTree(output); + }); + + templates.evaluate( + "{{GET(\"/library/search\") | json(false)}}", + output -> { + Json31.mapper().readTree(output); + }); + + templates.evaluate( + "{{openapi | yaml}}", + output -> { + Yaml31.mapper().readTree(output); + }); + + templates.evaluate( + "{{GET(\"/library/search\") | yaml}}", + output -> { + Yaml31.mapper().readTree(output); + }); + } + + @OpenAPITest(value = AppLib.class) + public void tags(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + templates + .evaluateThat("{{tag(\"Library\").description }}") + .isEqualToIgnoringNewLines( + "Outlines the available actions in the Library System API. The system is designed to" + + " allow users to search for books, view details, and manage the library" + + " inventory."); + + templates + .evaluateThat( + """ + {% for tag in tags %} + == {{ tag.name }} + + {{ tag.description }} + + // 2. Loop through all routes associated with this tag + {% for route in tag.routes %} + === {{ route.summary }} + + {{ route.description }} + + *URL:* `{{ route.path }}` ({{ route.method }}) + + {% if route.parameters is not empty %} + *Parameters:* + {{ route | parameters | table }} + {% endif %} + + // Only show Request Body if it exists (e.g. for POST/PUT) + {% if route.body is not null %} + *Data Payload:* + {{ route | request | body | json }} + {% endif %} + + // Example response for success + .Response + {{ route | response(200) | json }} + + {% if route.security is not empty %} + .Required Permissions + [cols="1,3"] + |=== + |Type | Scopes + + {# Iterate through security schemes #} + {% for scheme in route.security %} + {% for req in scheme %} + | *{{ req.key }}* | {{ req.value | join(", ") }} + {% endfor %} + {% endfor %} + |=== + + {% endif %} + {% endfor %} + {% endfor %} + """) + .isEqualToIgnoringNewLines( + """ + == Library + Outlines the available actions in the Library System API. The system is designed to allow users to search for books, view details, and manage the library inventory. + // 2. Loop through all routes associated with this tag + === Get Specific Book Details + View the full information for a single specific book using its unique ISBN. + *URL:* `/library/books/{isbn}` (GET) + + *Parameters:* + [cols="1,1,1,3", options="header"] + |=== + |Name|Type|In|Description + |`+isbn+` + |`+string+` + |`+path+` + |The unique ID from the URL (e.g., /books/978-3-16-148410-0) + + |=== + // Only show Request Body if it exists (e.g. for POST/PUT) + + // Example response for success + .Response + [source, json] + ---- + { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { + "id" : "int64", + "name" : "string" + }, + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } ] + } + ---- + .Required Permissions + [cols="1,3"] + |=== + |Type | Scopes + + | *librarySecurity* | read:books|=== + + === Quick Search + Find books by a partial title (e.g., searching "Harry" finds "Harry Potter"). + *URL:* `/library/search` (GET) + + *Parameters:* + [cols="1,1,1,3", options="header"] + |=== + |Name|Type|In|Description + |`+q+` + |`+string+` + |`+query+` + |The word or phrase to search for. + + |=== + // Only show Request Body if it exists (e.g. for POST/PUT) + + // Example response for success + .Response + [source, json] + ---- + [ { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { + "id" : "int64", + "name" : "string" + }, + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } ] + } ] + ---- + .Required Permissions + [cols="1,3"] + |=== + |Type | Scopes + + | *librarySecurity* | read:books|=== + + === Browse Books (Paginated) + Look up a specific book title where there might be many editions or copies, splitting the results into manageable pages. + *URL:* `/library/books` (GET) + + *Parameters:* + [cols="1,1,1,3", options="header"] + |=== + |Name|Type|In|Description + |`+Accept+` + |`+string+` + |`+header+` + | + + |`+title+` + |`+string+` + |`+query+` + |The exact book title to filter by. + + |`+page+` + |`+int32+` + |`+query+` + |Which page number to load (defaults to 1). + + |`+size+` + |`+int32+` + |`+query+` + |How many books to show per page (defaults to 20). + + |=== + // Only show Request Body if it exists (e.g. for POST/PUT) + + // Example response for success + .Response + [source, json] + ---- + { + "content" : [ { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { + "id" : "int64", + "name" : "string" + }, + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } ] + } ], + "numberOfElements" : "int32", + "totalElements" : "int64", + "totalPages" : "int64", + "pageRequest" : { + "page" : "int64", + "size" : "int32" + }, + "nextPageRequest" : { }, + "previousPageRequest" : { } + } + ---- + .Required Permissions + [cols="1,3"] + |=== + |Type | Scopes + + | *librarySecurity* | read:books|=== + + == Inventory + Managing Inventory + // 2. Loop through all routes associated with this tag + === Add New Book + Register a new book in the system. + *URL:* `/library/books` (POST) + + *Parameters:* + [cols="1,1,1,3", options="header"] + |=== + |Name|Type|In|Description + |`+Accept+` + |`+string+` + |`+header+` + | + + |`+Content-Type+` + |`+string+` + |`+header+` + | + + |=== + // Only show Request Body if it exists (e.g. for POST/PUT) + *Data Payload:* + [source, json] + ---- + { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { + "id" : "int64", + "name" : "string" + }, + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } ] + } + ---- + // Example response for success + .Response + [source, json] + ---- + { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { + "id" : "int64", + "name" : "string" + }, + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } ] + } + ---- + .Required Permissions + [cols="1,3"] + |=== + |Type | Scopes + + | *librarySecurity* | write:books|=== + + === Add New Author + + *URL:* `/library/authors` (POST) + + + // Only show Request Body if it exists (e.g. for POST/PUT) + *Data Payload:* + [source, json] + ---- + { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } + ---- + // Example response for success + .Response + [source, json] + ---- + { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } + ---- + .Required Permissions + [cols="1,3"] + |=== + |Type | Scopes + + | *librarySecurity* | write:author + |===\ + """); + } + + @OpenAPITest(value = AppLib.class) + public void server(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + templates.evaluateThat("{{ server(0).url }}").isEqualTo("https://library.jooby.io"); + + templates + .evaluateThat("{{ server(\"Production\").url }}") + .isEqualTo("https://library.jooby.io"); + } + + @OpenAPITest(value = AppLib.class) + public void schema(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + + templates + .evaluateThat("{{schema(\"Book\") | truncate | json}}") + .isEqualToIgnoringNewLines( + """ + [source, json] + ---- + { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { }, + "authors" : [ { } ] + } + ----\ + """); + + templates + .evaluateThat("{{schema(\"Book\") | truncate | yaml(false) }}") + .isEqualToIgnoringNewLines( + """ + isbn: string + title: string + publicationDate: date + text: string + type: string + publisher: {} + authors: + - {}\ + """); + + // example on same schema must generate same output + var output = templates.evaluate("{{schema(\"Book\") | example | json}}"); + assertEquals(output, templates.evaluate("{{schema(\"Book\") | example | json}}")); + + var yamlOutput = templates.evaluate("{{model(\"Book\") | example | yaml}}"); + assertEquals(yamlOutput, templates.evaluate("{{model(\"Book\") | example | yaml}}")); + + templates + .evaluateThat("{{schema(\"Book\") | json(false) }}") + .isEqualToIgnoringNewLines( + """ + { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { + "id" : "int64", + "name" : "string" + }, + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } ] + } + """); + + templates + .evaluateThat("{{schema(\"Address\") | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,3", options="header"] + |=== + |Name|Type|Description + |`+street+` + |`+string+` + |The specific street address. Includes the house number, street name, and apartment number if applicable. Example: "123 Maple Avenue, Apt 4B". + + |`+city+` + |`+string+` + |The town, city, or municipality. Used for grouping authors by location or calculating shipping regions. + + |`+zip+` + |`+string+` + |The postal or zip code. Stored as text (String) rather than a number to support codes that start with zero (e.g., "02138") or contain letters (e.g., "K1A 0B1"). + + |===\ + """); + + templates + .evaluateThat("{{schema(\"Address\") | list }}") + .isEqualToIgnoringNewLines( + """ + street:: + * type: `+string+` + * description: The specific street address. Includes the house number, street name, and apartment number if applicable. Example: "123 Maple Avenue, Apt 4B". + city:: + * type: `+string+` + * description: The town, city, or municipality. Used for grouping authors by location or calculating shipping regions. + zip:: + * type: `+string+` + * description: The postal or zip code. Stored as text (String) rather than a number to support codes that start with zero (e.g., "02138") or contain letters (e.g., "K1A 0B1").\ + """); + + templates + .evaluateThat("{{schema(\"Book.type\") | list }}") + .isEqualToIgnoringNewLines( + """ + *NOVEL*:: + * A fictional narrative story. Examples: "Pride and Prejudice", "Harry Potter", "Dune". These are creative works meant for entertainment or artistic expression. + *BIOGRAPHY*:: + * A written account of a real person's life. Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". These are non-fiction historical records of an individual. + *TEXTBOOK*:: + * An educational book used for study. Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". These are designed for students and are often used as reference material in academic courses. + *MAGAZINE*:: + * A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format. + *JOURNAL*:: + * A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts.\ + """); + templates + .evaluateThat("{{schema(\"Book.type\") | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,3", options="header"] + |=== + |Name|Description + | *NOVEL* + | A fictional narrative story. Examples: "Pride and Prejudice", "Harry Potter", "Dune". These are creative works meant for entertainment or artistic expression. + | *BIOGRAPHY* + | A written account of a real person's life. Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". These are non-fiction historical records of an individual. + | *TEXTBOOK* + | An educational book used for study. Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". These are designed for students and are often used as reference material in academic courses. + | *MAGAZINE* + | A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format. + | *JOURNAL* + | A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts. + |===\ + """); + } + + @OpenAPITest(value = AppLib.class) + public void curl(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + + templates + .evaluateThat("{{POST(\"/library/authors\") | curl(\"-i\", language=\"bash\") }}") + .isEqualToIgnoringNewLines( + """ + [source, bash] + ---- + curl -i\\ + --data-urlencode "ssn=string"\\ + --data-urlencode "name=string"\\ + --data-urlencode "address.street=string"\\ + --data-urlencode "address.city=string"\\ + --data-urlencode "address.zip=string"\\ + -X POST '/library/authors' + ----\ + """); + + templates + .evaluateThat("{{POST(\"/library/authors\") | curl }}") + .isEqualToIgnoringNewLines( + """ + [source] + ---- + curl --data-urlencode "ssn=string"\\ + --data-urlencode "name=string"\\ + --data-urlencode "address.street=string"\\ + --data-urlencode "address.city=string"\\ + --data-urlencode "address.zip=string"\\ + -X POST '/library/authors' + ----\ + """); + + templates + .evaluateThat("{{POST(\"/library/books\") | request | curl }}") + .isEqualToIgnoringNewLines( + """ + [source] + ---- + curl -H 'Accept: application/json'\\ + -H 'Content-Type: application/json'\\ + -d '{"isbn":"string","title":"string","publicationDate":"date","text":"string","type":"string","publisher":{"id":"int64","name":"string"},"authors":[{"ssn":"string","name":"string","address":{"street":"string","city":"string","zip":"string"}}]}'\\ + -X POST '/library/books' + ----\ + """); + + templates + .evaluateThat("{{GET(\"/library/books\") | request | curl }}") + .isEqualToIgnoringNewLines( + """ + [source] + ---- + curl -H 'Accept: application/json'\\ + -X GET '/library/books?title=string&page=int32&size=int32' + ----\ + """); + + templates + .evaluateThat("{{GET(\"/library/books/{isbn}\") | request | curl }}") + .isEqualToIgnoringNewLines( + """ + [source] + ---- + curl -X GET '/library/books/{isbn}' + ----\ + """); + + templates + .evaluateThat("{{GET(\"/library/books/{isbn}\") | request | curl(\"-i\") }}") + .isEqualToIgnoringNewLines( + """ + [source] + ---- + curl -i\\ + -X GET '/library/books/{isbn}' + ----\ + """); + + templates + .evaluateThat( + "{{GET(\"/library/books/{isbn}\") | request | curl(\"-i\", \"-X\", \"POST\") }}") + .isEqualToIgnoringNewLines( + """ + [source] + ---- + curl -i\\ + -X POST '/library/books/{isbn}' + ----\ + """); + + templates + .evaluateThat( + "{{GET(\"/library/books/{isbn}\") | request | curl(\"-i\", \"-X\", \"POST\") }}") + .isEqualToIgnoringNewLines( + """ + [source] + ---- + curl -i\\ + -X POST '/library/books/{isbn}' + ----\ + """); + + templates + .evaluateThat( + "{{GET(\"/library/books\") | request | curl(\"-H\", \"'Accept: application/xml'\") }}") + .isEqualToIgnoringNewLines( + """ + [source] + ---- + curl -H 'Accept: application/xml'\\ + -X GET '/library/books?title=string&page=int32&size=int32' + ----\ + """); + } + + @OpenAPITest(value = AppLib.class) + public void link(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + + templates + .evaluateThat("{{GET(\"/library/books\") | response | link }}") + .isEqualTo("Page[<>]"); + + templates + .evaluateThat("{{GET(\"/library/search\") | response | link }}") + .isEqualTo("<>[]"); + } + + @OpenAPITest(value = App3820b.class) + public void checkPrimitives(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + + templates.evaluateThat("{{GET(\"/strings\") | response | link }}").isEqualTo("string[]"); + + templates + .evaluateThat("{{GET(\"/strings\") | response | json }}") + .isEqualToIgnoringNewLines( + """ + [source, json] + ---- + [ "string" ] + ----\ + """); + + templates + .evaluateThat("{{GET(\"/string\") | response | json }}") + .isEqualToIgnoringNewLines( + """ + [source, json] + ---- + "string" + ----\ + """); + } + + @OpenAPITest(value = AppLib.class) + public void response(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + + templates + .evaluateThat("{{GET(\"/library/books\") | response | json }}") + .isEqualToIgnoringNewLines( + """ + [source, json] + ---- + { + "content" : [ { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { + "id" : "int64", + "name" : "string" + }, + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } ] + } ], + "numberOfElements" : "int32", + "totalElements" : "int64", + "totalPages" : "int64", + "pageRequest" : { + "page" : "int64", + "size" : "int32" + }, + "nextPageRequest" : { }, + "previousPageRequest" : { } + } + ----\ + """); + + templates + .evaluateThat("{{GET(\"/library/search\") | response | json }}") + .isEqualToIgnoringNewLines( + """ + [source, json] + ---- + [ { + "isbn" : "string", + "title" : "string", + "publicationDate" : "date", + "text" : "string", + "type" : "string", + "publisher" : { + "id" : "int64", + "name" : "string" + }, + "authors" : [ { + "ssn" : "string", + "name" : "string", + "address" : { + "street" : "string", + "city" : "string", + "zip" : "string" + } + } ] + } ] + ----\ + """); + + /* Error response code: */ + templates + .evaluateThat("{{GET(\"/library/books/{isbn}\") | response(code=404) | json }}") + .isEqualToIgnoringNewLines( + """ + [source, json] + ---- + { + "message" : "Not Found: error if it doesn't exist.", + "reason" : "Not Found", + "statusCode" : 404 + } + ----\ + """); + + templates + .evaluateThat("{{GET(\"/library/books/{isbn}\") | response(code=404) | http }}") + .isEqualToIgnoringNewLines( + """ + [source,http,options="nowrap"] + ---- + HTTP/1.1 404 Not Found + ----\ + """); + + /* Override default response code: */ + templates + .evaluateThat("{{POST(\"/library/books\") | response(code=201) | http }}") + .isEqualToIgnoringNewLines( + """ + [source,http,options="nowrap"] + ---- + HTTP/1.1 201 Created + Content-Type: application/json + {"isbn":"string","title":"string","publicationDate":"date","text":"string","type":"string","publisher":{"id":"int64","name":"string"},"authors":[{"ssn":"string","name":"string","address":{"street":"string","city":"string","zip":"string"}}]} + ----\ + """); + + /* Default response */ + templates + .evaluateThat("{{POST(\"/library/books\") | response | http }}") + .isEqualToIgnoringNewLines( + """ + [source,http,options="nowrap"] + ---- + HTTP/1.1 200 Success + Content-Type: application/json + {"isbn":"string","title":"string","publicationDate":"date","text":"string","type":"string","publisher":{"id":"int64","name":"string"},"authors":[{"ssn":"string","name":"string","address":{"street":"string","city":"string","zip":"string"}}]} + ----\ + """); + + templates + .evaluateThat("{{POST(\"/library/books\") | response | list }}") + .isEqualToIgnoringNewLines( + """ + isbn:: + * type: `+string+` + * description: The unique "barcode" for this book (ISBN). We use this to identify exactly which book edition we are talking about. + title:: + * type: `+string+` + * description: The name printed on the cover. + publicationDate:: + * type: `+date+` + * description: When this book was released to the public. + text:: + * type: `+string+` + * description: The full story or content of the book. Since this can be very long, we store it in a special way (Large Object) to keep the database fast. + type:: + * type: `+string+` + * description: Defines the format and release schedule of the item. + ** *NOVEL*: A fictional narrative story. Examples: "Pride and Prejudice", "Harry Potter", "Dune". These are creative works meant for entertainment or artistic expression. + ** *BIOGRAPHY*: A written account of a real person's life. Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". These are non-fiction historical records of an individual. + ** *TEXTBOOK*: An educational book used for study. Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". These are designed for students and are often used as reference material in academic courses. + ** *MAGAZINE*: A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format. + ** *JOURNAL*: A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts. + publisher:: + * type: `+object+` + * description: A company that produces and sells books. + authors:: + * type: `+array+` + * description: The list of people who wrote this book.\ + """); + + templates + .evaluateThat("{{POST(\"/library/books\") | response | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,3a", options="header"] + |=== + |Name|Type|Description + |`+isbn+` + |`+string+` + |The unique "barcode" for this book (ISBN). We use this to identify exactly which book edition we are talking about. + + |`+title+` + |`+string+` + |The name printed on the cover. + + |`+publicationDate+` + |`+date+` + |When this book was released to the public. + + |`+text+` + |`+string+` + |The full story or content of the book. Since this can be very long, we store it in a special way (Large Object) to keep the database fast. + + |`+type+` + |`+string+` + |Defines the format and release schedule of the item. + + * *NOVEL*: A fictional narrative story. Examples: "Pride and Prejudice", "Harry Potter", "Dune". These are creative works meant for entertainment or artistic expression. + * *BIOGRAPHY*: A written account of a real person's life. Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". These are non-fiction historical records of an individual. + * *TEXTBOOK*: An educational book used for study. Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". These are designed for students and are often used as reference material in academic courses. + * *MAGAZINE*: A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format. + * *JOURNAL*: A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts. + + |`+publisher+` + |`+object+` + |A company that produces and sells books. + + |`+authors+` + |`+array+` + |The list of people who wrote this book. + + |===\ + """); + } + + @OpenAPITest(value = AppLib.class) + public void request(OpenAPIExt openapi) throws IOException { + var templates = new PebbleTemplateSupport(CurrentDir.testClass(getClass(), "adoc"), openapi); + + templates + .evaluateThat("{{GET(\"/library/books\") | request | path }}") + .isEqualTo("/library/books?title=string&page=int32&size=int32"); + + templates + .evaluateThat("{{GET(\"/library/books\") | request | path(title=\"...\") }}") + .isEqualTo("/library/books?title=..."); + + templates + .evaluateThat("{{GET(\"/library/books\") | request | path(title=\"...\", page=1) }}") + .isEqualTo("/library/books?title=...&page=1"); + + templates + .evaluateThat("{{GET(\"/library/books\") | request | path(title=\"word space\", page=1) }}") + .isEqualTo("/library/books?title=word%20space&page=1"); + + templates + .evaluateThat("{{GET(\"/library/books/{isbn}\") | request | path }}") + .isEqualTo("/library/books/{isbn}"); + + templates + .evaluateThat("{{GET(\"/library/books/{isbn}\") | request | path(isbn=123) }}") + .isEqualTo("/library/books/123"); + + templates + .evaluateThat("{{POST(\"/library/books\") | request | list }}") + .isEqualToIgnoringNewLines( + """ + Accept:: + * type: `+string+` + * in: `+header+` + Content-Type:: + * type: `+string+` + * in: `+header+` + isbn:: + * type: `+string+` + * in: `+body+` + * description: The unique "barcode" for this book (ISBN). We use this to identify exactly which book edition we are talking about. + title:: + * type: `+string+` + * in: `+body+` + * description: The name printed on the cover. + publicationDate:: + * type: `+date+` + * in: `+body+` + * description: When this book was released to the public. + text:: + * type: `+string+` + * in: `+body+` + * description: The full story or content of the book. Since this can be very long, we store it in a special way (Large Object) to keep the database fast. + type:: + * type: `+string+` + * in: `+body+` + * description: Defines the format and release schedule of the item. + ** *NOVEL*: A fictional narrative story. Examples: "Pride and Prejudice", "Harry Potter", "Dune". These are creative works meant for entertainment or artistic expression. + ** *BIOGRAPHY*: A written account of a real person's life. Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". These are non-fiction historical records of an individual. + ** *TEXTBOOK*: An educational book used for study. Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". These are designed for students and are often used as reference material in academic courses. + ** *MAGAZINE*: A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format. + ** *JOURNAL*: A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts. + publisher:: + * type: `+object+` + * in: `+body+` + * description: A company that produces and sells books. + authors:: + * type: `+array+` + * in: `+body+` + * description: The list of people who wrote this book.\ + """); + + templates + .evaluateThat("{{POST(\"/library/books\") | request | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,1,3a", options="header"] + |=== + |Name|Type|In|Description + |`+Accept+` + |`+string+` + |`+header+` + | + + |`+Content-Type+` + |`+string+` + |`+header+` + | + + |`+isbn+` + |`+string+` + |`+body+` + |The unique "barcode" for this book (ISBN). We use this to identify exactly which book edition we are talking about. + + |`+title+` + |`+string+` + |`+body+` + |The name printed on the cover. + + |`+publicationDate+` + |`+date+` + |`+body+` + |When this book was released to the public. + + |`+text+` + |`+string+` + |`+body+` + |The full story or content of the book. Since this can be very long, we store it in a special way (Large Object) to keep the database fast. + + |`+type+` + |`+string+` + |`+body+` + |Defines the format and release schedule of the item. + + * *NOVEL*: A fictional narrative story. Examples: "Pride and Prejudice", "Harry Potter", "Dune". These are creative works meant for entertainment or artistic expression. + * *BIOGRAPHY*: A written account of a real person's life. Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". These are non-fiction historical records of an individual. + * *TEXTBOOK*: An educational book used for study. Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". These are designed for students and are often used as reference material in academic courses. + * *MAGAZINE*: A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format. + * *JOURNAL*: A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts. + + |`+publisher+` + |`+object+` + |`+body+` + |A company that produces and sells books. + + |`+authors+` + |`+array+` + |`+body+` + |The list of people who wrote this book. + + |===\ + """); + + templates + .evaluateThat("{{GET(\"/library/books\") | request | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,1,3", options="header"] + |=== + |Name|Type|In|Description + |`+Accept+` + |`+string+` + |`+header+` + | + + |`+title+` + |`+string+` + |`+query+` + |The exact book title to filter by. + + |`+page+` + |`+int32+` + |`+query+` + |Which page number to load (defaults to 1). + + |`+size+` + |`+int32+` + |`+query+` + |How many books to show per page (defaults to 20). + + |===\ + """); + + templates + .evaluateThat("{{GET(\"/library/books\") | request | parameters(query) | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,3", options="header"] + |=== + |Name|Type|Description + |`+title+` + |`+string+` + |The exact book title to filter by. + + |`+page+` + |`+int32+` + |Which page number to load (defaults to 1). + + |`+size+` + |`+int32+` + |How many books to show per page (defaults to 20). + + |===\ + """); + + templates + .evaluateThat( + "{{GET(\"/library/books\") | request | parameters(query, ['title']) | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,3", options="header"] + |=== + |Name|Type|Description + |`+title+` + |`+string+` + |The exact book title to filter by. + + |===\ + """); + + templates + .evaluateThat("{{GET(\"/library/books\") | request | parameters('path') | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,3", options="header"] + |=== + |Name|Type|Description + |===\ + """); + + templates + .evaluateThat("{{GET(\"/library/books\") | parameters('path') | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,3", options="header"] + |=== + |Name|Type|Description + |===\ + """); + + templates + .evaluateThat("{{GET(\"/library/books\") | parameters(cookie) | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,3", options="header"] + |=== + |Name|Description + |===\ + """); + + templates + .evaluateThat("{{POST(\"/library/books\") | request(body=\"none\") | http }}") + .isEqualToIgnoringNewLines( + """ + [source,http,options="nowrap"] + ---- + POST /library/books HTTP/1.1 + Accept: application/json + Content-Type: application/json + {} + ----\ + """); + + // example on same schema must generate same output + templates + .evaluateThat("{{GET(\"/library/books\") | request }}") + .isEqualTo("GET /library/books"); + + // example on same schema must generate same output + templates + .evaluateThat("{{GET(\"/library/books\") | request | http }}") + .isEqualToIgnoringNewLines( + """ + [source,http,options="nowrap"] + ---- + GET /library/books HTTP/1.1 + Accept: application/json + ----\ + """); + + templates + .evaluateThat("{{POST(\"/library/books\") | request | http }}") + .isEqualToIgnoringNewLines( + """ + [source,http,options="nowrap"] + ---- + POST /library/books HTTP/1.1 + Accept: application/json + Content-Type: application/json + {"isbn":"string","title":"string","publicationDate":"date","text":"string","type":"string","publisher":{"id":"int64","name":"string"},"authors":[{"ssn":"string","name":"string","address":{"street":"string","city":"string","zip":"string"}}]} + ----\ + """); + + templates + .evaluateThat("{{POST(\"/library/books\") | request | parameters(header) | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,3", options="header"] + |=== + |Name|Description + |`+Accept+` + | + + |`+Content-Type+` + | + + |===\ + """); + + templates + .evaluateThat( + "{{POST(\"/library/books\") | request | parameters(header) | table(columns=['name'])" + + " }}") + .isEqualToIgnoringNewLines( + """ + [cols="1", options="header"] + |=== + |Name + |`+Accept+` + + |`+Content-Type+` + + |===\ + """); + + templates + .evaluateThat("{{POST(\"/library/books\") | request | body | table }}") + .isEqualToIgnoringNewLines( + """ + [cols="1,1,3a", options="header"] + |=== + |Name|Type|Description + |`+isbn+` + |`+string+` + |The unique "barcode" for this book (ISBN). We use this to identify exactly which book edition we are talking about. + + |`+title+` + |`+string+` + |The name printed on the cover. + + |`+publicationDate+` + |`+date+` + |When this book was released to the public. + + |`+text+` + |`+string+` + |The full story or content of the book. Since this can be very long, we store it in a special way (Large Object) to keep the database fast. + + |`+type+` + |`+string+` + |Defines the format and release schedule of the item. + + * *NOVEL*: A fictional narrative story. Examples: "Pride and Prejudice", "Harry Potter", "Dune". These are creative works meant for entertainment or artistic expression. + * *BIOGRAPHY*: A written account of a real person's life. Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". These are non-fiction historical records of an individual. + * *TEXTBOOK*: An educational book used for study. Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". These are designed for students and are often used as reference material in academic courses. + * *MAGAZINE*: A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format. + * *JOURNAL*: A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts. + + |`+publisher+` + |`+object+` + |A company that produces and sells books. + + |`+authors+` + |`+array+` + |The list of people who wrote this book. + + |===\ + """); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/app/AppLib.java b/modules/jooby-openapi/src/test/java/issues/i3820/app/AppLib.java new file mode 100644 index 0000000000..309b32a6ce --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/app/AppLib.java @@ -0,0 +1,42 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820.app; + +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + +import io.jooby.Jooby; + +/** + * Library API. + * + *

An imaginary, but delightful Library API for interacting with library services and + * information. Built with love by https://jooby.io. + * + * @version 1.0.0 + * @license.name Apache 2.0 + * @license.url http://www.apache.org/licenses/LICENSE-2.0.html + * @contact.name Jooby Demo + * @contact.url https://jooby.io + * @contact.email support@jooby.io + * @server.url https://library.jooby.io + * @server.description Production + * @securityScheme.name librarySecurity + * @securityScheme.type apiKey + * @securityScheme.in header + * @securityScheme.paramName X-Auth + * @securityScheme.flows.implicit.authorizationUrl https://library.jooby.io/auth + * @securityScheme.flows.implicit.scopes.name [write:books, read:books, write:author] + * @securityScheme.flows.implicit.scopes.description [modify books in your account, read books] + * @x-logo.url https://redoredocly.github.io/redoc/museum-logo.png + * @tag Library. Outlines the available actions in the Library System API. The system is designed to + * allow users to search for books, view details, and manage the library inventory. + * @tag Inventory. Managing Inventory + */ +public class AppLib extends Jooby { + { + mvc(toMvcExtension(LibApi.class)); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/app/LibApi.java b/modules/jooby-openapi/src/test/java/issues/i3820/app/LibApi.java new file mode 100644 index 0000000000..d0ca32dd39 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/app/LibApi.java @@ -0,0 +1,124 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820.app; + +import java.util.List; + +import io.jooby.annotation.*; +import io.jooby.exception.NotFoundException; +import issues.i3820.model.Author; +import issues.i3820.model.Book; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.inject.Inject; + +/** The Public Front Desk of the library. */ +@Path("/library") +public class LibApi { + + private final Library library; + + @Inject + public LibApi(Library library) { + this.library = library; + } + + /** + * Get Specific Book Details + * + *

View the full information for a single specific book using its unique ISBN. + * + * @param isbn The unique ID from the URL (e.g., /books/978-3-16-148410-0) + * @return The book data + * @throws NotFoundException 404 error if it doesn't exist. + * @tag Library + * @securityRequirement librarySecurity read:books + */ + @GET + @Path("/books/{isbn}") + public Book getBook(@PathParam String isbn) { + return library.findBook(isbn).orElseThrow(() -> new NotFoundException(isbn)); + } + + /** + * Quick Search + * + *

Find books by a partial title (e.g., searching "Harry" finds "Harry Potter"). + * + * @param q The word or phrase to search for. + * @return A list of books matching that term. + * @x-badges [{name:Beta, position:before, color:purple}] + * @tag Library + * @securityRequirement librarySecurity read:books + */ + @GET + @Path("/search") + public List searchBooks(@QueryParam String q) { + var pattern = "%" + (q != null ? q : "") + "%"; + + return library.searchBooks(pattern); + } + + /** + * Browse Books (Paginated) + * + *

Look up a specific book title where there might be many editions or copies, splitting the + * results into manageable pages. + * + * @param title The exact book title to filter by. + * @param page Which page number to load (defaults to 1). + * @param size How many books to show per page (defaults to 20). + * @return A "Page" object containing the books and info like "Total Pages: 5". + * @tag Library + * @securityRequirement librarySecurity read:books + */ + @GET + @Path("/books") + @Produces("application/json") + public Page getBooksByTitle( + @QueryParam String title, @QueryParam int page, @QueryParam int size) { + // Ensure we have sensible defaults if the user sends nothing + int pageNum = page > 0 ? page : 1; + int pageSize = size > 0 ? size : 20; + + // Ask the database for just this specific slice of data + return library.findBooksByTitle(title, PageRequest.ofPage(pageNum).size(pageSize)); + } + + /** + * Add New Book + * + *

Register a new book in the system. + * + * @param book New book to add. + * @return A text message confirming success. + * @tag Inventory + * @securityRequirement librarySecurity write:books + */ + @POST + @Path("/books") + @Consumes("application/json") + @Produces("application/json") + public Book addBook(Book book) { + // Save it + return library.add(book); + } + + /** + * Add New Author + * + * @param author New author to add. + * @return Created author. + * @tag Inventory + * @securityRequirement librarySecurity write:author + */ + @POST + @Path("/authors") + public Author addAuthor(@FormParam Author author) { + // Save it + return author; + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/app/Library.java b/modules/jooby-openapi/src/test/java/issues/i3820/app/Library.java new file mode 100644 index 0000000000..3d4fa9ee52 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/app/Library.java @@ -0,0 +1,87 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820.app; + +import java.util.List; +import java.util.Optional; + +import issues.i3820.model.*; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.*; + +/** + * The "Librarian" of our system. + * + *

This interface handles all the work of finding, saving, and removing books and authors from + * the database. You don't need to write the code for this; the system builds it automatically based + * on these method names. + */ +public interface Library { + + // --- Finding Items --- + + /** + * Looks up a single book using its ISBN code. + * + * @param isbn The unique code to look for. + * @return An "Optional" box that contains the book if we found it, or is empty if we didn't. + */ + @Find + Optional findBook(String isbn); + + /** Looks up an author using their ID. */ + @Find + Optional findAuthor(String ssn); + + /** + * Finds books that match a specific title. + * + *

Because there might be thousands of results, this method splits them into "pages". You ask + * for "Page 1" or "Page 5", and it gives you just that chunk. + * + * @param title The exact title to look for. + * @param pageRequest Which page of results do you want? + * @return A page containing a list of books and total count info. + */ + @Find + Page findBooksByTitle(String title, PageRequest pageRequest); + + // --- Custom Searches --- + + /** + * Search for books that have a specific word in the title. + * + *

Example: If you search for "%Harry%", it finds "Harry Potter" and "Dirty Harry". It also + * sorts the results alphabetically by title. + */ + @Query("where title like :pattern order by title") + List searchBooks(String pattern); + + /** + * A custom report that just lists the titles of new books. Useful for creating quick lists + * without loading all the book details. + * + * @param minYear The oldest year we care about (e.g., 2023). + * @return Just the names of the books. + */ + @Query("select title from Book where extract(year from publicationDate) >= :minYear") + List findRecentBookTitles(int minYear); + + // --- Saving & Deleting --- + + /** Registers a new book in the system. */ + @Insert + Book add(Book book); + + /** Saves changes made to an author's details. */ + @Update + void update(Author author); + + /** Permanently removes a book from the library. */ + @Delete + void remove(Book book); +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/model/Address.java b/modules/jooby-openapi/src/test/java/issues/i3820/model/Address.java new file mode 100644 index 0000000000..c0a010fca8 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/model/Address.java @@ -0,0 +1,35 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820.model; + +/** + * A reusable way to store address details (Street, City, Zip). We can reuse this on Authors, + * Publishers, or Users. + */ +public class Address { + /** + * The specific street address. + * + *

Includes the house number, street name, and apartment number if applicable. Example: "123 + * Maple Avenue, Apt 4B". + */ + public String street; + + /** + * The town, city, or municipality. + * + *

Used for grouping authors by location or calculating shipping regions. + */ + public String city; + + /** + * The postal or zip code. + * + *

Stored as text (String) rather than a number to support codes that start with zero (e.g., + * "02138") or contain letters (e.g., "K1A 0B1"). + */ + public String zip; +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/model/Author.java b/modules/jooby-openapi/src/test/java/issues/i3820/model/Author.java new file mode 100644 index 0000000000..2d5d4a25ec --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/model/Author.java @@ -0,0 +1,35 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820.model; + +import java.util.HashSet; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** A person who writes books. */ +public class Author { + + /** The author's unique government ID (SSN). */ + public String ssn; + + /** The full name of the author. */ + public String name; + + /** + * Where the author lives. This information is stored inside the Author table, not a separate one. + */ + public Address address; + + @JsonIgnore public Set books = new HashSet<>(); + + public Author() {} + + public Author(String ssn, String name) { + this.ssn = ssn; + this.name = name; + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/model/Book.java b/modules/jooby-openapi/src/test/java/issues/i3820/model/Book.java new file mode 100644 index 0000000000..3d9a7c3038 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/model/Book.java @@ -0,0 +1,118 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820.model; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +/** + * Represents a physical Book in our library. + * + *

This is the main item visitors look for. It holds details like the title, the actual text + * content, and who published it. + */ +public class Book { + + /** + * The unique "barcode" for this book (ISBN). We use this to identify exactly which book edition + * we are talking about. + */ + private String isbn; + + /** The name printed on the cover. */ + private String title; + + /** When this book was released to the public. */ + private LocalDate publicationDate; + + /** + * The full story or content of the book. + * + *

Since this can be very long, we store it in a special way (Large Object) to keep the + * database fast. + */ + private String text; + + /** Categorizes the item (e.g., is it a regular Book or a Magazine?). */ + private BookType type; + + /** + * The company that published this book. + * + *

Performance Note: We only load this information if you specifically ask for it ("Lazy"), + * which saves memory. + */ + private Publisher publisher; + + /** The list of people who wrote this book. */ + private Set authors = new HashSet<>(); + + public Book() {} + + public Book(String isbn, String title, BookType type) { + this.isbn = isbn; + this.title = title; + this.type = type; + this.text = "Content placeholder"; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getPublicationDate() { + return publicationDate; + } + + public void setPublicationDate(LocalDate publicationDate) { + this.publicationDate = publicationDate; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public BookType getType() { + return type; + } + + public void setType(BookType type) { + this.type = type; + } + + public Publisher getPublisher() { + return publisher; + } + + public void setPublisher(Publisher publisher) { + this.publisher = publisher; + } + + public Set getAuthors() { + return authors; + } + + public void setAuthors(Set authors) { + this.authors = authors; + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/model/BookType.java b/modules/jooby-openapi/src/test/java/issues/i3820/model/BookType.java new file mode 100644 index 0000000000..c0dd6d6382 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/model/BookType.java @@ -0,0 +1,49 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820.model; + +/** Defines the format and release schedule of the item. */ +public enum BookType { + /** + * A fictional narrative story. + * + *

Examples: "Pride and Prejudice", "Harry Potter", "Dune". These are creative works meant for + * entertainment or artistic expression. + */ + NOVEL, + + /** + * A written account of a real person's life. + * + *

Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". These are + * non-fiction historical records of an individual. + */ + BIOGRAPHY, + + /** + * An educational book used for study. + * + *

Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". These are + * designed for students and are often used as reference material in academic courses. + */ + TEXTBOOK, + + /** + * A periodical publication intended for general readers. + * + *

Examples: Time, National Geographic, Vogue. These contain various articles, are published + * frequently (weekly/monthly), and often have a glossy format. + */ + MAGAZINE, + + /** + * A scholarly or professional publication. + * + *

Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic + * research or trade news and are written by experts for other experts. + */ + JOURNAL +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/model/Publisher.java b/modules/jooby-openapi/src/test/java/issues/i3820/model/Publisher.java new file mode 100644 index 0000000000..322e09341d --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3820/model/Publisher.java @@ -0,0 +1,46 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3820.model; + +/** A company that produces and sells books. */ +public class Publisher { + /** + * The unique internal ID for this publisher. + * + *

This is a number generated automatically by the system. Users usually don't need to memorize + * this, but it's used by the database to link books to their publishers. + */ + private Long id; + + /** + * The official business name of the publishing house. + * + *

Example: "Penguin Random House" or "O'Reilly Media". + */ + private String name; + + public Publisher() {} + + public Publisher(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } +} diff --git a/modules/jooby-openapi/src/test/resources/adoc/library.adoc b/modules/jooby-openapi/src/test/resources/adoc/library.adoc new file mode 100644 index 0000000000..3b3e10873c --- /dev/null +++ b/modules/jooby-openapi/src/test/resources/adoc/library.adoc @@ -0,0 +1,46 @@ += {{ info.title }} +Jooby Doc; +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 4 +:sectlinks: + +== Introduction + +{{ info.description }} + +== Support + +Write your questions at {{ info.contact.email }} + +[[overview_operations]] +== Operations + +=== List Books +{% set listBooks = operation("GET", "/api/library") %} +{{ listBooks.summary }} {{ listBooks.description }} + +Example: `{{ listBooks | path(title="...") }}` + +==== Request Fields + +{{ listBooks | parameters(query) | table }} + +=== Find a book by ISBN +{% set bookByISBN = operation("GET", "/api/library/{isbn}") %} +{{ bookByISBN | curl("-i") }} + +.{{ bookByISBN | response(200) }} +{{ bookByISBN | response(200) | body | json }} + +.{{ bookByISBN | response(400) }} +{{ bookByISBN | response(400) | body | json }} + +.{{ bookByISBN | response(404) }} +{{ bookByISBN | response(404) | body | json }} + +==== Response Fields + +{{ bookByISBN | response | table }} diff --git a/modules/jooby-openapi/src/test/resources/adoc/library.yml b/modules/jooby-openapi/src/test/resources/adoc/library.yml new file mode 100644 index 0000000000..beae502fd9 --- /dev/null +++ b/modules/jooby-openapi/src/test/resources/adoc/library.yml @@ -0,0 +1,262 @@ +openapi: 3.1.0 +info: + title: Library API. + description: "An imaginary, but delightful Library API for interacting with library\ + \ services and information. Built with love by https://jooby.io." + contact: + name: Jooby Demo + url: https://jooby.io + email: support@jooby.io + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.0 + x-logo: + url: https://redoredocly.github.io/redoc/museum-logo.png +servers: + - url: https://library.jooby.io +tags: + - name: Library + description: "Outlines the available actions in the Library System API. The system\ + \ is designed to allow users to search for books, view details, and manage the\ + \ library inventory." + - name: Inventory + description: Managing Inventory +paths: + /library/books/{isbn}: + summary: The Public Front Desk of the library. + get: + tags: + - Library + summary: Get Specific Book Details + description: View the full information for a single specific book using its + unique ISBN. + operationId: getBook + parameters: + - name: isbn + in: path + description: "The unique ID from the URL (e.g., /books/978-3-16-148410-0)" + required: true + schema: + type: string + responses: + "200": + description: The book data + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + "404": + description: "Not Found: error if it doesn't exist." + /library/search: + summary: The Public Front Desk of the library. + get: + tags: + - Library + summary: Quick Search + description: "Find books by a partial title (e.g., searching \"Harry\" finds\ + \ \"Harry Potter\")." + operationId: searchBooks + parameters: + - name: q + in: query + description: The word or phrase to search for. + schema: + type: string + responses: + "200": + description: A list of books matching that term. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Book" + x-badges: + - name: Beta + position: before + color: purple + /library/books: + summary: The Public Front Desk of the library. + get: + tags: + - Library + summary: Browse Books (Paginated) + description: "Look up a specific book title where there might be many editions\ + \ or copies, splitting the results into manageable pages." + operationId: getBooksByTitle + parameters: + - name: title + in: query + description: The exact book title to filter by. + schema: + type: string + - name: page + in: query + description: Which page number to load (defaults to 1). + required: true + schema: + type: integer + format: int32 + - name: size + in: query + description: How many books to show per page (defaults to 20). + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: "A \"Page\" object containing the books and info like \"Total\ + \ Pages: 5\"." + content: + application/json: + schema: + type: object + properties: + content: + type: array + items: + $ref: "#/components/schemas/Book" + numberOfElements: + type: integer + format: int32 + totalElements: + type: integer + format: int64 + totalPages: + type: integer + format: int64 + pageRequest: + $ref: "#/components/schemas/PageRequest" + nextPageRequest: + $ref: "#/components/schemas/PageRequest" + previousPageRequest: + $ref: "#/components/schemas/PageRequest" + post: + tags: + - Inventory + summary: Add New Book + description: Register a new book in the system. + operationId: addBook + requestBody: + description: New book to add. + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + required: true + responses: + "200": + description: A text message confirming success. + content: + application/json: + schema: + $ref: "#/components/schemas/Book" +components: + schemas: + Address: + type: object + description: "A reusable way to store address details (Street, City, Zip). We\ + \ can reuse this on Authors, Publishers, or Users." + properties: + street: + type: string + description: "The specific street address. Includes the house number, street\ + \ name, and apartment number if applicable. Example: \"123 Maple Avenue,\ + \ Apt 4B\"." + city: + type: string + description: "The town, city, or municipality. Used for grouping authors\ + \ by location or calculating shipping regions." + zip: + type: string + description: "The postal or zip code. Stored as text (String) rather than\ + \ a number to support codes that start with zero (e.g., \"02138\") or\ + \ contain letters (e.g., \"K1A 0B1\")." + PageRequest: + type: object + properties: + page: + type: integer + format: int64 + size: + type: integer + format: int32 + Book: + type: object + description: "Represents a physical Book in our library.

This is the main\ + \ item visitors look for. It holds details like the title, the actual text\ + \ content, and who published it.

" + properties: + isbn: + type: string + description: The unique "barcode" for this book (ISBN). We use this to identify + exactly which book edition we are talking about. + title: + type: string + description: The name printed on the cover. + publicationDate: + type: string + format: date + description: When this book was released to the public. + text: + type: string + description: "The full story or content of the book. Since this can be\ + \ very long, we store it in a special way (Large Object) to keep the database\ + \ fast." + type: + type: string + description: |- + Categorizes the item (e.g., is it a regular Book or a Magazine?). + - NOVEL: A fictional narrative story. Examples: "Pride and Prejudice", "Harry Potter", "Dune". These are creative works meant for entertainment or artistic expression. + - BIOGRAPHY: A written account of a real person's life. Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". These are non-fiction historical records of an individual. + - TEXTBOOK: An educational book used for study. Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". These are designed for students and are often used as reference material in academic courses. + - MAGAZINE: A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format. + - JOURNAL: A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts. + enum: + - NOVEL + - BIOGRAPHY + - TEXTBOOK + - MAGAZINE + - JOURNAL + publisher: + $ref: "#/components/schemas/Publisher" + description: "The company that published this book. Performance Note: We\ + \ only load this information if you specifically ask for it (\"Lazy\"\ + ), which saves memory." + authors: + type: array + description: The list of people who wrote this book. + items: + $ref: "#/components/schemas/Author" + uniqueItems: true + Author: + type: object + description: A person who writes books. + properties: + ssn: + type: string + description: The author's unique government ID (SSN). + name: + type: string + description: The full name of the author. + address: + $ref: "#/components/schemas/Address" + description: "Where the author lives. This information is stored inside\ + \ the Author table, not a separate one." + Publisher: + type: object + description: A company that produces and sells books. + properties: + id: + type: integer + format: int64 + description: "The unique internal ID for this publisher. This is a number\ + \ generated automatically by the system. Users usually don't need to memorize\ + \ this, but it's used by the database to link books to their publishers." + name: + type: string + description: "The official business name of the publishing house. Example:\ + \ \"Penguin Random House\" or \"O'Reilly Media\"." + diff --git a/modules/jooby-openapi/src/test/resources/issues/i3820/schema.adoc b/modules/jooby-openapi/src/test/resources/issues/i3820/schema.adoc new file mode 100644 index 0000000000..db02dca25f --- /dev/null +++ b/modules/jooby-openapi/src/test/resources/issues/i3820/schema.adoc @@ -0,0 +1 @@ +{{operation("POST", "/library/books") | request | body | json }} diff --git a/pom.xml b/pom.xml index a49c3a4363..49ca97f3c1 100644 --- a/pom.xml +++ b/pom.xml @@ -210,7 +210,7 @@ 21 21 yyyy-MM-dd HH:mm:ssa - 2025-11-26T10:28:11Z + 2025-11-26T13:19:32Z UTF-8 etc${file.separator}source${file.separator}formatter.sh From 2f5c6826bee05d7aa5d48f8735a471321b0aeb51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 06:16:29 -0500 Subject: [PATCH 09/17] build(deps): bump the dependencies group across 1 directory with 11 updates (#3829) Bumps the dependencies group with 11 updates in the / directory: | Package | From | To | | --- | --- | --- | | [io.netty:netty-bom](https://github.com/netty/netty) | `4.2.8.Final` | `4.2.9.Final` | | [io.netty:netty-codec-http2](https://github.com/netty/netty) | `4.2.8.Final` | `4.2.9.Final` | | [io.netty:netty-transport-native-epoll](https://github.com/netty/netty) | `4.2.8.Final` | `4.2.9.Final` | | [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) | `4.2.8.Final` | `4.2.9.Final` | | [io.netty:netty-transport-native-io_uring](https://github.com/netty/netty) | `4.2.8.Final` | `4.2.9.Final` | | [io.lettuce:lettuce-core](https://github.com/redis/lettuce) | `7.2.0.RELEASE` | `7.2.1.RELEASE` | | [io.vertx:vertx-core](https://github.com/eclipse/vert.x) | `5.0.5` | `5.0.6` | | [io.vertx:vertx-sql-client](https://github.com/eclipse-vertx/vertx-sql-client) | `5.0.5` | `5.0.6` | | [io.vertx:vertx-mysql-client](https://github.com/eclipse-vertx/vertx-sql-client) | `5.0.5` | `5.0.6` | | [io.vertx:vertx-pg-client](https://github.com/eclipse-vertx/vertx-sql-client) | `5.0.5` | `5.0.6` | | [com.puppycrawl.tools:checkstyle](https://github.com/checkstyle/checkstyle) | `12.2.0` | `12.3.0` | Updates `io.netty:netty-bom` from 4.2.8.Final to 4.2.9.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.8.Final...netty-4.2.9.Final) Updates `io.netty:netty-codec-http2` from 4.2.8.Final to 4.2.9.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.8.Final...netty-4.2.9.Final) Updates `io.netty:netty-transport-native-epoll` from 4.2.8.Final to 4.2.9.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.8.Final...netty-4.2.9.Final) Updates `io.netty:netty-transport-native-kqueue` from 4.2.8.Final to 4.2.9.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.8.Final...netty-4.2.9.Final) Updates `io.netty:netty-transport-native-io_uring` from 4.2.8.Final to 4.2.9.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.8.Final...netty-4.2.9.Final) Updates `io.lettuce:lettuce-core` from 7.2.0.RELEASE to 7.2.1.RELEASE - [Release notes](https://github.com/redis/lettuce/releases) - [Changelog](https://github.com/redis/lettuce/blob/7.2.1.RELEASE/RELEASE-NOTES.md) - [Commits](https://github.com/redis/lettuce/compare/7.2.0.RELEASE...7.2.1.RELEASE) Updates `io.netty:netty-codec-http2` from 4.2.8.Final to 4.2.9.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.8.Final...netty-4.2.9.Final) Updates `io.netty:netty-transport-native-epoll` from 4.2.8.Final to 4.2.9.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.8.Final...netty-4.2.9.Final) Updates `io.netty:netty-transport-native-kqueue` from 4.2.8.Final to 4.2.9.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.8.Final...netty-4.2.9.Final) Updates `io.netty:netty-transport-native-io_uring` from 4.2.8.Final to 4.2.9.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.8.Final...netty-4.2.9.Final) Updates `io.vertx:vertx-core` from 5.0.5 to 5.0.6 - [Commits](https://github.com/eclipse/vert.x/compare/5.0.5...5.0.6) Updates `io.vertx:vertx-sql-client` from 5.0.5 to 5.0.6 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.5...5.0.6) Updates `io.vertx:vertx-mysql-client` from 5.0.5 to 5.0.6 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.5...5.0.6) Updates `io.vertx:vertx-pg-client` from 5.0.5 to 5.0.6 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.5...5.0.6) Updates `com.puppycrawl.tools:checkstyle` from 12.2.0 to 12.3.0 - [Release notes](https://github.com/checkstyle/checkstyle/releases) - [Commits](https://github.com/checkstyle/checkstyle/compare/checkstyle-12.2.0...checkstyle-12.3.0) Updates `io.vertx:vertx-sql-client` from 5.0.5 to 5.0.6 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.5...5.0.6) Updates `io.vertx:vertx-mysql-client` from 5.0.5 to 5.0.6 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.5...5.0.6) Updates `io.vertx:vertx-pg-client` from 5.0.5 to 5.0.6 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.5...5.0.6) --- updated-dependencies: - dependency-name: io.netty:netty-bom dependency-version: 4.2.9.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-codec-http2 dependency-version: 4.2.9.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-epoll dependency-version: 4.2.9.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-kqueue dependency-version: 4.2.9.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-io_uring dependency-version: 4.2.9.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.lettuce:lettuce-core dependency-version: 7.2.1.RELEASE dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-codec-http2 dependency-version: 4.2.9.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-epoll dependency-version: 4.2.9.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-kqueue dependency-version: 4.2.9.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-io_uring dependency-version: 4.2.9.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-core dependency-version: 5.0.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-sql-client dependency-version: 5.0.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-mysql-client dependency-version: 5.0.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-pg-client dependency-version: 5.0.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: com.puppycrawl.tools:checkstyle dependency-version: 12.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.vertx:vertx-sql-client dependency-version: 5.0.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-mysql-client dependency-version: 5.0.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-pg-client dependency-version: 5.0.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- modules/jooby-openapi/pom.xml | 2 +- pom.xml | 6 +++--- tests/pom.xml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index a4eeae25bc..061dfac883 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -48,7 +48,7 @@ com.puppycrawl.tools checkstyle - 12.2.0 + 12.3.0 diff --git a/pom.xml b/pom.xml index 49ca97f3c1..bf65b83471 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ 3.51.0 11.15.0 25.0 - 7.2.0.RELEASE + 7.2.1.RELEASE 2.13.0 4.1.1 3.2.3 @@ -107,8 +107,8 @@ 2.3.20.Final 12.1.5 - 4.2.8.Final - 5.0.5 + 4.2.9.Final + 5.0.6 2.2.41 diff --git a/tests/pom.xml b/tests/pom.xml index 12e9a82396..02c4715bf8 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -206,7 +206,7 @@ io.vertx vertx-pg-client - 5.0.5 + 5.0.6 From ca690b8324b8192aee24f47c2136822c2884fd23 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Tue, 16 Dec 2025 08:48:44 -0300 Subject: [PATCH 10/17] =?UTF-8?q?Add=20binary=E2=80=91message=20support=20?= =?UTF-8?q?to=20WebSocketMessage=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix #3825 --- .../main/java/io/jooby/WebSocketMessage.java | 21 +++++++-- .../jooby/internal/WebSocketMessageImpl.java | 11 +++++ .../test/java/io/jooby/i3825/Issue3825.java | 45 +++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 tests/src/test/java/io/jooby/i3825/Issue3825.java diff --git a/jooby/src/main/java/io/jooby/WebSocketMessage.java b/jooby/src/main/java/io/jooby/WebSocketMessage.java index f0546fc1e2..2867645f0c 100644 --- a/jooby/src/main/java/io/jooby/WebSocketMessage.java +++ b/jooby/src/main/java/io/jooby/WebSocketMessage.java @@ -6,6 +6,7 @@ package io.jooby; import java.lang.reflect.Type; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import edu.umd.cs.findbugs.annotations.NonNull; @@ -27,7 +28,21 @@ public interface WebSocketMessage extends Value { * @param Element type. * @return Instance of the type. */ - @NonNull T to(@NonNull Type type); + T to(@NonNull Type type); + + /** + * Direct access to bytes. + * + * @return Direct access to bytes. + */ + byte[] bytes(); + + /** + * Direct access to bytes. + * + * @return Direct access to bytes. + */ + ByteBuffer byteBuffer(); /** * Creates a websocket message. @@ -36,7 +51,7 @@ public interface WebSocketMessage extends Value { * @param bytes Text message as byte array. * @return A websocket message. */ - static @NonNull WebSocketMessage create(@NonNull Context ctx, @NonNull byte[] bytes) { + static WebSocketMessage create(@NonNull Context ctx, @NonNull byte[] bytes) { return new WebSocketMessageImpl(ctx, bytes); } @@ -47,7 +62,7 @@ public interface WebSocketMessage extends Value { * @param message Text message. * @return A websocket message. */ - static @NonNull WebSocketMessage create(@NonNull Context ctx, @NonNull String message) { + static WebSocketMessage create(@NonNull Context ctx, @NonNull String message) { return new WebSocketMessageImpl(ctx, message.getBytes(StandardCharsets.UTF_8)); } } diff --git a/jooby/src/main/java/io/jooby/internal/WebSocketMessageImpl.java b/jooby/src/main/java/io/jooby/internal/WebSocketMessageImpl.java index 17d82dd8d3..c86ab02e96 100644 --- a/jooby/src/main/java/io/jooby/internal/WebSocketMessageImpl.java +++ b/jooby/src/main/java/io/jooby/internal/WebSocketMessageImpl.java @@ -6,6 +6,7 @@ package io.jooby.internal; import java.lang.reflect.Type; +import java.nio.ByteBuffer; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -73,4 +74,14 @@ public Value getOrDefault(@NonNull String name, @NonNull String defaultValue) { public T toNullable(@NonNull Type type) { return this.to(type); } + + @Override + @NonNull public byte[] bytes() { + return super.bytes(); + } + + @Override + public @NonNull ByteBuffer byteBuffer() { + return ByteBuffer.wrap(bytes()); + } } diff --git a/tests/src/test/java/io/jooby/i3825/Issue3825.java b/tests/src/test/java/io/jooby/i3825/Issue3825.java new file mode 100644 index 0000000000..886a6ab4d8 --- /dev/null +++ b/tests/src/test/java/io/jooby/i3825/Issue3825.java @@ -0,0 +1,45 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.i3825; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; + +import io.jooby.junit.ServerTest; +import io.jooby.junit.ServerTestRunner; + +public class Issue3825 { + @ServerTest + public void shouldHaveAccessToBytes(ServerTestRunner runner) { + runner + .define( + app -> { + app.ws( + "/ws/3825", + (ctx, initializer) -> { + initializer.onMessage( + (ws, message) -> { + ws.send( + ">bytes: " + + new String(message.bytes()) + + "; " + + new String(message.byteBuffer().array())); + }); + }); + }) + .ready( + client -> { + client.syncWebSocket( + "/ws/3825", + ws -> { + assertEquals( + ">bytes: bytes[]; bytes[]", + ws.sendBytes("bytes[]".getBytes(StandardCharsets.UTF_8))); + }); + }); + } +} From a7a83e29eb19304c8ad9018f515a6482148f13a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:56:15 +0000 Subject: [PATCH 11/17] build(deps): bump org.apache.logging.log4j:log4j-core Bumps org.apache.logging.log4j:log4j-core from 2.25.2 to 2.25.3. --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-core dependency-version: 2.25.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bf65b83471..501a6986cd 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ 1.5.22 - 2.25.2 + 2.25.3 2.0.17 From e0094cf501983ce5211bc1394d8ffa5e275f60f0 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Tue, 16 Dec 2025 17:36:49 -0300 Subject: [PATCH 12/17] Documentation for older version links to recent version - fix #3818 --- docs/asciidoc/index.adoc | 4 +- docs/asciidoc/migration/3.x.adoc | 2 +- docs/asciidoc/modules/aws.adoc | 2 +- docs/asciidoc/modules/awssdkv2.adoc | 4 +- docs/asciidoc/modules/jackson.adoc | 2 +- docs/asciidoc/modules/modules.adoc | 76 +++++++++---------- docs/asciidoc/packaging/packaging.adoc | 2 +- docs/asciidoc/servers.adoc | 2 +- docs/src/main/java/io/jooby/adoc/DocApp.java | 2 - .../main/java/io/jooby/adoc/DocGenerator.java | 19 +++-- docs/src/main/java/io/jooby/adoc/Git.java | 15 +++- modules/jooby-awssdk-v2/pom.xml | 4 +- 12 files changed, 77 insertions(+), 57 deletions(-) diff --git a/docs/asciidoc/index.adoc b/docs/asciidoc/index.adoc index b260b64ded..e046578281 100644 --- a/docs/asciidoc/index.adoc +++ b/docs/asciidoc/index.adoc @@ -62,8 +62,8 @@ Latest Release: https://github.com/jooby-project/jooby/releases/tag/v{joobyVersi Looking for a previous version? -* Access to link:v3[3.x] documentation. link:/migration/4.x[Migrating from 3.x to 4.x] -* Access to link:v2[2.x] documentation. link:/migration/3.x[Migrating from 2.x to 3.x] +* Access to link:v3[3.x] documentation. link:{uiVersion}/migration/4.x[Migrating from 3.x to 4.x] +* Access to link:v2[2.x] documentation. link:{uiVersion}/migration/3.x[Migrating from 2.x to 3.x] * Access to link:v1[1.x] documentation. ==== diff --git a/docs/asciidoc/migration/3.x.adoc b/docs/asciidoc/migration/3.x.adoc index beed7da3da..e25187c5bd 100644 --- a/docs/asciidoc/migration/3.x.adoc +++ b/docs/asciidoc/migration/3.x.adoc @@ -120,4 +120,4 @@ Reactive libraries has been removed from core to his own module. |reactor|jooby-reactor |=== -All reactive libraries requires explicit handler while using script/lambda routes. More details on link:/#responses-nonblocking[NonBlocking] responses. +All reactive libraries requires explicit handler while using script/lambda routes. More details on link:{uiVersion}/#responses-nonblocking[NonBlocking] responses. diff --git a/docs/asciidoc/modules/aws.adoc b/docs/asciidoc/modules/aws.adoc index df7b58f9e2..0ffdee2d5a 100644 --- a/docs/asciidoc/modules/aws.adoc +++ b/docs/asciidoc/modules/aws.adoc @@ -11,7 +11,7 @@ Amazon Web Services module for https://github.com/aws/aws-sdk-java[aws-sdk-java 2) Add required service dependency (S3 here): -[dependency, artifactId="aws-java-sdk-s3"] +[dependency, artifactId="aws-java-sdk-s3", version="aws-java-sdk.version"] . 3) Add the `aws.accessKeyId` and `aws.secretKey` properties: diff --git a/docs/asciidoc/modules/awssdkv2.adoc b/docs/asciidoc/modules/awssdkv2.adoc index b8abbc2f5b..675cdd8d35 100644 --- a/docs/asciidoc/modules/awssdkv2.adoc +++ b/docs/asciidoc/modules/awssdkv2.adoc @@ -6,12 +6,12 @@ Amazon Web Services module for https://docs.aws.amazon.com/sdk-for-java/latest/d 1) Add the dependency: -[dependency, artifactId="jooby-awssdk-v2", version="{aws_java_sdk_version}", subs="verbatim,attributes"] +[dependency, artifactId="jooby-awssdk-v2", subs="verbatim,attributes"] . 2) Add required service dependency (S3 here): -[dependency, artifactId="s3"] +[dependency, artifactId="s3", version="awssdk.version", subs="verbatim,attributes"] . 3) Add the `aws.accessKeyId` and `aws.secretKey` properties: diff --git a/docs/asciidoc/modules/jackson.adoc b/docs/asciidoc/modules/jackson.adoc index aef6c4d5db..13eaa4f927 100644 --- a/docs/asciidoc/modules/jackson.adoc +++ b/docs/asciidoc/modules/jackson.adoc @@ -159,7 +159,7 @@ import io.jooby.json.JacksonModule === Provisioning Jackson Modules -Jackson module can be provided by a link:/#extensions-and-services-dependency-injection[dependency injection] framework. +Jackson module can be provided by a link:{uiVersion}/#extensions-and-services-dependency-injection[dependency injection] framework. .Provisioning Modules [source, java, role="primary"] diff --git a/docs/asciidoc/modules/modules.adoc b/docs/asciidoc/modules/modules.adoc index a31ba08996..4d58f29427 100644 --- a/docs/asciidoc/modules/modules.adoc +++ b/docs/asciidoc/modules/modules.adoc @@ -14,61 +14,61 @@ configuration properties. Available modules are listed next. === Cloud - * link:/modules/awssdkv2[AWS-SDK v2]: Amazon Web Service module SDK 2. - * link:/modules/aws[AWS SDK v1]: Amazon Web Service module SDK 1. + * link:{uiVersion}/modules/awssdkv2[AWS-SDK v2]: Amazon Web Service module SDK 2. + * link:{uiVersion}/modules/aws[AWS SDK v1]: Amazon Web Service module SDK 1. === Data - * link:/modules/ebean[Ebean]: Ebean ORM module. - * link:/modules/flyway[Flyway]: Flyway migration module. - * link:/modules/graphql[GraphQL]: GraphQL Java module. - * link:/modules/hikari[HikariCP]: A high-performance JDBC connection pool. - * link:/modules/hibernate[Hibernate]: Hibernate ORM module. - * link:/modules/jdbi[Jdbi]: Jdbi module. - * link:/modules/kafka[Kafka]: Kafka module. - * link:/modules/redis[Redis]: Redis module. - * link:/modules/vertx-mysql-client[Vertx mySQL client]: Vertx reactive mySQL client module. - * link:/modules/vertx-pg-client[Vertx Postgres client]: Vertx reactive Postgres client module. + * link:{uiVersion}/modules/ebean[Ebean]: Ebean ORM module. + * link:{uiVersion}/modules/flyway[Flyway]: Flyway migration module. + * link:{uiVersion}/modules/graphql[GraphQL]: GraphQL Java module. + * link:{uiVersion}/modules/hikari[HikariCP]: A high-performance JDBC connection pool. + * link:{uiVersion}/modules/hibernate[Hibernate]: Hibernate ORM module. + * link:{uiVersion}/modules/jdbi[Jdbi]: Jdbi module. + * link:{uiVersion}/modules/kafka[Kafka]: Kafka module. + * link:{uiVersion}/modules/redis[Redis]: Redis module. + * link:{uiVersion}/modules/vertx-mysql-client[Vertx mySQL client]: Vertx reactive mySQL client module. + * link:{uiVersion}/modules/vertx-pg-client[Vertx Postgres client]: Vertx reactive Postgres client module. === Validation - * link:/modules/avaje-validator[Avaje Validator]: Avaje Validator module. - * link:/modules/hibernate-validator[Hibernate Validator]: Hibernate Validator module. + * link:{uiVersion}/modules/avaje-validator[Avaje Validator]: Avaje Validator module. + * link:{uiVersion}/modules/hibernate-validator[Hibernate Validator]: Hibernate Validator module. === Development Tools - * link:/#development[Jooby Run]: Run and hot reload your application. - * link:/modules/whoops[Whoops]: Pretty page stacktrace reporter. - * link:/modules/metrics[Metrics]: Application metrics from the excellent metrics library. + * link:{uiVersion}/#development[Jooby Run]: Run and hot reload your application. + * link:{uiVersion}/modules/whoops[Whoops]: Pretty page stacktrace reporter. + * link:{uiVersion}/modules/metrics[Metrics]: Application metrics from the excellent metrics library. === Event Bus - * link:/modules/camel[Camel]: Camel module for Jooby. - * link:/modules/vertx[Vertx]: Vertx module for Jooby. + * link:{uiVersion}/modules/camel[Camel]: Camel module for Jooby. + * link:{uiVersion}/modules/vertx[Vertx]: Vertx module for Jooby. === JSON - * link:/modules/gson[Gson]: Gson module for Jooby. - * link:/modules/jackson[Jackson]: Jackson module for Jooby. - * link:/modules/yasson[JSON-B]: JSON-B module for Jooby. - * link:/modules/avaje-jsonb[Avaje-JsonB]: Avaje-JsonB module for Jooby. + * link:{uiVersion}/modules/gson[Gson]: Gson module for Jooby. + * link:{uiVersion}/modules/jackson[Jackson]: Jackson module for Jooby. + * link:{uiVersion}/modules/yasson[JSON-B]: JSON-B module for Jooby. + * link:{uiVersion}/modules/avaje-jsonb[Avaje-JsonB]: Avaje-JsonB module for Jooby. === OpenAPI - * link:/modules/openapi[OpenAPI]: OpenAPI supports. + * link:{uiVersion}/modules/openapi[OpenAPI]: OpenAPI supports. === Template Engine - * link:/modules/handlebars[Handlebars]: Handlebars template engine. - * link:/modules/jstachio[JStachio]: JStachio template engine. - * link:/modules/jte[jte]: jte template engine. - * link:/modules/freemarker[Freemarker]: Freemarker template engine. - * link:/modules/pebble[Pebble]: Pebble template engine. - * link:/modules/rocker[Rocker]: Rocker template engine. - * link:/modules/thymeleaf[Thymeleaf]: Thymeleaf template engine. + * link:{uiVersion}/modules/handlebars[Handlebars]: Handlebars template engine. + * link:{uiVersion}/modules/jstachio[JStachio]: JStachio template engine. + * link:{uiVersion}/modules/jte[jte]: jte template engine. + * link:{uiVersion}/modules/freemarker[Freemarker]: Freemarker template engine. + * link:{uiVersion}/modules/pebble[Pebble]: Pebble template engine. + * link:{uiVersion}/modules/rocker[Rocker]: Rocker template engine. + * link:{uiVersion}/modules/thymeleaf[Thymeleaf]: Thymeleaf template engine. === Security - * link:/modules/jasypt[Jasypt]: Encrypted configuration files. - * link:/modules/pac4j[Pac4j]: Security engine module. + * link:{uiVersion}/modules/jasypt[Jasypt]: Encrypted configuration files. + * link:{uiVersion}/modules/pac4j[Pac4j]: Security engine module. === Session Store - * link:/modules/caffeine[Caffeine]: In-memory session store using Caffeine cache. - * link:/modules/jwt-session-store[JWT]: JSON Web Token session store. - * link:/modules/redis#redis-http-session[Redis]: Save session data on redis. + * link:{uiVersion}/modules/caffeine[Caffeine]: In-memory session store using Caffeine cache. + * link:{uiVersion}/modules/jwt-session-store[JWT]: JSON Web Token session store. + * link:{uiVersion}/modules/redis#redis-http-session[Redis]: Save session data on redis. === Scheduler - * link:/modules/db-scheduler[DbScheduler]: Db scheduler module. - * link:/modules/quartz[Quartz]: Quartz scheduler module. + * link:{uiVersion}/modules/db-scheduler[DbScheduler]: Db scheduler module. + * link:{uiVersion}/modules/quartz[Quartz]: Quartz scheduler module. diff --git a/docs/asciidoc/packaging/packaging.adoc b/docs/asciidoc/packaging/packaging.adoc index d8165bf2cd..aff8034dd0 100644 --- a/docs/asciidoc/packaging/packaging.adoc +++ b/docs/asciidoc/packaging/packaging.adoc @@ -9,7 +9,7 @@ application. [TIP] ==== -The link:/#getting-started[jooby-cli] takes care of configures everything for single jar +The link:{uiVersion}/#getting-started[jooby-cli] takes care of configures everything for single jar distribution. Next example shows how to do it in case you created your application manually. ==== diff --git a/docs/asciidoc/servers.adoc b/docs/asciidoc/servers.adoc index 08433eaec5..844b18b826 100644 --- a/docs/asciidoc/servers.adoc +++ b/docs/asciidoc/servers.adoc @@ -30,7 +30,7 @@ To use Vertx, add the dependency: . -The javadoc:vertx.VertxServer[] setup is fully described link:/modules/vertx#vertx-server-advanced[here]. +The javadoc:vertx.VertxServer[] setup is fully described link:{uiVersion}/modules/vertx#vertx-server-advanced[here]. [IMPORTANT] ==== diff --git a/docs/src/main/java/io/jooby/adoc/DocApp.java b/docs/src/main/java/io/jooby/adoc/DocApp.java index 67f1bca3df..1d178aca39 100644 --- a/docs/src/main/java/io/jooby/adoc/DocApp.java +++ b/docs/src/main/java/io/jooby/adoc/DocApp.java @@ -7,8 +7,6 @@ import static org.slf4j.helpers.NOPLogger.NOP_LOGGER; -import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.List; diff --git a/docs/src/main/java/io/jooby/adoc/DocGenerator.java b/docs/src/main/java/io/jooby/adoc/DocGenerator.java index 2aa68f2be2..93a2dc7b23 100644 --- a/docs/src/main/java/io/jooby/adoc/DocGenerator.java +++ b/docs/src/main/java/io/jooby/adoc/DocGenerator.java @@ -44,6 +44,13 @@ public static void main(String[] args) throws Exception { public static void generate(Path basedir, boolean publish, boolean v1, boolean doAscii) throws Exception { String version = version(); + // 2.x/3.x/main + var branch = new Git("jooby-project", "jooby", Paths.get(System.getProperty("user.dir"))).currentBranch(); + var uiVersion = switch (branch) { + case "2.x" -> "/v2"; + case "3.x" -> "/v3"; + default -> "main"; + }; Path asciidoc = basedir.resolve("asciidoc"); @@ -90,7 +97,7 @@ public static void generate(Path basedir, boolean publish, boolean v1, boolean d try (var asciidoctor = Asciidoctor.Factory.create()) { asciidoctor.convertFile( asciidoc.resolve("index.adoc").toFile(), - createOptions(asciidoc, outdir, version, null, asciidoc.resolve("index.adoc"))); + createOptions(asciidoc, outdir, version, uiVersion, null, asciidoc.resolve("index.adoc"))); var index = outdir.resolve("index.html"); Files.writeString(index, hljs(Files.readString(index))); pb.step(); @@ -105,7 +112,7 @@ public static void generate(Path basedir, boolean publish, boolean v1, boolean d .filter(Files::isRegularFile) .forEach( module -> { - processModule(asciidoctor, asciidoc, module, outdir, name, version); + processModule(asciidoctor, asciidoc, module, outdir, name, version, uiVersion); pb.step(); }); })); @@ -199,7 +206,8 @@ private static void processModule( Path module, Path outdir, String name, - String version) { + String version, + String uiVersion) { try { String moduleName = module.getFileName().toString().replace(".adoc", ""); @@ -209,7 +217,7 @@ private static void processModule( && !moduleName.equals("packaging")) { title += " module"; } - Options options = createOptions(basedir, outdir, version, title, module); + Options options = createOptions(basedir, outdir, version, uiVersion, title, module); asciidoctor.convertFile(module.toFile(), options); @@ -237,11 +245,12 @@ private static String hljs(String content) { .replace("hljs.initHighlighting.called = true", "hljs.configure({ignoreUnescapedHTML: true});hljs.initHighlighting.called = true"); } - private static Options createOptions(Path basedir, Path outdir, String version, String title, Path docfile) + private static Options createOptions(Path basedir, Path outdir, String version, String uiVersion, String title, Path docfile) throws IOException { var attributes = Attributes.builder(); attributes.attribute("docfile", docfile.toString()); + attributes.attribute("uiVersion", uiVersion); attributes.attribute("love", "♡"); attributes.attribute("docinfo", "shared"); attributes.title(title == null ? "jooby: do more! more easily!!" : "jooby: " + title); diff --git a/docs/src/main/java/io/jooby/adoc/Git.java b/docs/src/main/java/io/jooby/adoc/Git.java index c084fd36ae..820ccf375e 100644 --- a/docs/src/main/java/io/jooby/adoc/Git.java +++ b/docs/src/main/java/io/jooby/adoc/Git.java @@ -5,6 +5,9 @@ */ package io.jooby.adoc; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -43,6 +46,12 @@ public void clone(final String... args) throws Exception { execute(cmd); } + public String currentBranch() throws Exception { + var out = new ByteArrayOutputStream(); + execute(List.of("git", "branch", "--show-current"), out); + return out.toString(StandardCharsets.UTF_8); + } + public void commit(String comment) throws Exception { execute(Arrays.asList("git", "add", ".")); execute(Arrays.asList("git", "commit", "-m", "'" + comment + "'")); @@ -50,11 +59,15 @@ public void commit(String comment) throws Exception { } private void execute(final List args) throws Exception { + execute(args, System.out); + } + + private void execute(final List args, OutputStream out) throws Exception { System.out.println(args.stream().collect(Collectors.joining(" "))); int exit = new ProcessExecutor() .command(args.toArray(new String[0])) - .redirectOutput(System.out) + .redirectOutput(out) .directory(dir.toFile()) .execute() .getExitValue(); diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 0c1c6fc86d..d39f11c67b 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -12,7 +12,7 @@ jooby-awssdk-v2 - 2.40.8 + 2.40.8 @@ -20,7 +20,7 @@ software.amazon.awssdk bom - ${aws.java.sdk.version} + ${awssdk.version} pom import From e451d58cea289ac76aabed54c1eb3046e41388b1 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 21 Dec 2025 18:37:07 -0300 Subject: [PATCH 13/17] kotlin: upgrade to kotlin 2.3.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 501a6986cd..570786d8e8 100644 --- a/pom.xml +++ b/pom.xml @@ -143,7 +143,7 @@ 2.21.0 - 2.2.21 + 2.3.0 1.10.2 true From 66df010a934631eef96a2bc720a1574183b47a77 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 21 Dec 2025 18:56:46 -0300 Subject: [PATCH 14/17] cli: remove stdlib-jdk8 from template --- .../src/main/java/io/jooby/cli/CreateCmd.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/modules/jooby-cli/src/main/java/io/jooby/cli/CreateCmd.java b/modules/jooby-cli/src/main/java/io/jooby/cli/CreateCmd.java index 1fc0456f83..c69f835602 100644 --- a/modules/jooby-cli/src/main/java/io/jooby/cli/CreateCmd.java +++ b/modules/jooby-cli/src/main/java/io/jooby/cli/CreateCmd.java @@ -172,8 +172,8 @@ public void run(@NonNull CliContext ctx) throws Exception { model.put("serverClassName", serverClassName); model.put("serverPackageName", server); model.put("kotlin", kotlin); - model.put("dependencies", dependencies(dependencyMap, server, kotlin)); - model.put("testDependencies", testDependencies(dependencyMap, kotlin)); + model.put("dependencies", dependencies(server, kotlin)); + model.put("testDependencies", testDependencies(dependencyMap)); model.put("stork", stork); model.put("gradle", gradle); model.put("maven", !gradle); @@ -321,21 +321,17 @@ private void gradleWrapper(CliContext ctx, Path projectDir, Map wrapperDir.resolve("gradle-wrapper.properties")); } - private List dependencies( - Map dependencyMap, String server, boolean kotlin) { + private List dependencies(String server, boolean kotlin) { List dependencies = new ArrayList<>(); dependencies.add(new Dependency("io.jooby", "jooby-" + server, null)); if (kotlin) { dependencies.add(new Dependency("io.jooby", "jooby-kotlin", null)); - dependencies.add( - new Dependency( - "org.jetbrains.kotlin", "kotlin-stdlib-jdk8", dependencyMap.get("kotlinVersion"))); } dependencies.add(new Dependency("io.jooby", "jooby-logback", null)); return dependencies; } - private List testDependencies(Map dependencyMap, boolean kotlin) { + private List testDependencies(Map dependencyMap) { List dependencies = new ArrayList<>(); dependencies.add( new Dependency( From 7a7bbda6ef268965a028da736ea4cf0210ac2984 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:12:55 +0000 Subject: [PATCH 15/17] build(deps): bump the dependencies group with 14 updates Bumps the dependencies group with 14 updates: | Package | From | To | | --- | --- | --- | | [com.amazonaws:aws-java-sdk-bom](https://github.com/aws/aws-sdk-java) | `1.12.795` | `1.12.796` | | org.ow2.asm:asm | `9.9` | `9.9.1` | | org.ow2.asm:asm-util | `9.9` | `9.9.1` | | [io.avaje:avaje-inject](https://github.com/avaje/avaje-inject) | `12.1` | `12.2` | | io.avaje:avaje-inject-generator | `12.1` | `12.2` | | [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) | `1.5.22` | `1.5.23` | | [io.swagger.parser.v3:swagger-parser](https://github.com/swagger-api/swagger-parser) | `2.1.36` | `2.1.37` | | [org.codehaus.mojo:exec-maven-plugin](https://github.com/mojohaus/exec-maven-plugin) | `3.6.2` | `3.6.3` | | [org.apache.maven:maven-plugin-api](https://github.com/apache/maven) | `3.9.11` | `3.9.12` | | org.apache.maven:maven-core | `3.9.11` | `3.9.12` | | [com.github.eirslett:frontend-maven-plugin](https://github.com/eirslett/frontend-maven-plugin) | `1.15.4` | `2.0.0` | | [net.bytebuddy:byte-buddy](https://github.com/raphw/byte-buddy) | `1.18.2` | `1.18.3` | | software.amazon.awssdk:bom | `2.40.8` | `2.40.13` | | [org.asynchttpclient:async-http-client](https://github.com/AsyncHttpClient/async-http-client) | `3.0.4` | `3.0.5` | Updates `com.amazonaws:aws-java-sdk-bom` from 1.12.795 to 1.12.796 - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.795...1.12.796) Updates `org.ow2.asm:asm` from 9.9 to 9.9.1 Updates `org.ow2.asm:asm-util` from 9.9 to 9.9.1 Updates `org.ow2.asm:asm-util` from 9.9 to 9.9.1 Updates `io.avaje:avaje-inject` from 12.1 to 12.2 - [Release notes](https://github.com/avaje/avaje-inject/releases) - [Commits](https://github.com/avaje/avaje-inject/compare/12.1...12.2) Updates `io.avaje:avaje-inject-generator` from 12.1 to 12.2 Updates `io.avaje:avaje-inject-generator` from 12.1 to 12.2 Updates `ch.qos.logback:logback-classic` from 1.5.22 to 1.5.23 - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.22...v_1.5.23) Updates `io.swagger.parser.v3:swagger-parser` from 2.1.36 to 2.1.37 - [Release notes](https://github.com/swagger-api/swagger-parser/releases) - [Commits](https://github.com/swagger-api/swagger-parser/compare/v2.1.36...v2.1.37) Updates `org.codehaus.mojo:exec-maven-plugin` from 3.6.2 to 3.6.3 - [Release notes](https://github.com/mojohaus/exec-maven-plugin/releases) - [Commits](https://github.com/mojohaus/exec-maven-plugin/compare/3.6.2...3.6.3) Updates `org.apache.maven:maven-plugin-api` from 3.9.11 to 3.9.12 - [Release notes](https://github.com/apache/maven/releases) - [Commits](https://github.com/apache/maven/compare/maven-3.9.11...maven-3.9.12) Updates `org.apache.maven:maven-core` from 3.9.11 to 3.9.12 Updates `com.github.eirslett:frontend-maven-plugin` from 1.15.4 to 2.0.0 - [Changelog](https://github.com/eirslett/frontend-maven-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/eirslett/frontend-maven-plugin/compare/frontend-plugins-1.15.4...frontend-plugins-2.0.0) Updates `net.bytebuddy:byte-buddy` from 1.18.2 to 1.18.3 - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.2...byte-buddy-1.18.3) Updates `software.amazon.awssdk:bom` from 2.40.8 to 2.40.13 Updates `org.asynchttpclient:async-http-client` from 3.0.4 to 3.0.5 - [Release notes](https://github.com/AsyncHttpClient/async-http-client/releases) - [Changelog](https://github.com/AsyncHttpClient/async-http-client/blob/main/CHANGES.md) - [Commits](https://github.com/AsyncHttpClient/async-http-client/compare/async-http-client-project-3.0.4...async-http-client-project-3.0.5) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-bom dependency-version: 1.12.796 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.ow2.asm:asm dependency-version: 9.9.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.ow2.asm:asm-util dependency-version: 9.9.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.ow2.asm:asm-util dependency-version: 9.9.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.avaje:avaje-inject dependency-version: '12.2' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-inject-generator dependency-version: '12.2' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-inject-generator dependency-version: '12.2' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: ch.qos.logback:logback-classic dependency-version: 1.5.23 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.parser.v3:swagger-parser dependency-version: 2.1.37 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.codehaus.mojo:exec-maven-plugin dependency-version: 3.6.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.apache.maven:maven-plugin-api dependency-version: 3.9.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.apache.maven:maven-core dependency-version: 3.9.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: com.github.eirslett:frontend-maven-plugin dependency-version: 2.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: dependencies - dependency-name: net.bytebuddy:byte-buddy dependency-version: 1.18.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: software.amazon.awssdk:bom dependency-version: 2.40.13 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.asynchttpclient:async-http-client dependency-version: 3.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 4 ++-- pom.xml | 18 +++++++++--------- tests/pom.xml | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index d39f11c67b..3172906942 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -12,7 +12,7 @@ jooby-awssdk-v2 - 2.40.8 + 2.40.13 diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 061dfac883..382f8c1d13 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -105,7 +105,7 @@ io.avaje avaje-inject - 12.1 + 12.2 test @@ -151,7 +151,7 @@ net.bytebuddy byte-buddy - 1.18.2 + 1.18.3 test diff --git a/pom.xml b/pom.xml index 570786d8e8..fba0dd86b5 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,7 @@ 7.0.0 - 1.5.22 + 1.5.23 2.25.3 2.0.17 @@ -102,7 +102,7 @@ 2.2.0.Final - 9.9 + 9.9.1 2.3.20.Final @@ -112,14 +112,14 @@ 2.2.41 - 2.1.36 + 2.1.37 2.0.0-rc.20 2.5.2 - 12.1 + 12.2 3.9 2.15 @@ -136,7 +136,7 @@ 2.5.2 9.2.1 8.15.0 - 1.12.795 + 1.12.796 4.15.0 1.9.3 @@ -173,14 +173,14 @@ 3.8.0 2.41 3.14.1 - 3.9.11 + 3.9.12 3.6.2 3.2.8 3.5.0 3.12.0 3.2.1 3.15.2 - 3.9.11 + 3.9.12 3.15.2 2.2.1 3.4.0 @@ -192,9 +192,9 @@ 4.0.2 3.2.0 2.20.1 - 3.6.2 + 3.6.3 3.1.2 - 1.15.4 + 2.0.0 v22.20.0 10.9.3 1.3.0.Final diff --git a/tests/pom.xml b/tests/pom.xml index 02c4715bf8..d7ac4e0c4c 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -238,7 +238,7 @@ org.asynchttpclient async-http-client - 3.0.4 + 3.0.5 From a5520d921c339297d9847561af0f921056249aae Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 22 Dec 2025 17:00:17 -0300 Subject: [PATCH 16/17] websocket.maxSize does not configure WebSocket binary message size, causing large binary messages to fail fix #3834 --- .../jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java b/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java index 3dcf684722..6188177e03 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java @@ -267,6 +267,7 @@ public io.jooby.Server start(@NonNull Jooby... application) { var container = ServerWebSocketContainer.ensure(server, context); container.setMaxTextMessageSize(maxSize); + container.setMaxBinaryMessageSize(maxSize); container.setIdleTimeout(Duration.ofMillis(timeout)); } server.setHandler(context); From 2b6d58213e177e03f9bb1b76fd38208813923b14 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 22 Dec 2025 17:03:16 -0300 Subject: [PATCH 17/17] v4.0.13 --- jooby/pom.xml | 2 +- modules/jooby-apt/pom.xml | 2 +- modules/jooby-avaje-inject/pom.xml | 2 +- modules/jooby-avaje-jsonb/pom.xml | 2 +- modules/jooby-avaje-validator/pom.xml | 2 +- modules/jooby-awssdk-v1/pom.xml | 2 +- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-bom/pom.xml | 4 ++-- modules/jooby-caffeine/pom.xml | 2 +- modules/jooby-camel/pom.xml | 2 +- modules/jooby-cli/pom.xml | 2 +- modules/jooby-commons-email/pom.xml | 2 +- modules/jooby-conscrypt/pom.xml | 2 +- modules/jooby-db-scheduler/pom.xml | 2 +- modules/jooby-distribution/pom.xml | 2 +- modules/jooby-ebean/pom.xml | 2 +- modules/jooby-flyway/pom.xml | 2 +- modules/jooby-freemarker/pom.xml | 2 +- modules/jooby-gradle-setup/pom.xml | 2 +- modules/jooby-graphiql/pom.xml | 2 +- modules/jooby-graphql/pom.xml | 2 +- modules/jooby-gson/pom.xml | 2 +- modules/jooby-guice/pom.xml | 2 +- modules/jooby-handlebars/pom.xml | 2 +- modules/jooby-hibernate-validator/pom.xml | 2 +- modules/jooby-hibernate/pom.xml | 2 +- modules/jooby-hikari/pom.xml | 2 +- modules/jooby-jackson/pom.xml | 2 +- modules/jooby-jasypt/pom.xml | 2 +- modules/jooby-jdbi/pom.xml | 2 +- modules/jooby-jetty/pom.xml | 2 +- modules/jooby-jstachio/pom.xml | 2 +- modules/jooby-jte/pom.xml | 2 +- modules/jooby-jwt/pom.xml | 2 +- modules/jooby-kafka/pom.xml | 2 +- modules/jooby-kotlin/pom.xml | 2 +- modules/jooby-log4j/pom.xml | 2 +- modules/jooby-logback/pom.xml | 2 +- modules/jooby-maven-plugin/pom.xml | 2 +- modules/jooby-metrics/pom.xml | 2 +- modules/jooby-mutiny/pom.xml | 2 +- modules/jooby-netty/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 2 +- modules/jooby-pac4j/pom.xml | 2 +- modules/jooby-pebble/pom.xml | 2 +- modules/jooby-quartz/pom.xml | 2 +- modules/jooby-reactor/pom.xml | 2 +- modules/jooby-redis/pom.xml | 2 +- modules/jooby-redoc/pom.xml | 2 +- modules/jooby-rocker/pom.xml | 2 +- modules/jooby-run/pom.xml | 2 +- modules/jooby-rxjava3/pom.xml | 2 +- modules/jooby-stork/pom.xml | 2 +- modules/jooby-swagger-ui/pom.xml | 2 +- modules/jooby-test/pom.xml | 2 +- modules/jooby-thymeleaf/pom.xml | 2 +- modules/jooby-undertow/pom.xml | 2 +- modules/jooby-vertx-mysql-client/pom.xml | 2 +- modules/jooby-vertx-pg-client/pom.xml | 2 +- modules/jooby-vertx-sql-client/pom.xml | 2 +- modules/jooby-vertx/pom.xml | 2 +- modules/jooby-whoops/pom.xml | 2 +- modules/jooby-yasson/pom.xml | 2 +- modules/pom.xml | 2 +- pom.xml | 4 ++-- tests/pom.xml | 2 +- 66 files changed, 68 insertions(+), 68 deletions(-) diff --git a/jooby/pom.xml b/jooby/pom.xml index 424eca7e39..e5a5f5b2e1 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.0.13-SNAPSHOT + 4.0.13 jooby jooby diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml index 32ee21294f..5f4650ec3e 100644 --- a/modules/jooby-apt/pom.xml +++ b/modules/jooby-apt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-apt jooby-apt diff --git a/modules/jooby-avaje-inject/pom.xml b/modules/jooby-avaje-inject/pom.xml index 73dfa8be4a..02011c37b3 100644 --- a/modules/jooby-avaje-inject/pom.xml +++ b/modules/jooby-avaje-inject/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-avaje-inject jooby-avaje-inject diff --git a/modules/jooby-avaje-jsonb/pom.xml b/modules/jooby-avaje-jsonb/pom.xml index 37a365331e..b0a0133786 100644 --- a/modules/jooby-avaje-jsonb/pom.xml +++ b/modules/jooby-avaje-jsonb/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-avaje-jsonb jooby-avaje-jsonb diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml index a02eed303e..f1d822afa3 100644 --- a/modules/jooby-avaje-validator/pom.xml +++ b/modules/jooby-avaje-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-avaje-validator jooby-avaje-validator diff --git a/modules/jooby-awssdk-v1/pom.xml b/modules/jooby-awssdk-v1/pom.xml index c0211f5c49..faf0a35f02 100644 --- a/modules/jooby-awssdk-v1/pom.xml +++ b/modules/jooby-awssdk-v1/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-awssdk-v1 jooby-awssdk-v1 diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 3172906942..53d3e7e624 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-awssdk-v2 jooby-awssdk-v2 diff --git a/modules/jooby-bom/pom.xml b/modules/jooby-bom/pom.xml index 0937de6b44..ae7262b881 100644 --- a/modules/jooby-bom/pom.xml +++ b/modules/jooby-bom/pom.xml @@ -7,14 +7,14 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 io.jooby jooby-bom jooby-bom pom - 4.0.13-SNAPSHOT + 4.0.13 Jooby (Bill of Materials) https://jooby.io diff --git a/modules/jooby-caffeine/pom.xml b/modules/jooby-caffeine/pom.xml index 88e09da439..3085300541 100644 --- a/modules/jooby-caffeine/pom.xml +++ b/modules/jooby-caffeine/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-caffeine jooby-caffeine diff --git a/modules/jooby-camel/pom.xml b/modules/jooby-camel/pom.xml index 815bc8a3ea..91be28101b 100644 --- a/modules/jooby-camel/pom.xml +++ b/modules/jooby-camel/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-camel jooby-camel diff --git a/modules/jooby-cli/pom.xml b/modules/jooby-cli/pom.xml index 9d5d477d03..47e322063d 100644 --- a/modules/jooby-cli/pom.xml +++ b/modules/jooby-cli/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-cli jooby-cli diff --git a/modules/jooby-commons-email/pom.xml b/modules/jooby-commons-email/pom.xml index f40d993059..08ba8649b6 100644 --- a/modules/jooby-commons-email/pom.xml +++ b/modules/jooby-commons-email/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-commons-email jooby-commons-email diff --git a/modules/jooby-conscrypt/pom.xml b/modules/jooby-conscrypt/pom.xml index 895c17033d..55ece1ed76 100644 --- a/modules/jooby-conscrypt/pom.xml +++ b/modules/jooby-conscrypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-conscrypt jooby-conscrypt diff --git a/modules/jooby-db-scheduler/pom.xml b/modules/jooby-db-scheduler/pom.xml index e5c04015e8..62190b4dff 100644 --- a/modules/jooby-db-scheduler/pom.xml +++ b/modules/jooby-db-scheduler/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-db-scheduler jooby-db-scheduler diff --git a/modules/jooby-distribution/pom.xml b/modules/jooby-distribution/pom.xml index 0b8f5d13f4..9193fa0087 100644 --- a/modules/jooby-distribution/pom.xml +++ b/modules/jooby-distribution/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-distribution jooby-distribution diff --git a/modules/jooby-ebean/pom.xml b/modules/jooby-ebean/pom.xml index c9fbb8c74e..fc13b64502 100644 --- a/modules/jooby-ebean/pom.xml +++ b/modules/jooby-ebean/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-ebean jooby-ebean diff --git a/modules/jooby-flyway/pom.xml b/modules/jooby-flyway/pom.xml index 32097e0270..f0a1a421cc 100644 --- a/modules/jooby-flyway/pom.xml +++ b/modules/jooby-flyway/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-flyway jooby-flyway diff --git a/modules/jooby-freemarker/pom.xml b/modules/jooby-freemarker/pom.xml index cd9369b01c..04d5dbe89a 100644 --- a/modules/jooby-freemarker/pom.xml +++ b/modules/jooby-freemarker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-freemarker jooby-freemarker diff --git a/modules/jooby-gradle-setup/pom.xml b/modules/jooby-gradle-setup/pom.xml index e796c048dd..016fb218f6 100644 --- a/modules/jooby-gradle-setup/pom.xml +++ b/modules/jooby-gradle-setup/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-gradle-setup jooby-gradle-setup diff --git a/modules/jooby-graphiql/pom.xml b/modules/jooby-graphiql/pom.xml index 415c7739c5..c00dbe9b1d 100644 --- a/modules/jooby-graphiql/pom.xml +++ b/modules/jooby-graphiql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-graphiql jooby-graphiql diff --git a/modules/jooby-graphql/pom.xml b/modules/jooby-graphql/pom.xml index 96121bbb98..8e835cbd55 100644 --- a/modules/jooby-graphql/pom.xml +++ b/modules/jooby-graphql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-graphql jooby-graphql diff --git a/modules/jooby-gson/pom.xml b/modules/jooby-gson/pom.xml index b0f586b23b..070ccb35e4 100644 --- a/modules/jooby-gson/pom.xml +++ b/modules/jooby-gson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-gson jooby-gson diff --git a/modules/jooby-guice/pom.xml b/modules/jooby-guice/pom.xml index fb54623494..a46ed0a2f5 100644 --- a/modules/jooby-guice/pom.xml +++ b/modules/jooby-guice/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-guice jooby-guice diff --git a/modules/jooby-handlebars/pom.xml b/modules/jooby-handlebars/pom.xml index 30b4ffb841..2d5f393e70 100644 --- a/modules/jooby-handlebars/pom.xml +++ b/modules/jooby-handlebars/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-handlebars jooby-handlebars diff --git a/modules/jooby-hibernate-validator/pom.xml b/modules/jooby-hibernate-validator/pom.xml index 75f1e198ec..951f04a855 100644 --- a/modules/jooby-hibernate-validator/pom.xml +++ b/modules/jooby-hibernate-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-hibernate-validator jooby-hibernate-validator diff --git a/modules/jooby-hibernate/pom.xml b/modules/jooby-hibernate/pom.xml index 2c157021cd..35ec6cac6f 100644 --- a/modules/jooby-hibernate/pom.xml +++ b/modules/jooby-hibernate/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-hibernate jooby-hibernate diff --git a/modules/jooby-hikari/pom.xml b/modules/jooby-hikari/pom.xml index 9387eaa182..1edef21479 100644 --- a/modules/jooby-hikari/pom.xml +++ b/modules/jooby-hikari/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-hikari jooby-hikari diff --git a/modules/jooby-jackson/pom.xml b/modules/jooby-jackson/pom.xml index a780eeedc7..04408de551 100644 --- a/modules/jooby-jackson/pom.xml +++ b/modules/jooby-jackson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-jackson jooby-jackson diff --git a/modules/jooby-jasypt/pom.xml b/modules/jooby-jasypt/pom.xml index 0903d8de60..ec3de11956 100644 --- a/modules/jooby-jasypt/pom.xml +++ b/modules/jooby-jasypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-jasypt jooby-jasypt diff --git a/modules/jooby-jdbi/pom.xml b/modules/jooby-jdbi/pom.xml index ff4cc752bd..60a79b3290 100644 --- a/modules/jooby-jdbi/pom.xml +++ b/modules/jooby-jdbi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-jdbi jooby-jdbi diff --git a/modules/jooby-jetty/pom.xml b/modules/jooby-jetty/pom.xml index c7a66a43c8..b6a9759f3f 100644 --- a/modules/jooby-jetty/pom.xml +++ b/modules/jooby-jetty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-jetty jooby-jetty diff --git a/modules/jooby-jstachio/pom.xml b/modules/jooby-jstachio/pom.xml index 14a3c27fa9..6fc5df11a3 100644 --- a/modules/jooby-jstachio/pom.xml +++ b/modules/jooby-jstachio/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-jstachio jooby-jstachio diff --git a/modules/jooby-jte/pom.xml b/modules/jooby-jte/pom.xml index a794fbb846..eebb43aa0a 100644 --- a/modules/jooby-jte/pom.xml +++ b/modules/jooby-jte/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-jte jooby-jte diff --git a/modules/jooby-jwt/pom.xml b/modules/jooby-jwt/pom.xml index 23baf4a67d..1a3f1a8a21 100644 --- a/modules/jooby-jwt/pom.xml +++ b/modules/jooby-jwt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-jwt jooby-jwt diff --git a/modules/jooby-kafka/pom.xml b/modules/jooby-kafka/pom.xml index 0203249609..433b5fc676 100644 --- a/modules/jooby-kafka/pom.xml +++ b/modules/jooby-kafka/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-kafka jooby-kafka diff --git a/modules/jooby-kotlin/pom.xml b/modules/jooby-kotlin/pom.xml index 32efb7d334..2382238c78 100644 --- a/modules/jooby-kotlin/pom.xml +++ b/modules/jooby-kotlin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-kotlin jooby-kotlin diff --git a/modules/jooby-log4j/pom.xml b/modules/jooby-log4j/pom.xml index ed4d2c42c6..82860b042f 100644 --- a/modules/jooby-log4j/pom.xml +++ b/modules/jooby-log4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-log4j jooby-log4j diff --git a/modules/jooby-logback/pom.xml b/modules/jooby-logback/pom.xml index 12397ed3db..e14e0ef47d 100644 --- a/modules/jooby-logback/pom.xml +++ b/modules/jooby-logback/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-logback jooby-logback diff --git a/modules/jooby-maven-plugin/pom.xml b/modules/jooby-maven-plugin/pom.xml index 2cb9798d81..5519e03b41 100644 --- a/modules/jooby-maven-plugin/pom.xml +++ b/modules/jooby-maven-plugin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-maven-plugin jooby-maven-plugin diff --git a/modules/jooby-metrics/pom.xml b/modules/jooby-metrics/pom.xml index 0474ccd62c..1d309a9ea7 100644 --- a/modules/jooby-metrics/pom.xml +++ b/modules/jooby-metrics/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-metrics jooby-metrics diff --git a/modules/jooby-mutiny/pom.xml b/modules/jooby-mutiny/pom.xml index 75ecf14a15..07f2200158 100644 --- a/modules/jooby-mutiny/pom.xml +++ b/modules/jooby-mutiny/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-mutiny jooby-mutiny diff --git a/modules/jooby-netty/pom.xml b/modules/jooby-netty/pom.xml index cfa0a5f334..d8846c1b6c 100644 --- a/modules/jooby-netty/pom.xml +++ b/modules/jooby-netty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-netty jooby-netty diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 382f8c1d13..daeb7eed55 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-openapi jooby-openapi diff --git a/modules/jooby-pac4j/pom.xml b/modules/jooby-pac4j/pom.xml index 01f361e151..12575e49d5 100644 --- a/modules/jooby-pac4j/pom.xml +++ b/modules/jooby-pac4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-pac4j jooby-pac4j diff --git a/modules/jooby-pebble/pom.xml b/modules/jooby-pebble/pom.xml index 50e8fec3fb..c0545dd3aa 100644 --- a/modules/jooby-pebble/pom.xml +++ b/modules/jooby-pebble/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-pebble jooby-pebble diff --git a/modules/jooby-quartz/pom.xml b/modules/jooby-quartz/pom.xml index 4da9a37f53..9fd4210df4 100644 --- a/modules/jooby-quartz/pom.xml +++ b/modules/jooby-quartz/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-quartz jooby-quartz diff --git a/modules/jooby-reactor/pom.xml b/modules/jooby-reactor/pom.xml index ee434f510f..0f1fb1c4ac 100644 --- a/modules/jooby-reactor/pom.xml +++ b/modules/jooby-reactor/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-reactor jooby-reactor diff --git a/modules/jooby-redis/pom.xml b/modules/jooby-redis/pom.xml index 51d2011202..3a0465c215 100644 --- a/modules/jooby-redis/pom.xml +++ b/modules/jooby-redis/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-redis jooby-redis diff --git a/modules/jooby-redoc/pom.xml b/modules/jooby-redoc/pom.xml index 094ae17dc2..07bcc8d601 100644 --- a/modules/jooby-redoc/pom.xml +++ b/modules/jooby-redoc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-redoc jooby-redoc diff --git a/modules/jooby-rocker/pom.xml b/modules/jooby-rocker/pom.xml index 0c9e1c3718..738a963b70 100644 --- a/modules/jooby-rocker/pom.xml +++ b/modules/jooby-rocker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-rocker jooby-rocker diff --git a/modules/jooby-run/pom.xml b/modules/jooby-run/pom.xml index 03420649c7..af9f84953c 100644 --- a/modules/jooby-run/pom.xml +++ b/modules/jooby-run/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-run jooby-run diff --git a/modules/jooby-rxjava3/pom.xml b/modules/jooby-rxjava3/pom.xml index 94e018d5eb..828486ce37 100644 --- a/modules/jooby-rxjava3/pom.xml +++ b/modules/jooby-rxjava3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-rxjava3 jooby-rxjava3 diff --git a/modules/jooby-stork/pom.xml b/modules/jooby-stork/pom.xml index f1b9a5b7b9..7129c86b87 100644 --- a/modules/jooby-stork/pom.xml +++ b/modules/jooby-stork/pom.xml @@ -4,7 +4,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-stork diff --git a/modules/jooby-swagger-ui/pom.xml b/modules/jooby-swagger-ui/pom.xml index 49abea4b73..e65f6e6473 100644 --- a/modules/jooby-swagger-ui/pom.xml +++ b/modules/jooby-swagger-ui/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-swagger-ui jooby-swagger-ui diff --git a/modules/jooby-test/pom.xml b/modules/jooby-test/pom.xml index 368f743642..7e28a167bd 100644 --- a/modules/jooby-test/pom.xml +++ b/modules/jooby-test/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-test jooby-test diff --git a/modules/jooby-thymeleaf/pom.xml b/modules/jooby-thymeleaf/pom.xml index cb160fe460..4c17817897 100644 --- a/modules/jooby-thymeleaf/pom.xml +++ b/modules/jooby-thymeleaf/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-thymeleaf jooby-thymeleaf diff --git a/modules/jooby-undertow/pom.xml b/modules/jooby-undertow/pom.xml index ffb82911e9..5b56363b9e 100644 --- a/modules/jooby-undertow/pom.xml +++ b/modules/jooby-undertow/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-undertow jooby-undertow diff --git a/modules/jooby-vertx-mysql-client/pom.xml b/modules/jooby-vertx-mysql-client/pom.xml index f94ec98050..1d2e0537c0 100644 --- a/modules/jooby-vertx-mysql-client/pom.xml +++ b/modules/jooby-vertx-mysql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-vertx-mysql-client jooby-vertx-mysql-client diff --git a/modules/jooby-vertx-pg-client/pom.xml b/modules/jooby-vertx-pg-client/pom.xml index 23c4957834..52966587ab 100644 --- a/modules/jooby-vertx-pg-client/pom.xml +++ b/modules/jooby-vertx-pg-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-vertx-pg-client jooby-vertx-pg-client diff --git a/modules/jooby-vertx-sql-client/pom.xml b/modules/jooby-vertx-sql-client/pom.xml index 2c35774643..add34dee44 100644 --- a/modules/jooby-vertx-sql-client/pom.xml +++ b/modules/jooby-vertx-sql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-vertx-sql-client jooby-vertx-sql-client diff --git a/modules/jooby-vertx/pom.xml b/modules/jooby-vertx/pom.xml index b98a7ed507..b5a3039813 100644 --- a/modules/jooby-vertx/pom.xml +++ b/modules/jooby-vertx/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-vertx jooby-vertx diff --git a/modules/jooby-whoops/pom.xml b/modules/jooby-whoops/pom.xml index 2d49951015..5b7c684d88 100644 --- a/modules/jooby-whoops/pom.xml +++ b/modules/jooby-whoops/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-whoops jooby-whoops diff --git a/modules/jooby-yasson/pom.xml b/modules/jooby-yasson/pom.xml index 894b7ad878..d5e8336c47 100644 --- a/modules/jooby-yasson/pom.xml +++ b/modules/jooby-yasson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.13-SNAPSHOT + 4.0.13 jooby-yasson jooby-yasson diff --git a/modules/pom.xml b/modules/pom.xml index d40ce5473a..d2f28ca017 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -4,7 +4,7 @@ io.jooby jooby-project - 4.0.13-SNAPSHOT + 4.0.13 modules diff --git a/pom.xml b/pom.xml index fba0dd86b5..6a2b9d17bf 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.jooby jooby-project - 4.0.13-SNAPSHOT + 4.0.13 pom jooby-project @@ -210,7 +210,7 @@ 21 21 yyyy-MM-dd HH:mm:ssa - 2025-11-26T13:19:32Z + 2025-12-22T20:03:05Z UTF-8 etc${file.separator}source${file.separator}formatter.sh diff --git a/tests/pom.xml b/tests/pom.xml index d7ac4e0c4c..1ded1bb9c1 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.0.13-SNAPSHOT + 4.0.13 tests tests