From 8232433a58b23f5659f98b77931a99ed53f0a1f2 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 11 Aug 2025 16:40:48 +0200 Subject: [PATCH 01/23] VERTXLIB-62 Vert.x 5 WIP --- core/pom.xml | 10 ++++-- .../java/org/folio/tlib/RouterCreator.java | 9 ++--- .../java/org/folio/tlib/api/Tenant2Api.java | 34 ++++++++++--------- mod-example/pom.xml | 10 ++++-- .../tlib/example/service/BookService.java | 2 +- pom.xml | 14 +++++--- 6 files changed, 48 insertions(+), 31 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 4a7e79b..e73ac52 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -22,15 +22,19 @@ io.vertx - vertx-web-openapi + vertx-openapi io.vertx - vertx-rx-java2 + vertx-web-openapi-router io.vertx - vertx-web-api-contract + vertx-web-validation + + + io.vertx + vertx-rx-java2 io.vertx diff --git a/core/src/main/java/org/folio/tlib/RouterCreator.java b/core/src/main/java/org/folio/tlib/RouterCreator.java index 6c6b282..7a9330a 100644 --- a/core/src/main/java/org/folio/tlib/RouterCreator.java +++ b/core/src/main/java/org/folio/tlib/RouterCreator.java @@ -5,6 +5,7 @@ import io.vertx.core.Vertx; import io.vertx.ext.web.Router; import org.folio.okapi.common.XOkapiHeaders; +import org.folio.okapi.common.logging.FolioLocal; import org.folio.okapi.common.logging.FolioLoggingContext; /** @@ -40,13 +41,13 @@ static Future mountAll(Vertx vertx, RouterCreator [] routerCreators, Str Future future = Future.succeededFuture(); Router router = Router.router(vertx); router.route().handler(ctx -> { - FolioLoggingContext.put(FolioLoggingContext.MODULE_ID_LOGGING_VAR_NAME, module); + FolioLoggingContext.put(FolioLocal.MODULE_ID, module); MultiMap headers = ctx.request().headers(); - FolioLoggingContext.put(FolioLoggingContext.TENANT_ID_LOGGING_VAR_NAME, + FolioLoggingContext.put(FolioLocal.TENANT_ID, headers.get(XOkapiHeaders.TENANT)); - FolioLoggingContext.put(FolioLoggingContext.REQUEST_ID_LOGGING_VAR_NAME, + FolioLoggingContext.put(FolioLocal.REQUEST_ID, headers.get(XOkapiHeaders.REQUEST_ID)); - FolioLoggingContext.put(FolioLoggingContext.USER_ID_LOGGING_VAR_NAME, + FolioLoggingContext.put(FolioLocal.USER_ID, headers.get(XOkapiHeaders.USER_ID)); ctx.next(); }); diff --git a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java index db5aaf5..d3cde75 100644 --- a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java +++ b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java @@ -8,10 +8,11 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.openapi.RouterBuilder; +import io.vertx.ext.web.openapi.router.RouterBuilder; import io.vertx.ext.web.validation.RequestParameter; import io.vertx.ext.web.validation.RequestParameters; import io.vertx.ext.web.validation.ValidationHandler; +import io.vertx.openapi.contract.OpenAPIContract; import io.vertx.sqlclient.Tuple; import java.util.HashMap; import java.util.LinkedList; @@ -187,9 +188,9 @@ private static Future saveJob(Vertx vertx, JsonObject tenantJob) { private void handlers(Vertx vertx, RouterBuilder routerBuilder) { log.info("setting up tenant handlers ... begin"); - routerBuilder - .operation("postTenant") - .handler(ctx -> { + + routerBuilder.getRoute("postTenant") + .addHandler(ctx -> { RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY); log.info("postTenant handler {}", params.toJson().encode()); JsonObject tenantAttributes = ctx.body().asJsonObject(); @@ -218,10 +219,10 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { failHandler(ctx, 500, e); }); }) - .failureHandler(ctx -> Tenant2Api.failHandler(ctx, 400, ctx.failure())); - routerBuilder - .operation("getTenantJob") - .handler(ctx -> { + .addFailureHandler(ctx -> Tenant2Api.failHandler(ctx, 400, ctx.failure())); + + routerBuilder.getRoute("getTenantJob") + .addHandler(ctx -> { RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY); String id = params.pathParameter("id").getString(); String tenant = params.headerParameter(XOkapiHeaders.TENANT).getString(); @@ -241,10 +242,10 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { }) .onFailure(e -> failHandler(ctx, 500, e)); }) - .failureHandler(ctx -> Tenant2Api.failHandler(ctx, 400, ctx.failure())); - routerBuilder - .operation("deleteTenantJob") - .handler(ctx -> { + .addFailureHandler(ctx -> Tenant2Api.failHandler(ctx, 400, ctx.failure())); + + routerBuilder.getRoute("deleteTenantJob") + .addHandler(ctx -> { RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY); String id = params.pathParameter("id").getString(); String tenant = params.headerParameter(XOkapiHeaders.TENANT).getString(); @@ -260,7 +261,8 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { }) .onFailure(e -> failHandler(ctx, 500, e)); }) - .failureHandler(ctx -> Tenant2Api.failHandler(ctx, 400, ctx.failure())); + .addFailureHandler(ctx -> Tenant2Api.failHandler(ctx, 400, ctx.failure())); + log.info("setting up tenant handlers ... done"); } @@ -272,11 +274,11 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { */ @Override public Future createRouter(Vertx vertx) { - return RouterBuilder.create(vertx, "openapi/tenant-2.0.yaml") - .map(routerBuilder -> { + return OpenAPIContract.from(vertx, "openapi/tenant-2.0.yaml") + .map(contract -> { + RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); handlers(vertx, routerBuilder); return routerBuilder.createRouter(); }); } - } diff --git a/mod-example/pom.xml b/mod-example/pom.xml index 55d4ee2..56656c5 100644 --- a/mod-example/pom.xml +++ b/mod-example/pom.xml @@ -29,15 +29,19 @@ io.vertx - vertx-web-openapi + vertx-openapi io.vertx - vertx-rx-java2 + vertx-web-openapi-router io.vertx - vertx-web-api-contract + vertx-web-validation + + + io.vertx + vertx-rx-java2 io.vertx diff --git a/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java b/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java index 65eb0bf..9dd710d 100644 --- a/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java +++ b/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java @@ -9,7 +9,7 @@ import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; -import io.vertx.ext.web.openapi.RouterBuilder; +import io.vertx.ext.web.openapi.router.RouterBuilder; import io.vertx.ext.web.validation.RequestParameters; import io.vertx.ext.web.validation.ValidationHandler; import java.util.UUID; diff --git a/pom.xml b/pom.xml index 99252ba..c3a7113 100644 --- a/pom.xml +++ b/pom.xml @@ -29,8 +29,8 @@ UTF-8 - 6.2.0 - 4.5.13 + 6.3.0-SNAPSHOT + 5.0.2 core @@ -42,7 +42,7 @@ org.apache.logging.log4j log4j-bom - 2.24.1 + 2.24.3 pom import @@ -146,6 +146,13 @@ io.vertx.codegen.CodeGenProcessor + + + io.vertx + vertx-codegen + ${vertx.version} + + @@ -247,7 +254,6 @@ 3.5.0 google_checks.xml - UTF-8 warning false true From c3ed27239fc10b9793b7d4569253ac2303920009 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 13 Aug 2025 15:47:21 +0200 Subject: [PATCH 02/23] Further --- .../java/org/folio/tlib/api/Tenant2Api.java | 8 +-- .../org/folio/tlib/postgres/TenantPgPool.java | 5 +- .../tlib/postgres/impl/TenantPgPoolImpl.java | 53 ++++++------------- core/src/main/resources/log4j2.properties | 24 ++++++--- .../test/java/org/folio/tlib/api/EchoApi.java | 29 +++++----- .../folio/tlib/postgres/PgCqlStorageTest.java | 15 +++--- .../folio/tlib/postgres/TenantPgPoolTest.java | 21 ++------ .../org/folio/tlib/example/data/Book.java | 14 ++++- .../tlib/example/service/BookService.java | 39 +++++++------- .../tlib/example/storage/BookStorage.java | 5 +- pom.xml | 1 + 11 files changed, 101 insertions(+), 113 deletions(-) diff --git a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java index d3cde75..4f615fd 100644 --- a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java +++ b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java @@ -276,9 +276,9 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { public Future createRouter(Vertx vertx) { return OpenAPIContract.from(vertx, "openapi/tenant-2.0.yaml") .map(contract -> { - RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); - handlers(vertx, routerBuilder); - return routerBuilder.createRouter(); - }); + RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); + handlers(vertx, routerBuilder); + return routerBuilder.createRouter(); + }); } } diff --git a/core/src/main/java/org/folio/tlib/postgres/TenantPgPool.java b/core/src/main/java/org/folio/tlib/postgres/TenantPgPool.java index 433bb15..b3ddec1 100644 --- a/core/src/main/java/org/folio/tlib/postgres/TenantPgPool.java +++ b/core/src/main/java/org/folio/tlib/postgres/TenantPgPool.java @@ -3,7 +3,6 @@ import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.pgclient.PgConnectOptions; -import io.vertx.pgclient.PgPool; import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.PoolOptions; import io.vertx.sqlclient.Row; @@ -14,9 +13,9 @@ import org.folio.tlib.postgres.impl.TenantPgPoolImpl; /** - * The {@link PgPool} for a tenant. + * The {@link Pool} for a tenant. */ -public interface TenantPgPool extends PgPool { +public interface TenantPgPool extends Pool { /** * create tenant pool for tenant. diff --git a/core/src/main/java/org/folio/tlib/postgres/impl/TenantPgPoolImpl.java b/core/src/main/java/org/folio/tlib/postgres/impl/TenantPgPoolImpl.java index b5500db..5e7ec58 100644 --- a/core/src/main/java/org/folio/tlib/postgres/impl/TenantPgPoolImpl.java +++ b/core/src/main/java/org/folio/tlib/postgres/impl/TenantPgPoolImpl.java @@ -7,10 +7,11 @@ import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; +import io.vertx.core.net.ClientSSLOptions; import io.vertx.core.net.OpenSSLEngineOptions; import io.vertx.core.net.PemTrustOptions; +import io.vertx.pgclient.PgBuilder; import io.vertx.pgclient.PgConnectOptions; -import io.vertx.pgclient.PgPool; import io.vertx.pgclient.SslMode; import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.PoolOptions; @@ -29,16 +30,15 @@ import java.util.function.Function; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.folio.okapi.common.GenericCompositeFuture; import org.folio.tlib.postgres.TenantPgPool; /** - * The {@link PgPool} for a tenant. + * The {@link Pool} for a tenant. */ public class TenantPgPoolImpl implements TenantPgPool { private static final Logger log = LogManager.getLogger(TenantPgPoolImpl.class); - static Map pgPoolMap = new HashMap<>(); + static Map pgPoolMap = new HashMap<>(); static String host = System.getenv("DB_HOST"); static String port = System.getenv("DB_PORT"); @@ -53,7 +53,7 @@ public class TenantPgPoolImpl implements TenantPgPool { static PgConnectOptions pgConnectOptions = new PgConnectOptions(); final String tenant; - PgPool pgPool; + Pool pgPool; JsonObject config; final PoolOptions poolOptions; @@ -99,13 +99,13 @@ private TenantPgPoolImpl(Vertx vertx, String tenant, PoolOptions poolOptions) { /** * Create pool for Tenant. * - *

The returned pool implements PgPool interface so this cab be used like PgPool as usual. - * PgPool.setModule *must* be called before the queries are executed, since schema is based - * on module name. + *

The returned pool implements Pool interface so this can be used like Pool as usual. + * TenantPgPool.setModule *must* be called before the queries are executed, since schema is + * based on module name. * * @param vertx Vert.x handle * @param tenant Tenant - * @return pool with PgPool semantics + * @return pool with Pool semantics */ public static TenantPgPoolImpl tenantPgPool(Vertx vertx, String tenant) { if (module == null) { @@ -138,11 +138,11 @@ public static TenantPgPoolImpl tenantPgPool(Vertx vertx, String tenant) { } if (serverPem != null) { connectOptions.setSslMode(SslMode.VERIFY_FULL); - connectOptions.setHostnameVerificationAlgorithm("HTTPS"); - connectOptions.setPemTrustOptions( - new PemTrustOptions().addCertValue(Buffer.buffer(serverPem))); - connectOptions.setEnabledSecureTransportProtocols(Collections.singleton("TLSv1.3")); - connectOptions.setOpenSslEngineOptions(new OpenSSLEngineOptions()); + ClientSSLOptions cso = new ClientSSLOptions(); + cso.setHostnameVerificationAlgorithm("HTTPS"); + cso.setTrustOptions(new PemTrustOptions().addCertValue(Buffer.buffer(serverPem))); + cso.setEnabledSecureTransportProtocols(Collections.singleton("TLSv1.3")); + connectOptions.setSslOptions(cso); } PoolOptions poolOptions = new PoolOptions(); if (maxPoolSize != null) { @@ -150,7 +150,7 @@ public static TenantPgPoolImpl tenantPgPool(Vertx vertx, String tenant) { } TenantPgPoolImpl tenantPgPool = new TenantPgPoolImpl(vertx, sanitize(tenant), poolOptions); tenantPgPool.pgPool = pgPoolMap.computeIfAbsent(connectOptions, key -> - PgPool.pool(vertx, connectOptions, poolOptions)); + PgBuilder.pool().using(vertx).connectingTo(connectOptions).with(poolOptions).build()); return tenantPgPool; } @@ -169,10 +169,6 @@ public Pool getPool() { return pgPool; } - @Override - public void getConnection(Handler> handler) { - pgPool.getConnection(handler); - } @Override public Future getConnection() { @@ -196,13 +192,6 @@ public PreparedQuery> preparedQuery(String s, PrepareOptions prepare return pgPool.preparedQuery(s, prepareOptions); } - @Override - public void close(Handler> handler) { - // release our pool from the map - while (pgPoolMap.values().remove(pgPool)) { } - pgPool.close(handler); - } - @Override public Future close() { // release our pool from the map @@ -254,16 +243,6 @@ Future explainAnalyze(String sql, Tuple tuple) { }).mapEmpty(); } - @Override - public PgPool connectHandler(Handler handler) { - return pgPool.connectHandler(handler); - } - - @Override - public PgPool connectionProvider(Function> function) { - return pgPool.connectionProvider(function); - } - @Override public int size() { return pgPool.size(); @@ -277,7 +256,7 @@ public int size() { public static Future closeAll() { List> futures = new ArrayList<>(pgPoolMap.size()); pgPoolMap.forEach((a, b) -> futures.add(b.close())); - return GenericCompositeFuture.all(futures) + return Future.all(futures) .onComplete(x -> pgPoolMap.clear()) .mapEmpty(); } diff --git a/core/src/main/resources/log4j2.properties b/core/src/main/resources/log4j2.properties index 48964d0..38032d8 100644 --- a/core/src/main/resources/log4j2.properties +++ b/core/src/main/resources/log4j2.properties @@ -1,19 +1,27 @@ status = error name = PropertiesConfig -packages = org.folio.okapi.common.logging filters = threshold filter.threshold.type = ThresholdFilter filter.threshold.level = info -appenders = console +appender.full.type = Console +appender.full.name = FULL +appender.full.layout.type = PatternLayout +appender.full.layout.pattern = %d{HH:mm:ss} [${map:requestid}] [${map:tenantid}] [${map:userid}] [${map:moduleid}] %-5p %-20.20C{1} %m%n -appender.console.type = Console -appender.console.name = STDOUT -appender.console.layout.type = PatternLayout -appender.console.layout.pattern = %d{HH:mm:ss} [$${FolioLoggingContext:requestid}] [$${FolioLoggingContext:tenantid}] [$${FolioLoggingContext:userid}] [$${FolioLoggingContext:moduleid}] %-5p %-20.20C{1} %m%n +appender.empty.type = Console +appender.empty.name = EMPTY +appender.empty.layout.type = PatternLayout +appender.empty.layout.pattern = %d{HH:mm:ss} [] [] [] [] %-5p %-20.20C{1} %m%n rootLogger.level = info -rootLogger.appenderRefs = info -rootLogger.appenderRef.stdout.ref = STDOUT +rootLogger.appenderRefs = empty +rootLogger.appenderRef.basic.ref = EMPTY + +logger.full.level = info +logger.full.appenderRefs = full +logger.full.appenderRef.full.ref = FULL +logger.full.name = full +logger.full.additivity = false diff --git a/core/src/test/java/org/folio/tlib/api/EchoApi.java b/core/src/test/java/org/folio/tlib/api/EchoApi.java index add8d54..0d49667 100644 --- a/core/src/test/java/org/folio/tlib/api/EchoApi.java +++ b/core/src/test/java/org/folio/tlib/api/EchoApi.java @@ -6,7 +6,9 @@ import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; -import io.vertx.ext.web.openapi.RouterBuilder; +import io.vertx.ext.web.openapi.router.RouterBuilder; +import io.vertx.openapi.contract.OpenAPIContract; + import org.folio.tlib.RouterCreator; public class EchoApi implements RouterCreator { @@ -29,18 +31,19 @@ static void handleError(RoutingContext ctx, int status, String msg) { @Override public Future createRouter(Vertx vertx) { - return RouterBuilder.create(vertx, "openapi/echo.yaml") - .map(routerBuilder -> { - // https://vertx.io/docs/vertx-web/java/#_limiting_body_size - routerBuilder.rootHandler(BodyHandler.create().setBodyLimit(BODY_LIMIT)); - routerBuilder - .operation("echo") // operationId in spec - .handler(ctx -> echo(ctx) - .onFailure(e -> handleError(ctx, 500, e)) - ) - .failureHandler(ctx -> handleError(ctx, 400, ctx.failure())); - return routerBuilder.createRouter(); - }); + return OpenAPIContract.from(vertx, "openapi/echo.yaml") + .map(contract -> { + RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); + //routerBuilder.map(routerBuilder -> { + // https://vertx.io/docs/vertx-web/java/#_limiting_body_size + routerBuilder.rootHandler(BodyHandler.create().setBodyLimit(BODY_LIMIT)); + routerBuilder.getRoute("echo") // operationId in spec + .addHandler(ctx -> echo(ctx) + .onFailure(e -> handleError(ctx, 500, e)) + ) + .addFailureHandler(ctx -> handleError(ctx, 400, ctx.failure())); + return routerBuilder.createRouter(); + }); } Future echo(RoutingContext ctx) { diff --git a/core/src/test/java/org/folio/tlib/postgres/PgCqlStorageTest.java b/core/src/test/java/org/folio/tlib/postgres/PgCqlStorageTest.java index 95e056b..d0858f6 100644 --- a/core/src/test/java/org/folio/tlib/postgres/PgCqlStorageTest.java +++ b/core/src/test/java/org/folio/tlib/postgres/PgCqlStorageTest.java @@ -5,8 +5,9 @@ import io.vertx.core.json.JsonObject; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; +import io.vertx.pgclient.PgBuilder; import io.vertx.pgclient.PgConnectOptions; -import io.vertx.pgclient.PgPool; +import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.PoolOptions; import java.util.HashSet; import java.util.Iterator; @@ -42,7 +43,7 @@ class PgCqlStorageTest { @Container static final PostgreSQLContainer container = TenantPgPoolContainer.create(); - public static PgPool pgPool; + public static Pool pgPool; static List batch = List.of( Tuple.of(UUID.randomUUID(), "On the road with Bob Dylan", "Larry \"Ratso\" Sloman"), @@ -53,14 +54,16 @@ class PgCqlStorageTest { @BeforeAll static void beforeAll(Vertx vertx, VertxTestContext context) { - pgPool = PgPool.pool(vertx, - new PgConnectOptions() + PgConnectOptions connectOptions = new PgConnectOptions() .setPort(container.getFirstMappedPort()) .setHost(container.getHost()) .setDatabase(container.getDatabaseName()) .setUser(container.getUsername()) - .setPassword(container.getPassword()), - new PoolOptions().setMaxSize(2)); + .setPassword(container.getPassword()); + + PoolOptions poolOptions = new PoolOptions().setMaxSize(2); + pgPool = PgBuilder.pool().using(vertx).connectingTo(connectOptions).with(poolOptions).build(); + pgPool.query("CREATE TABLE entries (id UUID, title TEXT, author TEXT)") .execute() .compose(x -> insertSample()) diff --git a/core/src/test/java/org/folio/tlib/postgres/TenantPgPoolTest.java b/core/src/test/java/org/folio/tlib/postgres/TenantPgPoolTest.java index 314ff08..88cb1b6 100644 --- a/core/src/test/java/org/folio/tlib/postgres/TenantPgPoolTest.java +++ b/core/src/test/java/org/folio/tlib/postgres/TenantPgPoolTest.java @@ -8,7 +8,6 @@ import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; import io.vertx.sqlclient.PrepareOptions; -import io.vertx.sqlclient.SqlConnection; import io.vertx.sqlclient.Tuple; import io.vertx.sqlclient.templates.SqlTemplate; import java.io.IOException; @@ -101,7 +100,7 @@ void after(Vertx vertx, VertxTestContext context) { private Future withPool(Vertx vertx, Function> mapper) { TenantPgPool pool = TenantPgPool.pool(vertx, "diku"); Future future = mapper.apply(pool); - return future.eventually(x -> pool.close()); + return future.onComplete(x -> pool.close()); } @Test @@ -186,7 +185,7 @@ void getConnection1(Vertx vertx, VertxTestContext context) { @SuppressWarnings("squid:S2699") // "Add at least one assertion" SQ does not know about context.* void getConnection2(Vertx vertx, VertxTestContext context) { withPool(vertx, pool -> - Future.future(promise -> pool.getConnection(promise)) + pool.getConnection() .compose(con -> con.query("SELECT count(*) FROM pg_database").execute())) .onComplete(context.succeedingThenComplete()); } @@ -265,7 +264,7 @@ void size(Vertx vertx, VertxTestContext context) { @SuppressWarnings("squid:S2699") // "Add at least one assertion" SQ does not know about context.* void close(Vertx vertx, VertxTestContext context) { TenantPgPool pool = TenantPgPool.pool(vertx, "diku"); - pool.close(context.succeedingThenComplete()); + pool.close().onComplete(context.succeedingThenComplete()); } @Test @@ -275,18 +274,4 @@ void closeAll(Vertx vertx, VertxTestContext context) { TenantPgPool.closeAll().onComplete(context.succeedingThenComplete()); } - @Test - void connectHandler(Vertx vertx, VertxTestContext context) { - TenantPgPool pool = TenantPgPool.pool(vertx, "diku"); - pool.connectHandler(conn -> - conn.query("CREATE TEMP TABLE connecthandler()") - .execute() - .eventually(x -> conn.close()) - ); - pool.withConnection(conn -> conn.preparedQuery("SELECT * FROM connecthandler").execute()) - .onComplete(context.succeeding(rowSet -> { - assertThat(rowSet.size(), is(0)); - context.completeNow(); - })); - } } diff --git a/mod-example/src/main/java/org/folio/tlib/example/data/Book.java b/mod-example/src/main/java/org/folio/tlib/example/data/Book.java index 0738010..86aadef 100644 --- a/mod-example/src/main/java/org/folio/tlib/example/data/Book.java +++ b/mod-example/src/main/java/org/folio/tlib/example/data/Book.java @@ -2,12 +2,13 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.vertx.codegen.annotations.DataObject; +import io.vertx.sqlclient.Row; import io.vertx.sqlclient.templates.annotations.Column; import io.vertx.sqlclient.templates.annotations.RowMapped; import java.util.UUID; /** - * Book DAO. + * Book object. */ @DataObject @RowMapped @@ -21,6 +22,17 @@ public class Book { @Column(name = "index_title") private String indexTitle; + /** + * The BookRowMapper class is not generated by vertx-codegen, so we use this for now. + */ + public static Book fromRow(Row row) { + Book book = new Book(); + book.setId(row.get(UUID.class, "id")); + book.setTitle(row.getString("title")); + book.setIndexTitle(row.getString("index_title")); + return book; + } + public UUID getId() { return id; } diff --git a/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java b/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java index 9dd710d..1922ca5 100644 --- a/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java +++ b/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java @@ -12,6 +12,7 @@ import io.vertx.ext.web.openapi.router.RouterBuilder; import io.vertx.ext.web.validation.RequestParameters; import io.vertx.ext.web.validation.ValidationHandler; +import io.vertx.openapi.contract.OpenAPIContract; import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -33,14 +34,15 @@ public class BookService implements RouterCreator, TenantInitHooks { @Override public Future createRouter(Vertx vertx) { - return RouterBuilder.create(vertx, "openapi/books-1.0.yaml") - .map(routerBuilder -> { - // https://vertx.io/docs/vertx-web/java/#_limiting_body_size - routerBuilder.rootHandler(BodyHandler.create().setBodyLimit(BODY_LIMIT)); - handlers(vertx, routerBuilder); - return routerBuilder.createRouter(); - }) - .onSuccess(res -> log.info("OpenAPI parsed OK")); + return OpenAPIContract.from(vertx, "openapi/books-1.0.yaml") + .map(contract -> { + RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); + // https://vertx.io/docs/vertx-web/java/#_limiting_body_size + routerBuilder.rootHandler(BodyHandler.create().setBodyLimit(BODY_LIMIT)); + handlers(vertx, routerBuilder); + return routerBuilder.createRouter(); + }) + .onSuccess(res -> log.info("OpenAPI parsed OK")); } private void handleContextFailure(RoutingContext ctx) { @@ -66,24 +68,21 @@ private void handleContextFailure(RoutingContext ctx) { * @param routerBuilder OpenAPI router builder */ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { - routerBuilder - .operation("postBook") // operationId in spec - .handler(ctx -> postBook(vertx, ctx) + routerBuilder.getRoute("postBook") // operationId in spec + .addHandler(ctx -> postBook(vertx, ctx) .onFailure(cause -> HttpResponse.responseError(ctx, 500, cause.getMessage())) ) - .failureHandler(this::handleContextFailure); - routerBuilder - .operation("getBook") - .handler(ctx -> getBook(vertx, ctx) + .addFailureHandler(this::handleContextFailure); + routerBuilder.getRoute("getBook") + .addHandler(ctx -> getBook(vertx, ctx) .onFailure(cause -> HttpResponse.responseError(ctx, 500, cause.getMessage())) ) - .failureHandler(this::handleContextFailure); - routerBuilder - .operation("getBooks") - .handler(ctx -> getBooks(vertx, ctx) + .addFailureHandler(this::handleContextFailure); + routerBuilder.getRoute("getBooks") + .addHandler(ctx -> getBooks(vertx, ctx) .onFailure(cause -> HttpResponse.responseError(ctx, 500, cause.getMessage())) ) - .failureHandler(this::handleContextFailure); + .addFailureHandler(this::handleContextFailure); } @Override diff --git a/mod-example/src/main/java/org/folio/tlib/example/storage/BookStorage.java b/mod-example/src/main/java/org/folio/tlib/example/storage/BookStorage.java index ea4e610..305b68a 100644 --- a/mod-example/src/main/java/org/folio/tlib/example/storage/BookStorage.java +++ b/mod-example/src/main/java/org/folio/tlib/example/storage/BookStorage.java @@ -15,7 +15,6 @@ import java.util.List; import java.util.UUID; import org.folio.tlib.example.data.Book; -import org.folio.tlib.example.data.BookRowMapper; import org.folio.tlib.postgres.PgCqlDefinition; import org.folio.tlib.postgres.PgCqlQuery; import org.folio.tlib.postgres.TenantPgPool; @@ -81,7 +80,7 @@ public Future init(JsonObject tenantAttributes) { public Future getBook(UUID id) { return SqlTemplate.forQuery(pool.getPool(), "SELECT * FROM " + getMyTable(pool) + " WHERE id=#{id}") - .mapTo(BookRowMapper.INSTANCE) + .mapTo(Book::fromRow) .execute(Collections.singletonMap("id", id)) .map(rowSet -> { RowIterator iterator = rowSet.iterator(); @@ -141,7 +140,7 @@ private String createQueryMyTable(RoutingContext ctx, TenantPgPool pool) { public Future> getBooks(RoutingContext ctx) { String sql = createQueryMyTable(ctx, pool); return SqlTemplate.forQuery(pool.getPool(), sql) - .mapTo(BookRowMapper.INSTANCE) + .mapTo(Book::fromRow) .execute(Collections.emptyMap()) .map(rowSet -> { List books = new LinkedList<>(); diff --git a/pom.xml b/pom.xml index c3a7113..e1ee8dd 100644 --- a/pom.xml +++ b/pom.xml @@ -151,6 +151,7 @@ io.vertx vertx-codegen ${vertx.version} + processor From d5ab1121821bd28b3f5a524dbe0f0045bf13a9f4 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Thu, 14 Aug 2025 13:20:07 +0200 Subject: [PATCH 03/23] Trying to resolve $ref However, it does not consider JSON schemas referring to other JSON schemas. --- core/pom.xml | 16 ++++ .../main/java/org/folio/tlib/OpenApiRef.java | 78 +++++++++++++++++++ .../java/org/folio/tlib/api/Tenant2Api.java | 10 ++- .../main/resources/openapi/tenant-2.0.yaml | 2 - .../java/org/folio/tlib/OpenApiRefTest.java | 36 +++++++++ .../test/java/org/folio/tlib/api/EchoApi.java | 9 ++- core/src/test/resources/openapi/reftest.yaml | 42 ++++++++++ 7 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/org/folio/tlib/OpenApiRef.java create mode 100644 core/src/test/java/org/folio/tlib/OpenApiRefTest.java create mode 100644 core/src/test/resources/openapi/reftest.yaml diff --git a/core/pom.xml b/core/pom.xml index e73ac52..0330621 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,6 +12,16 @@ io.vertx vertx-core + + com.fasterxml.jackson.core + jackson-databind + 2.17.1 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.17.1 + io.vertx vertx-web @@ -74,6 +84,12 @@ com.ongres.scram client + + + io.swagger.parser.v3 + swagger-parser + 2.1.31 + org.junit.jupiter diff --git a/core/src/main/java/org/folio/tlib/OpenApiRef.java b/core/src/main/java/org/folio/tlib/OpenApiRef.java new file mode 100644 index 0000000..9a41aee --- /dev/null +++ b/core/src/main/java/org/folio/tlib/OpenApiRef.java @@ -0,0 +1,78 @@ +package org.folio.tlib; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.ParseOptions; +import io.swagger.v3.parser.core.models.SwaggerParseResult; +import java.io.File; +import java.io.IOException; + +/** + * Class for resolving OpenAPI $ref references. + */ +public class OpenApiRef { + /** + * Resolves $ref in OpenAPI, suitable for Vert.x OpenAPI parser. + * + * @param path local-URI to OpenAPI YAML file + * @return resolved file in target + * @throws IOException if file could be found or similar + */ + public static String fix(String path) throws IOException { + // get filename portion of path + String refFilename = new File(path).getName(); + refFilename = "target/" + refFilename.replace(".yaml", "_ref.yaml"); + fix(path, refFilename); + return refFilename; + } + + static void fix(String inputPath, String outputPath) throws IOException { + ParseOptions parseOptions = new ParseOptions(); + parseOptions.setResolve(true); + SwaggerParseResult result = new OpenAPIV3Parser().readLocation(inputPath, null, parseOptions); + OpenAPI openApi = result.getOpenAPI(); + if (openApi == null) { + throw new IOException("Failed to parse OpenAPI: " + result.getMessages()); + } + ObjectMapper mapper; + mapper = new ObjectMapper(new YAMLFactory()); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + // Convert OpenAPI object to a tree for post-processing + JsonNode tree = mapper.valueToTree(openApi); + // swagger parser produces some properties that Vert.x openapi does not recognize. Omit these. + removeKeysRecursive(tree, + "exampleSetFlag", "extensions", "servers", "style", "types", "valueSetFlag"); + + mapper.writerWithDefaultPrettyPrinter().writeValue(new File(outputPath), tree); + } + + private static void removeKeysRecursive(JsonNode node, String... keys) { + if (node.isObject()) { + java.util.Iterator fieldNames = node.fieldNames(); + java.util.List toRemove = new java.util.ArrayList<>(); + while (fieldNames.hasNext()) { + String field = fieldNames.next(); + for (String k : keys) { + if (field.equals(k)) { + toRemove.add(field); + } + } + } + for (String field : toRemove) { + ((ObjectNode) node).remove(field); + } + // Recurse into children + node.fields().forEachRemaining(e -> removeKeysRecursive(e.getValue(), keys)); + } else if (node.isArray()) { + for (JsonNode item : node) { + removeKeysRecursive(item, keys); + } + } + } +} diff --git a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java index 4f615fd..8e77225 100644 --- a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java +++ b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java @@ -14,6 +14,7 @@ import io.vertx.ext.web.validation.ValidationHandler; import io.vertx.openapi.contract.OpenAPIContract; import io.vertx.sqlclient.Tuple; +import java.io.IOException; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -22,6 +23,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.okapi.common.XOkapiHeaders; +import org.folio.tlib.OpenApiRef; import org.folio.tlib.RouterCreator; import org.folio.tlib.TenantInitHooks; import org.folio.tlib.postgres.impl.TenantPgPoolImpl; @@ -274,7 +276,13 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { */ @Override public Future createRouter(Vertx vertx) { - return OpenAPIContract.from(vertx, "openapi/tenant-2.0.yaml") + String spec; + try { + spec = OpenApiRef.fix("openapi/tenant-2.0.yaml"); + } catch (IOException e) { + return Future.failedFuture(e); + } + return OpenAPIContract.from(vertx, spec) .map(contract -> { RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); handlers(vertx, routerBuilder); diff --git a/core/src/main/resources/openapi/tenant-2.0.yaml b/core/src/main/resources/openapi/tenant-2.0.yaml index 653fbcf..44905f3 100644 --- a/core/src/main/resources/openapi/tenant-2.0.yaml +++ b/core/src/main/resources/openapi/tenant-2.0.yaml @@ -16,8 +16,6 @@ paths: application/json: schema: $ref: "#/components/schemas/tenantAttributes" - example: - $ref: "examples/tenantAttributes.sample" required: true responses: "204": diff --git a/core/src/test/java/org/folio/tlib/OpenApiRefTest.java b/core/src/test/java/org/folio/tlib/OpenApiRefTest.java new file mode 100644 index 0000000..886c773 --- /dev/null +++ b/core/src/test/java/org/folio/tlib/OpenApiRefTest.java @@ -0,0 +1,36 @@ +package org.folio.tlib; + +import io.vertx.core.Vertx; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import io.vertx.openapi.contract.OpenAPIContract; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith({VertxExtension.class}) +public class OpenApiRefTest { + @Test + void test1(Vertx vertx, VertxTestContext context) throws Exception { + String input = "openapi/reftest.yaml"; + String output = "target/reftest-resolved.yaml"; + OpenApiRef.fix(input, output); + OpenAPIContract.from(vertx, "target/reftest-resolved.yaml") + .onComplete(context.succeedingThenComplete()); + } + + @Test + void test2(Vertx vertx, VertxTestContext context) throws Exception { + OpenAPIContract.from(vertx, OpenApiRef.fix("openapi/reftest.yaml")) + .onComplete(context.succeedingThenComplete()); + } + + @Test + void writeResolvedOpenApi() throws Exception { + // Reads the test YAML, resolves refs, writes to output file (YAML) + String input = "openapi/reftest.yaml"; + String output = "target/reftest-resolved.yaml"; + OpenApiRef.fix(input, output); + // Optionally, assert file exists or print path + System.out.println("Resolved OpenAPI written to: " + output); + } +} diff --git a/core/src/test/java/org/folio/tlib/api/EchoApi.java b/core/src/test/java/org/folio/tlib/api/EchoApi.java index 0d49667..82615e1 100644 --- a/core/src/test/java/org/folio/tlib/api/EchoApi.java +++ b/core/src/test/java/org/folio/tlib/api/EchoApi.java @@ -9,6 +9,7 @@ import io.vertx.ext.web.openapi.router.RouterBuilder; import io.vertx.openapi.contract.OpenAPIContract; +import org.folio.tlib.OpenApiRef; import org.folio.tlib.RouterCreator; public class EchoApi implements RouterCreator { @@ -31,7 +32,13 @@ static void handleError(RoutingContext ctx, int status, String msg) { @Override public Future createRouter(Vertx vertx) { - return OpenAPIContract.from(vertx, "openapi/echo.yaml") + String spec; + try { + spec = OpenApiRef.fix("openapi/echo.yaml"); + } catch (Exception e) { + return Future.failedFuture(e); + } + return OpenAPIContract.from(vertx, spec) .map(contract -> { RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); //routerBuilder.map(routerBuilder -> { diff --git a/core/src/test/resources/openapi/reftest.yaml b/core/src/test/resources/openapi/reftest.yaml new file mode 100644 index 0000000..4d5b73d --- /dev/null +++ b/core/src/test/resources/openapi/reftest.yaml @@ -0,0 +1,42 @@ +openapi: 3.0.0 +info: + title: ref test + version: v1 +paths: + /echo: + parameters: + - $ref: "#/components/parameters/okapi_tenant" + - $ref: headers/okapi-token.yaml + get: + description: echo request + operationId: refTestEcho + responses: + "200": + description: echo ok + content: + text/plain: + schema: + type: string + "400": + $ref: "#/components/responses/trait_400" +components: + responses: + trait_400: + description: Bad request + content: + text/plain: + schema: + type: string + example: Invalid JSON in request + application/json: + schema: + type: object + example: {"error":"Invalid JSON in request"} + parameters: + okapi_tenant: + name: X-Okapi-Tenant + in: header + required: true + schema: + type: string + pattern: '^[_a-z][_a-z0-9]*$' From 4651ea9cbd32adfd12ad8b2bc11ab55e27ddc505 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Thu, 14 Aug 2025 14:34:39 +0200 Subject: [PATCH 04/23] tenant-2.0.yaml validates with ref --- .../main/java/org/folio/tlib/OpenApiRef.java | 10 ++++--- .../main/resources/openapi/schemas/error.json | 27 ------------------- .../resources/openapi/schemas/errors.json | 19 ++++++++++++- .../resources/openapi/schemas/parameter.json | 18 ------------- .../resources/openapi/schemas/parameters.json | 17 ++++++++++-- .../main/resources/openapi/tenant-2.0.yaml | 2 ++ .../java/org/folio/tlib/OpenApiRefTest.java | 18 +++---------- .../org/folio/tlib/api/Tenant2ApiTest.java | 1 - 8 files changed, 46 insertions(+), 66 deletions(-) delete mode 100644 core/src/main/resources/openapi/schemas/error.json delete mode 100644 core/src/main/resources/openapi/schemas/parameter.json diff --git a/core/src/main/java/org/folio/tlib/OpenApiRef.java b/core/src/main/java/org/folio/tlib/OpenApiRef.java index 9a41aee..c82ab01 100644 --- a/core/src/main/java/org/folio/tlib/OpenApiRef.java +++ b/core/src/main/java/org/folio/tlib/OpenApiRef.java @@ -40,14 +40,18 @@ static void fix(String inputPath, String outputPath) throws IOException { throw new IOException("Failed to parse OpenAPI: " + result.getMessages()); } ObjectMapper mapper; - mapper = new ObjectMapper(new YAMLFactory()); + if (outputPath.endsWith(".yaml")) { + mapper = new ObjectMapper(new YAMLFactory()); + } else { + mapper = new ObjectMapper(); // JSON output + } mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // Convert OpenAPI object to a tree for post-processing JsonNode tree = mapper.valueToTree(openApi); - // swagger parser produces some properties that Vert.x openapi does not recognize. Omit these. + // swagger parser produces some properties that Vert.x OpenApi does not recognize. Omit these. removeKeysRecursive(tree, - "exampleSetFlag", "extensions", "servers", "style", "types", "valueSetFlag"); + "exampleSetFlag", "extensions", "jsonSchema", "servers", "style", "types", "valueSetFlag"); mapper.writerWithDefaultPrettyPrinter().writeValue(new File(outputPath), tree); } diff --git a/core/src/main/resources/openapi/schemas/error.json b/core/src/main/resources/openapi/schemas/error.json deleted file mode 100644 index 97a99a9..0000000 --- a/core/src/main/resources/openapi/schemas/error.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "description": "An error", - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Error message text" - }, - "type": { - "type": "string", - "description": "Error message type" - }, - "code": { - "type": "string", - "description": "Error message code" - }, - "parameters": { - "type": "object", - "description": "Error message parameters", - "$ref": "parameters.json" - } - }, - "additionalProperties": false, - "required": [ - "message" - ] -} diff --git a/core/src/main/resources/openapi/schemas/errors.json b/core/src/main/resources/openapi/schemas/errors.json index b358d4f..43e7dd9 100644 --- a/core/src/main/resources/openapi/schemas/errors.json +++ b/core/src/main/resources/openapi/schemas/errors.json @@ -7,7 +7,24 @@ "type": "array", "items": { "type": "object", - "$ref": "error.json" + "properties": { + "message": { + "type": "string", + "description": "Error message text" + }, + "type": { + "type": "string", + "description": "Error message type" + }, + "code": { + "type": "string", + "description": "Error message code" + } + }, + "additionalProperties": false, + "required": [ + "message" + ] } }, "total_records": { diff --git a/core/src/main/resources/openapi/schemas/parameter.json b/core/src/main/resources/openapi/schemas/parameter.json deleted file mode 100644 index a1a8f55..0000000 --- a/core/src/main/resources/openapi/schemas/parameter.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "description": "List of key/value parameters of an error", - "type": "object", - "properties": { - "key": { - "description": "The key for this parameter", - "type": "string" - }, - "value": { - "description": "The value of this parameter", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "key" - ] -} diff --git a/core/src/main/resources/openapi/schemas/parameters.json b/core/src/main/resources/openapi/schemas/parameters.json index 23d8eac..5a7f993 100644 --- a/core/src/main/resources/openapi/schemas/parameters.json +++ b/core/src/main/resources/openapi/schemas/parameters.json @@ -1,9 +1,22 @@ { - "description": "List of key/value parameters of an error", + "description": "List of key/value parameters", "type": "array", "items": { "type": "object", - "$ref": "parameter.json" + "properties": { + "key": { + "description": "The key for this parameter", + "type": "string" + }, + "value": { + "description": "The value of this parameter", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "key" + ] }, "additionalProperties": false } diff --git a/core/src/main/resources/openapi/tenant-2.0.yaml b/core/src/main/resources/openapi/tenant-2.0.yaml index 44905f3..3c9d2c3 100644 --- a/core/src/main/resources/openapi/tenant-2.0.yaml +++ b/core/src/main/resources/openapi/tenant-2.0.yaml @@ -130,3 +130,5 @@ components: $ref: schemas/tenantJob.json errors: $ref: schemas/errors.json + parameters: + $ref: schemas/parameters.json diff --git a/core/src/test/java/org/folio/tlib/OpenApiRefTest.java b/core/src/test/java/org/folio/tlib/OpenApiRefTest.java index 886c773..63a1ff7 100644 --- a/core/src/test/java/org/folio/tlib/OpenApiRefTest.java +++ b/core/src/test/java/org/folio/tlib/OpenApiRefTest.java @@ -10,27 +10,17 @@ @ExtendWith({VertxExtension.class}) public class OpenApiRefTest { @Test - void test1(Vertx vertx, VertxTestContext context) throws Exception { + void testJsonOutput(Vertx vertx, VertxTestContext context) throws Exception { String input = "openapi/reftest.yaml"; - String output = "target/reftest-resolved.yaml"; + String output = "target/reftest-resolved.json"; OpenApiRef.fix(input, output); - OpenAPIContract.from(vertx, "target/reftest-resolved.yaml") + OpenAPIContract.from(vertx, output) .onComplete(context.succeedingThenComplete()); } @Test - void test2(Vertx vertx, VertxTestContext context) throws Exception { + void testYamlOutput(Vertx vertx, VertxTestContext context) throws Exception { OpenAPIContract.from(vertx, OpenApiRef.fix("openapi/reftest.yaml")) .onComplete(context.succeedingThenComplete()); } - - @Test - void writeResolvedOpenApi() throws Exception { - // Reads the test YAML, resolves refs, writes to output file (YAML) - String input = "openapi/reftest.yaml"; - String output = "target/reftest-resolved.yaml"; - OpenApiRef.fix(input, output); - // Optionally, assert file exists or print path - System.out.println("Resolved OpenAPI written to: " + output); - } } diff --git a/core/src/test/java/org/folio/tlib/api/Tenant2ApiTest.java b/core/src/test/java/org/folio/tlib/api/Tenant2ApiTest.java index 9211fa2..cfbb571 100644 --- a/core/src/test/java/org/folio/tlib/api/Tenant2ApiTest.java +++ b/core/src/test/java/org/folio/tlib/api/Tenant2ApiTest.java @@ -134,7 +134,6 @@ void testPostTenantBadTenant1() { @Test void testPostTenantBadTenant2() { - String tenant = "test\"lib"; RestAssured.given() .contentType(ContentType.JSON) .body("{\"module_to\" : \"mod-eusage-reports-1.0.0\"}") From a39e3e921cb6ad0493f6638b19269ea0be2ff6c2 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 15 Aug 2025 15:19:19 +0200 Subject: [PATCH 05/23] Passing tests --- .../java/org/folio/tlib/api/Tenant2Api.java | 37 +++++++++---------- .../test/java/org/folio/tlib/api/EchoApi.java | 10 ++++- .../java/org/folio/tlib/api/EchoApiTest.java | 15 +++++++- .../org/folio/tlib/api/Tenant2ApiTest.java | 8 ++-- mod-example/pom.xml | 4 ++ .../tlib/example/service/BookService.java | 37 ++++++++++++------- .../tlib/example/storage/BookStorage.java | 8 +--- .../src/main/resources/openapi/books-1.0.yaml | 16 ++++---- .../main/resources/openapi/schemas/books.json | 20 +++++++++- .../folio/tlib/example/MainVerticleTest.java | 21 +---------- 10 files changed, 102 insertions(+), 74 deletions(-) diff --git a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java index 8e77225..da5f4e9 100644 --- a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java +++ b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java @@ -9,10 +9,8 @@ import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.openapi.router.RouterBuilder; -import io.vertx.ext.web.validation.RequestParameter; -import io.vertx.ext.web.validation.RequestParameters; -import io.vertx.ext.web.validation.ValidationHandler; import io.vertx.openapi.contract.OpenAPIContract; +import io.vertx.openapi.validation.ValidatedRequest; import io.vertx.sqlclient.Tuple; import java.io.IOException; import java.util.HashMap; @@ -22,7 +20,6 @@ import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.folio.okapi.common.XOkapiHeaders; import org.folio.tlib.OpenApiRef; import org.folio.tlib.RouterCreator; import org.folio.tlib.TenantInitHooks; @@ -65,8 +62,12 @@ static void failHandler(RoutingContext ctx, int code, Throwable e) { failHandler(ctx, ctx.statusCode(), HttpResponseStatus.valueOf(ctx.statusCode()).reasonPhrase()); } else { - log.error("{}", e.getMessage()); - failHandler(ctx, code, e.getMessage()); + Throwable t = e.getCause(); + if (t == null) { + t = e; + } + log.error("{}", e.getMessage(), e); + failHandler(ctx, code, t.getMessage()); } } @@ -193,9 +194,10 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { routerBuilder.getRoute("postTenant") .addHandler(ctx -> { - RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY); - log.info("postTenant handler {}", params.toJson().encode()); - JsonObject tenantAttributes = ctx.body().asJsonObject(); + ValidatedRequest validatedRequest = + ctx.get(RouterBuilder.KEY_META_DATA_VALIDATED_REQUEST); + JsonObject tenantAttributes = validatedRequest.getBody().getJsonObject(); + log.info("postTenant handler {}", tenantAttributes.encode()); String tenant = TenantUtil.tenant(ctx); createJob(vertx, tenant, tenantAttributes) @@ -225,13 +227,11 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { routerBuilder.getRoute("getTenantJob") .addHandler(ctx -> { - RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY); - String id = params.pathParameter("id").getString(); - String tenant = params.headerParameter(XOkapiHeaders.TENANT).getString(); - RequestParameter waitParameter = params.queryParameter("wait"); - int wait = waitParameter != null ? waitParameter.getInteger() : 0; - log.info("getTenantJob handler id={} wait={}", id, - waitParameter != null ? waitParameter.getInteger() : "null"); + String id = ctx.pathParam("id"); + String tenant = TenantUtil.tenant(ctx); + List waitParameter = ctx.queryParam("wait"); + int wait = waitParameter.isEmpty() ? 0 : Integer.parseInt(waitParameter.get(0)); + log.info("getTenantJob handler id={} wait={}", id, wait); getJob(vertx, tenant, UUID.fromString(id), wait) .onSuccess(res -> { if (res == null) { @@ -248,9 +248,8 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { routerBuilder.getRoute("deleteTenantJob") .addHandler(ctx -> { - RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY); - String id = params.pathParameter("id").getString(); - String tenant = params.headerParameter(XOkapiHeaders.TENANT).getString(); + String id = ctx.pathParam("id"); + String tenant = TenantUtil.tenant(ctx); log.info("deleteTenantJob handler id={}", id); deleteJob(vertx, tenant, UUID.fromString(id)) .onSuccess(res -> { diff --git a/core/src/test/java/org/folio/tlib/api/EchoApi.java b/core/src/test/java/org/folio/tlib/api/EchoApi.java index 82615e1..d8b27b5 100644 --- a/core/src/test/java/org/folio/tlib/api/EchoApi.java +++ b/core/src/test/java/org/folio/tlib/api/EchoApi.java @@ -6,9 +6,11 @@ import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.ext.web.openapi.router.OpenAPIRoute; import io.vertx.ext.web.openapi.router.RouterBuilder; import io.vertx.openapi.contract.OpenAPIContract; - +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.tlib.OpenApiRef; import org.folio.tlib.RouterCreator; @@ -16,6 +18,8 @@ public class EchoApi implements RouterCreator { static final int BODY_LIMIT = 65536; // 64 kb as an example of reasonable limit for Json content + private static final Logger log = LogManager.getLogger(EchoApi.class); + static void handleError(RoutingContext ctx, int status, Throwable t) { if (t == null) { handleError(ctx, ctx.statusCode(), "echo service status " + ctx.statusCode()); @@ -44,7 +48,9 @@ public Future createRouter(Vertx vertx) { //routerBuilder.map(routerBuilder -> { // https://vertx.io/docs/vertx-web/java/#_limiting_body_size routerBuilder.rootHandler(BodyHandler.create().setBodyLimit(BODY_LIMIT)); - routerBuilder.getRoute("echo") // operationId in spec + OpenAPIRoute openApiRoute = routerBuilder.getRoute("echo"); + openApiRoute.setDoValidation(false); + openApiRoute .addHandler(ctx -> echo(ctx) .onFailure(e -> handleError(ctx, 500, e)) ) diff --git a/core/src/test/java/org/folio/tlib/api/EchoApiTest.java b/core/src/test/java/org/folio/tlib/api/EchoApiTest.java index cd95148..e0fc084 100644 --- a/core/src/test/java/org/folio/tlib/api/EchoApiTest.java +++ b/core/src/test/java/org/folio/tlib/api/EchoApiTest.java @@ -62,7 +62,20 @@ void testHealth() { } @Test - void testEcho200() { + void testEcho200_1() { + String request = "x".repeat(5); + RestAssured.given() + .header("X-Okapi-Tenant", "tenant") + .contentType(ContentType.TEXT) + .body(request) + .post("/echo") + .then().statusCode(200) + .contentType(ContentType.TEXT) + .body(is(request)); + } + + @Test + void testEcho200_2() { String request = "x".repeat(BODY_LIMIT); RestAssured.given() .header("X-Okapi-Tenant", "tenant") diff --git a/core/src/test/java/org/folio/tlib/api/Tenant2ApiTest.java b/core/src/test/java/org/folio/tlib/api/Tenant2ApiTest.java index cfbb571..dfbb9ec 100644 --- a/core/src/test/java/org/folio/tlib/api/Tenant2ApiTest.java +++ b/core/src/test/java/org/folio/tlib/api/Tenant2ApiTest.java @@ -129,7 +129,7 @@ void testPostTenantBadTenant1() { .post("/_/tenant") .then().statusCode(400) .contentType(ContentType.TEXT) - .body(containsString("X-Okapi-Tenant in location HEADER: provided string should respect pattern")); + .body(containsString("The value of header parameter X-Okapi-Tenant is invalid. Reason: String does not match pattern")); } @Test @@ -140,7 +140,7 @@ void testPostTenantBadTenant2() { .post("/_/tenant") .then().statusCode(400) .contentType(ContentType.TEXT) - .body(containsString("Missing parameter X-Okapi-Tenant in HEADER")); + .body(containsString("The related request / response does not contain the required header parameter X-Okapi-Tenant")); } @Test @@ -415,7 +415,7 @@ void testGetBadId(){ .get("/_/tenant/" + id) .then().statusCode(400) .contentType(ContentType.TEXT) - .body(containsString("Validation error for parameter id in location")); + .body(containsString("he value of path parameter id is invalid. Reason: String does not match format \"uuid\"")); } @Test @@ -435,7 +435,7 @@ void testDeleteBadId(){ .delete("/_/tenant/" + id) .then().statusCode(400) .contentType(ContentType.TEXT) - .body(containsString("Validation error for parameter id in location")); + .body(containsString("The value of path parameter id is invalid. Reason: String does not match format \"uuid\"")); } @Test diff --git a/mod-example/pom.xml b/mod-example/pom.xml index 56656c5..d4098ff 100644 --- a/mod-example/pom.xml +++ b/mod-example/pom.xml @@ -82,6 +82,10 @@ netty-tcnative-boringssl-static runtime + + com.ongres.scram + client + junit diff --git a/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java b/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java index 1922ca5..ccb70e9 100644 --- a/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java +++ b/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java @@ -8,15 +8,15 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.openapi.router.RouterBuilder; -import io.vertx.ext.web.validation.RequestParameters; -import io.vertx.ext.web.validation.ValidationHandler; import io.vertx.openapi.contract.OpenAPIContract; +import io.vertx.openapi.validation.ValidatedRequest; +import java.io.IOException; import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.okapi.common.HttpResponse; +import org.folio.tlib.OpenApiRef; import org.folio.tlib.RouterCreator; import org.folio.tlib.TenantInitHooks; import org.folio.tlib.example.data.Book; @@ -28,17 +28,20 @@ */ public class BookService implements RouterCreator, TenantInitHooks { - public static final int BODY_LIMIT = 65536; // 64 kb - private final Logger log = LogManager.getLogger(BookService.class); @Override public Future createRouter(Vertx vertx) { - return OpenAPIContract.from(vertx, "openapi/books-1.0.yaml") + + String spec; + try { + spec = OpenApiRef.fix("openapi/books-1.0.yaml"); + } catch (IOException e) { + return Future.failedFuture(e); + } + return OpenAPIContract.from(vertx, spec) .map(contract -> { RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); - // https://vertx.io/docs/vertx-web/java/#_limiting_body_size - routerBuilder.rootHandler(BodyHandler.create().setBodyLimit(BODY_LIMIT)); handlers(vertx, routerBuilder); return routerBuilder.createRouter(); }) @@ -47,8 +50,14 @@ public Future createRouter(Vertx vertx) { private void handleContextFailure(RoutingContext ctx) { ctx.response().setStatusCode(ctx.statusCode()); - String msg = ctx.failure() != null ? ctx.failure().getMessage() - : HttpResponseStatus.valueOf(ctx.statusCode()).reasonPhrase(); + String msg; + if (ctx.failure() == null) { + msg = HttpResponseStatus.valueOf(ctx.statusCode()).reasonPhrase(); + } else if (ctx.failure().getCause() == null) { + msg = ctx.failure().getMessage(); + } else { + msg = ctx.failure().getCause().getMessage(); + } ctx.response().putHeader(HttpHeaders.CONTENT_TYPE, "text/plain"); ctx.response().end(msg); } @@ -108,9 +117,7 @@ private Future getBooks(Vertx vertx, RoutingContext ctx) { private Future getBook(Vertx vertx, RoutingContext ctx) { String tenant = TenantUtil.tenant(ctx); - - RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY); - UUID id = UUID.fromString(params.pathParameter("id").getString()); + UUID id = UUID.fromString(ctx.pathParam("id")); BookStorage storage = new BookStorage(vertx, tenant); return storage.getBook(id) .map(book -> { @@ -125,8 +132,10 @@ private Future getBook(Vertx vertx, RoutingContext ctx) { private Future postBook(Vertx vertx, RoutingContext ctx) { String tenant = TenantUtil.tenant(ctx); + ValidatedRequest validatedRequest = + ctx.get(RouterBuilder.KEY_META_DATA_VALIDATED_REQUEST); + Book book = JsonObject.mapFrom(validatedRequest.getBody().getJsonObject()).mapTo(Book.class); BookStorage storage = new BookStorage(vertx, tenant); - Book book = ctx.body().asPojo(Book.class); return storage.postBook(book) .map(res -> { ctx.response().setStatusCode(204).end(); diff --git a/mod-example/src/main/java/org/folio/tlib/example/storage/BookStorage.java b/mod-example/src/main/java/org/folio/tlib/example/storage/BookStorage.java index 305b68a..732acb5 100644 --- a/mod-example/src/main/java/org/folio/tlib/example/storage/BookStorage.java +++ b/mod-example/src/main/java/org/folio/tlib/example/storage/BookStorage.java @@ -5,9 +5,6 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.validation.RequestParameter; -import io.vertx.ext.web.validation.RequestParameters; -import io.vertx.ext.web.validation.ValidationHandler; import io.vertx.sqlclient.RowIterator; import io.vertx.sqlclient.templates.SqlTemplate; import java.util.Collections; @@ -115,9 +112,8 @@ private String createQueryMyTable(RoutingContext ctx, TenantPgPool pool) { pgCqlDefinition.addField("id", new PgCqlFieldUuid()); pgCqlDefinition.addField("title", new PgCqlFieldText().withFullText()); - RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY); - RequestParameter query = params.queryParameter("query"); - PgCqlQuery pgCqlQuery = pgCqlDefinition.parse(query == null ? null : query.getString()); + List query = ctx.queryParam("query"); + PgCqlQuery pgCqlQuery = pgCqlDefinition.parse(query.isEmpty() ? null : query.get(0)); String sql = "SELECT * FROM " + getMyTable(pool); String where = pgCqlQuery.getWhereClause(); if (where != null) { diff --git a/mod-example/src/main/resources/openapi/books-1.0.yaml b/mod-example/src/main/resources/openapi/books-1.0.yaml index e26f57a..ec1fd98 100644 --- a/mod-example/src/main/resources/openapi/books-1.0.yaml +++ b/mod-example/src/main/resources/openapi/books-1.0.yaml @@ -5,9 +5,9 @@ info: paths: /books: parameters: - - $ref: headers/okapi-tenant.yaml - - $ref: headers/okapi-token.yaml - - $ref: headers/okapi-url.yaml + - $ref: 'headers/okapi-tenant.yaml' + - $ref: 'headers/okapi-token.yaml' + - $ref: 'headers/okapi-url.yaml' post: description: Create book operationId: postBook @@ -49,9 +49,9 @@ paths: $ref: "#/components/responses/trait_500" /books/{id}: parameters: - - $ref: headers/okapi-tenant.yaml - - $ref: headers/okapi-token.yaml - - $ref: headers/okapi-url.yaml + - $ref: 'headers/okapi-tenant.yaml' + - $ref: 'headers/okapi-token.yaml' + - $ref: 'headers/okapi-url.yaml' - in: path name: id required: true @@ -112,6 +112,6 @@ components: example: Internal server error, contact administrator schemas: books: - $ref: schemas/books.json + $ref: 'schemas/books.json' book: - $ref: schemas/book.json + $ref: 'schemas/book.json' diff --git a/mod-example/src/main/resources/openapi/schemas/books.json b/mod-example/src/main/resources/openapi/schemas/books.json index 84803e1..49a4bfa 100644 --- a/mod-example/src/main/resources/openapi/schemas/books.json +++ b/mod-example/src/main/resources/openapi/schemas/books.json @@ -7,7 +7,25 @@ "type": "array", "items": { "type": "object", - "$ref": "book.json" + "properties": { + "id": { + "type": "string", + "description": "Title identifier", + "format": "uuid" + }, + "title": { + "type": "string", + "description": "Title text" + }, + "indexTitle": { + "type": "string", + "description": "Indexed title" + } + }, + "required": [ + "id", "title" + ], + "additionalProperties": false } } }, diff --git a/mod-example/src/test/java/org/folio/tlib/example/MainVerticleTest.java b/mod-example/src/test/java/org/folio/tlib/example/MainVerticleTest.java index a64a27c..be27e87 100644 --- a/mod-example/src/test/java/org/folio/tlib/example/MainVerticleTest.java +++ b/mod-example/src/test/java/org/folio/tlib/example/MainVerticleTest.java @@ -22,7 +22,6 @@ import org.junit.runner.RunWith; import org.testcontainers.containers.PostgreSQLContainer; -import static org.folio.tlib.example.service.BookService.BODY_LIMIT; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasSize; @@ -209,22 +208,6 @@ public void testGetBooks() { .put("purge", true), null); } - @Test - public void testBodyLimitBooks() { - Book a = new Book(); - a.setTitle("x".repeat(BODY_LIMIT)); - a.setId(UUID.randomUUID()); - - RestAssured.given() - .header(XOkapiHeaders.TENANT, TENANT) - .contentType(ContentType.JSON) - .body(JsonObject.mapFrom(a).encode()) - .post("/books") - .then().statusCode(413) - .contentType(ContentType.TEXT) - .body(is("Request Entity Too Large")); - } - @Test public void testValidationError() { Book book = new Book(); @@ -241,6 +224,6 @@ public void testValidationError() { .post("/books") .then().statusCode(400) .contentType(ContentType.TEXT) - .body(containsString("Provided object contains unexpected additional property: extra")); + .body(containsString(" Reason: Property \"extra\" does not match additional properties schema")); } -} \ No newline at end of file +} From dacb34287d693809a105f66c4da05c4f2ad62875 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 15 Aug 2025 17:15:33 +0200 Subject: [PATCH 06/23] openapi-deref-plugin --- core/pom.xml | 45 ++++++++-- .../java/org/folio/tlib/api/Tenant2Api.java | 10 +-- .../java/org/folio/tlib/OpenApiRefTest.java | 26 ------ .../test/java/org/folio/tlib/api/EchoApi.java | 9 +- mod-example/pom.xml | 30 ++++++- .../tlib/example/service/BookService.java | 11 +-- openapi-deref-plugin/pom.xml | 88 +++++++++++++++++++ .../java/org/folio/tlib/OpenApiDeref.java | 36 ++------ .../java/org/folio/tlib/OpenApiDerefMojo.java | 39 ++++++++ .../java/org/folio/tlib/OpenApiDerefTest.java | 23 +++++ .../openapi/headers/okapi-token.yaml | 6 ++ .../src/test/resources/openapi/reftest.yaml | 42 +++++++++ pom.xml | 1 + 13 files changed, 275 insertions(+), 91 deletions(-) delete mode 100644 core/src/test/java/org/folio/tlib/OpenApiRefTest.java create mode 100644 openapi-deref-plugin/pom.xml rename core/src/main/java/org/folio/tlib/OpenApiRef.java => openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java (64%) create mode 100644 openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java create mode 100644 openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java create mode 100644 openapi-deref-plugin/src/test/resources/openapi/headers/okapi-token.yaml create mode 100644 openapi-deref-plugin/src/test/resources/openapi/reftest.yaml diff --git a/core/pom.xml b/core/pom.xml index 0330621..37fda7f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -84,12 +84,6 @@ com.ongres.scram client - - - io.swagger.parser.v3 - swagger-parser - 2.1.31 - org.junit.jupiter @@ -167,8 +161,45 @@ + + + ${project.build.directory}/generated-resources + + **/* + + + + - + + org.folio + openapi-deref-plugin + 3.5.0-SNAPSHOT + + + dereference-tenant + + dereference + + generate-resources + + ${project.basedir}/src/main/resources/openapi/tenant-2.0.yaml + ${project.build.directory}/generated-resources/openapi/tenant-2.0.deref.yaml + + + + dereference-echo + + dereference + + generate-resources + + ${project.basedir}/src/test/resources/openapi/echo.yaml + ${project.build.directory}/echo.deref.yaml + + + + org.apache.maven.plugins maven-jar-plugin diff --git a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java index da5f4e9..cdab9e8 100644 --- a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java +++ b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java @@ -12,7 +12,6 @@ import io.vertx.openapi.contract.OpenAPIContract; import io.vertx.openapi.validation.ValidatedRequest; import io.vertx.sqlclient.Tuple; -import java.io.IOException; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -20,7 +19,6 @@ import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.folio.tlib.OpenApiRef; import org.folio.tlib.RouterCreator; import org.folio.tlib.TenantInitHooks; import org.folio.tlib.postgres.impl.TenantPgPoolImpl; @@ -275,13 +273,7 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { */ @Override public Future createRouter(Vertx vertx) { - String spec; - try { - spec = OpenApiRef.fix("openapi/tenant-2.0.yaml"); - } catch (IOException e) { - return Future.failedFuture(e); - } - return OpenAPIContract.from(vertx, spec) + return OpenAPIContract.from(vertx, "openapi/tenant-2.0.deref.yaml") .map(contract -> { RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); handlers(vertx, routerBuilder); diff --git a/core/src/test/java/org/folio/tlib/OpenApiRefTest.java b/core/src/test/java/org/folio/tlib/OpenApiRefTest.java deleted file mode 100644 index 63a1ff7..0000000 --- a/core/src/test/java/org/folio/tlib/OpenApiRefTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.folio.tlib; - -import io.vertx.core.Vertx; -import io.vertx.junit5.VertxExtension; -import io.vertx.junit5.VertxTestContext; -import io.vertx.openapi.contract.OpenAPIContract; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -@ExtendWith({VertxExtension.class}) -public class OpenApiRefTest { - @Test - void testJsonOutput(Vertx vertx, VertxTestContext context) throws Exception { - String input = "openapi/reftest.yaml"; - String output = "target/reftest-resolved.json"; - OpenApiRef.fix(input, output); - OpenAPIContract.from(vertx, output) - .onComplete(context.succeedingThenComplete()); - } - - @Test - void testYamlOutput(Vertx vertx, VertxTestContext context) throws Exception { - OpenAPIContract.from(vertx, OpenApiRef.fix("openapi/reftest.yaml")) - .onComplete(context.succeedingThenComplete()); - } -} diff --git a/core/src/test/java/org/folio/tlib/api/EchoApi.java b/core/src/test/java/org/folio/tlib/api/EchoApi.java index d8b27b5..110f72f 100644 --- a/core/src/test/java/org/folio/tlib/api/EchoApi.java +++ b/core/src/test/java/org/folio/tlib/api/EchoApi.java @@ -11,7 +11,6 @@ import io.vertx.openapi.contract.OpenAPIContract; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.folio.tlib.OpenApiRef; import org.folio.tlib.RouterCreator; public class EchoApi implements RouterCreator { @@ -36,13 +35,7 @@ static void handleError(RoutingContext ctx, int status, String msg) { @Override public Future createRouter(Vertx vertx) { - String spec; - try { - spec = OpenApiRef.fix("openapi/echo.yaml"); - } catch (Exception e) { - return Future.failedFuture(e); - } - return OpenAPIContract.from(vertx, spec) + return OpenAPIContract.from(vertx, "target/echo.deref.yaml") .map(contract -> { RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); //routerBuilder.map(routerBuilder -> { diff --git a/mod-example/pom.xml b/mod-example/pom.xml index d4098ff..05f6033 100644 --- a/mod-example/pom.xml +++ b/mod-example/pom.xml @@ -125,11 +125,39 @@ + + + ${project.build.directory}/generated-resources + + **/* + + + + + + org.folio + openapi-deref-plugin + 3.5.0-SNAPSHOT + + + dereference-books + + dereference + + generate-resources + + ${project.basedir}/src/main/resources/openapi/books-1.0.yaml + ${project.build.directory}/generated-resources/openapi/books-1.0.deref.yaml + + + + + org.apache.maven.plugins maven-shade-plugin - 3.5.1 + 3.6.0 package diff --git a/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java b/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java index ccb70e9..c4c0a4d 100644 --- a/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java +++ b/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java @@ -11,12 +11,10 @@ import io.vertx.ext.web.openapi.router.RouterBuilder; import io.vertx.openapi.contract.OpenAPIContract; import io.vertx.openapi.validation.ValidatedRequest; -import java.io.IOException; import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.okapi.common.HttpResponse; -import org.folio.tlib.OpenApiRef; import org.folio.tlib.RouterCreator; import org.folio.tlib.TenantInitHooks; import org.folio.tlib.example.data.Book; @@ -32,14 +30,7 @@ public class BookService implements RouterCreator, TenantInitHooks { @Override public Future createRouter(Vertx vertx) { - - String spec; - try { - spec = OpenApiRef.fix("openapi/books-1.0.yaml"); - } catch (IOException e) { - return Future.failedFuture(e); - } - return OpenAPIContract.from(vertx, spec) + return OpenAPIContract.from(vertx, "openapi/books-1.0.deref.yaml") .map(contract -> { RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); handlers(vertx, routerBuilder); diff --git a/openapi-deref-plugin/pom.xml b/openapi-deref-plugin/pom.xml new file mode 100644 index 0000000..cac0e8e --- /dev/null +++ b/openapi-deref-plugin/pom.xml @@ -0,0 +1,88 @@ + + 4.0.0 + + + org.folio + folio-vertx-lib + 3.5.0-SNAPSHOT + + + org.folio + openapi-deref-plugin + 3.5.0-SNAPSHOT + maven-plugin + OpenAPI Dereference Maven Plugin + Maven plugin to dereference OpenAPI $ref and produce a single file + https://github.com/folio-org/folio-vertx-lib + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.1 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.17.1 + + + io.swagger.parser.v3 + swagger-parser + 2.1.22 + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.12.0 + provided + + + org.junit.jupiter + junit-jupiter + test + + + org.apache.maven + maven-plugin-api + 3.9.6 + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + 21 + UTF-8 + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.12.0 + + openapi-deref + + + + default-descriptor + + descriptor + + + + + + + + diff --git a/core/src/main/java/org/folio/tlib/OpenApiRef.java b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java similarity index 64% rename from core/src/main/java/org/folio/tlib/OpenApiRef.java rename to openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java index c82ab01..6022f60 100644 --- a/core/src/main/java/org/folio/tlib/OpenApiRef.java +++ b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java @@ -15,22 +15,7 @@ /** * Class for resolving OpenAPI $ref references. */ -public class OpenApiRef { - /** - * Resolves $ref in OpenAPI, suitable for Vert.x OpenAPI parser. - * - * @param path local-URI to OpenAPI YAML file - * @return resolved file in target - * @throws IOException if file could be found or similar - */ - public static String fix(String path) throws IOException { - // get filename portion of path - String refFilename = new File(path).getName(); - refFilename = "target/" + refFilename.replace(".yaml", "_ref.yaml"); - fix(path, refFilename); - return refFilename; - } - +public class OpenApiDeref { static void fix(String inputPath, String outputPath) throws IOException { ParseOptions parseOptions = new ParseOptions(); parseOptions.setResolve(true); @@ -53,26 +38,17 @@ static void fix(String inputPath, String outputPath) throws IOException { removeKeysRecursive(tree, "exampleSetFlag", "extensions", "jsonSchema", "servers", "style", "types", "valueSetFlag"); + new File(outputPath).getParentFile().mkdirs(); mapper.writerWithDefaultPrettyPrinter().writeValue(new File(outputPath), tree); } private static void removeKeysRecursive(JsonNode node, String... keys) { if (node.isObject()) { - java.util.Iterator fieldNames = node.fieldNames(); - java.util.List toRemove = new java.util.ArrayList<>(); - while (fieldNames.hasNext()) { - String field = fieldNames.next(); - for (String k : keys) { - if (field.equals(k)) { - toRemove.add(field); - } - } - } - for (String field : toRemove) { - ((ObjectNode) node).remove(field); + ObjectNode obj = (ObjectNode) node; + for (String key : keys) { + obj.remove(key); } - // Recurse into children - node.fields().forEachRemaining(e -> removeKeysRecursive(e.getValue(), keys)); + obj.fields().forEachRemaining(entry -> removeKeysRecursive(entry.getValue(), keys)); } else if (node.isArray()) { for (JsonNode item : node) { removeKeysRecursive(item, keys); diff --git a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java new file mode 100644 index 0000000..f17651d --- /dev/null +++ b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java @@ -0,0 +1,39 @@ +package org.folio.tlib; + +import java.io.IOException; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +/** + * Mojo to dereference OpenAPI $ref and produce a single file. + */ +@Mojo(name = "dereference", defaultPhase = LifecyclePhase.GENERATE_RESOURCES) +public class OpenApiDerefMojo extends AbstractMojo { + + /** + * Path to the input OpenAPI YAML file. + */ + @Parameter(property = "input.yaml", required = true) + private String input; + + /** + * Path to the output file (YAML or JSON). + */ + @Parameter(property = "output.yaml", required = true) + private String output; + + @Override + public void execute() throws MojoExecutionException { + getLog().info("Dereferencing OpenAPI: " + input + " -> " + output); + try { + // I want to generate all leading directories in output path + OpenApiDeref.fix(input, output); + getLog().info("Dereferenced OpenAPI written to: " + output); + } catch (IOException e) { + throw new MojoExecutionException("Failed to dereference OpenAPI spec", e); + } + } +} diff --git a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java new file mode 100644 index 0000000..bb3c551 --- /dev/null +++ b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java @@ -0,0 +1,23 @@ +package org.folio.tlib; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; + +public class OpenApiDerefTest { + + @Test + void testDeref() throws Exception { + String input = "src/test/resources/openapi/reftest.yaml"; + String output = "target/generated-resources/openapi/reftest.deref.yaml"; + OpenApiDeref.fix(input, output); + // read output file into memory + String outputContent = new String(Files.readAllBytes(Paths.get(output)), StandardCharsets.UTF_8); + assertNotNull(outputContent); + // check that $ref is not found anywhere + assertFalse(outputContent.contains("$ref")); + } +} diff --git a/openapi-deref-plugin/src/test/resources/openapi/headers/okapi-token.yaml b/openapi-deref-plugin/src/test/resources/openapi/headers/okapi-token.yaml new file mode 100644 index 0000000..6de6cb2 --- /dev/null +++ b/openapi-deref-plugin/src/test/resources/openapi/headers/okapi-token.yaml @@ -0,0 +1,6 @@ +in: header +name: X-Okapi-Token +description: Okapi Token +required: false +schema: + type: string diff --git a/openapi-deref-plugin/src/test/resources/openapi/reftest.yaml b/openapi-deref-plugin/src/test/resources/openapi/reftest.yaml new file mode 100644 index 0000000..4d5b73d --- /dev/null +++ b/openapi-deref-plugin/src/test/resources/openapi/reftest.yaml @@ -0,0 +1,42 @@ +openapi: 3.0.0 +info: + title: ref test + version: v1 +paths: + /echo: + parameters: + - $ref: "#/components/parameters/okapi_tenant" + - $ref: headers/okapi-token.yaml + get: + description: echo request + operationId: refTestEcho + responses: + "200": + description: echo ok + content: + text/plain: + schema: + type: string + "400": + $ref: "#/components/responses/trait_400" +components: + responses: + trait_400: + description: Bad request + content: + text/plain: + schema: + type: string + example: Invalid JSON in request + application/json: + schema: + type: object + example: {"error":"Invalid JSON in request"} + parameters: + okapi_tenant: + name: X-Okapi-Tenant + in: header + required: true + schema: + type: string + pattern: '^[_a-z][_a-z0-9]*$' diff --git a/pom.xml b/pom.xml index e1ee8dd..781e1e6 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ 5.0.2 + openapi-deref-plugin core pg-testing mod-example From 8739f5aad6dc46f0dc471fe826fe0f04854e32be Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 15 Aug 2025 17:22:39 +0200 Subject: [PATCH 07/23] SQ --- .../java/org/folio/tlib/postgres/impl/TenantPgPoolImpl.java | 5 ----- core/src/test/java/org/folio/tlib/api/EchoApi.java | 5 ----- openapi-deref-plugin/pom.xml | 2 -- .../src/test/java/org/folio/tlib/OpenApiDerefTest.java | 2 +- 4 files changed, 1 insertion(+), 13 deletions(-) diff --git a/core/src/main/java/org/folio/tlib/postgres/impl/TenantPgPoolImpl.java b/core/src/main/java/org/folio/tlib/postgres/impl/TenantPgPoolImpl.java index 5e7ec58..579c112 100644 --- a/core/src/main/java/org/folio/tlib/postgres/impl/TenantPgPoolImpl.java +++ b/core/src/main/java/org/folio/tlib/postgres/impl/TenantPgPoolImpl.java @@ -1,14 +1,10 @@ package org.folio.tlib.postgres.impl; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; -import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; import io.vertx.core.net.ClientSSLOptions; -import io.vertx.core.net.OpenSSLEngineOptions; import io.vertx.core.net.PemTrustOptions; import io.vertx.pgclient.PgBuilder; import io.vertx.pgclient.PgConnectOptions; @@ -27,7 +23,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.tlib.postgres.TenantPgPool; diff --git a/core/src/test/java/org/folio/tlib/api/EchoApi.java b/core/src/test/java/org/folio/tlib/api/EchoApi.java index 110f72f..002205f 100644 --- a/core/src/test/java/org/folio/tlib/api/EchoApi.java +++ b/core/src/test/java/org/folio/tlib/api/EchoApi.java @@ -9,16 +9,12 @@ import io.vertx.ext.web.openapi.router.OpenAPIRoute; import io.vertx.ext.web.openapi.router.RouterBuilder; import io.vertx.openapi.contract.OpenAPIContract; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.folio.tlib.RouterCreator; public class EchoApi implements RouterCreator { static final int BODY_LIMIT = 65536; // 64 kb as an example of reasonable limit for Json content - private static final Logger log = LogManager.getLogger(EchoApi.class); - static void handleError(RoutingContext ctx, int status, Throwable t) { if (t == null) { handleError(ctx, ctx.statusCode(), "echo service status " + ctx.statusCode()); @@ -38,7 +34,6 @@ public Future createRouter(Vertx vertx) { return OpenAPIContract.from(vertx, "target/echo.deref.yaml") .map(contract -> { RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); - //routerBuilder.map(routerBuilder -> { // https://vertx.io/docs/vertx-web/java/#_limiting_body_size routerBuilder.rootHandler(BodyHandler.create().setBodyLimit(BODY_LIMIT)); OpenAPIRoute openApiRoute = routerBuilder.getRoute("echo"); diff --git a/openapi-deref-plugin/pom.xml b/openapi-deref-plugin/pom.xml index cac0e8e..d991f18 100644 --- a/openapi-deref-plugin/pom.xml +++ b/openapi-deref-plugin/pom.xml @@ -62,8 +62,6 @@ 21 UTF-8 - diff --git a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java index bb3c551..513e919 100644 --- a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java +++ b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java @@ -7,7 +7,7 @@ import java.nio.file.Paths; import org.junit.jupiter.api.Test; -public class OpenApiDerefTest { +class OpenApiDerefTest { @Test void testDeref() throws Exception { From 252b8c0356f33ec0a4c7c8cb2e9a11b54844ee84 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 15 Aug 2025 17:28:26 +0200 Subject: [PATCH 08/23] More testing of plugin --- .../java/org/folio/tlib/OpenApiDerefTest.java | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java index 513e919..09f8785 100644 --- a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java +++ b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java @@ -10,7 +10,7 @@ class OpenApiDerefTest { @Test - void testDeref() throws Exception { + void testDerefYaml() throws Exception { String input = "src/test/resources/openapi/reftest.yaml"; String output = "target/generated-resources/openapi/reftest.deref.yaml"; OpenApiDeref.fix(input, output); @@ -20,4 +20,35 @@ void testDeref() throws Exception { // check that $ref is not found anywhere assertFalse(outputContent.contains("$ref")); } + + @Test + void testDerefJson() throws Exception { + String input = "src/test/resources/openapi/reftest.yaml"; + String output = "target/generated-resources/openapi/reftest.deref.json"; + OpenApiDeref.fix(input, output); + // read output file into memory + String outputContent = new String(Files.readAllBytes(Paths.get(output)), StandardCharsets.UTF_8); + assertNotNull(outputContent); + // check that $ref is not found anywhere + assertFalse(outputContent.contains("$ref")); + } + + @Test + void testFailToParse() { + String input = "src/test/resources/openapi/headers/okapi-token.yaml"; + String output = "target/generated-resources/openapi/okapi-token.deref.json"; + assertThrows(java.io.IOException.class, () -> { + OpenApiDeref.fix(input, output); + }); + } + + @Test + void testNoFile() { + String input = "src/test/resources/openapi/headers/no-file-en.yaml"; + String output = "target/generated-resources/openapi/no-file.deref.json"; + assertThrows(java.io.IOException.class, () -> { + OpenApiDeref.fix(input, output); + }); + } + } From 30f4d4fda02b3be70291ea9b6343bd38d8b1aa63 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 15 Aug 2025 17:43:35 +0200 Subject: [PATCH 09/23] package private constructor --- .../src/main/java/org/folio/tlib/OpenApiDeref.java | 2 +- .../src/main/java/org/folio/tlib/OpenApiDerefMojo.java | 2 +- .../src/test/java/org/folio/tlib/OpenApiDerefTest.java | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java index 6022f60..f5c44c0 100644 --- a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java +++ b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java @@ -16,7 +16,7 @@ * Class for resolving OpenAPI $ref references. */ public class OpenApiDeref { - static void fix(String inputPath, String outputPath) throws IOException { + OpenApiDeref(String inputPath, String outputPath) throws IOException { ParseOptions parseOptions = new ParseOptions(); parseOptions.setResolve(true); SwaggerParseResult result = new OpenAPIV3Parser().readLocation(inputPath, null, parseOptions); diff --git a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java index f17651d..ccfd106 100644 --- a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java +++ b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java @@ -30,7 +30,7 @@ public void execute() throws MojoExecutionException { getLog().info("Dereferencing OpenAPI: " + input + " -> " + output); try { // I want to generate all leading directories in output path - OpenApiDeref.fix(input, output); + new OpenApiDeref(input, output); getLog().info("Dereferenced OpenAPI written to: " + output); } catch (IOException e) { throw new MojoExecutionException("Failed to dereference OpenAPI spec", e); diff --git a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java index 09f8785..0687e75 100644 --- a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java +++ b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java @@ -13,7 +13,7 @@ class OpenApiDerefTest { void testDerefYaml() throws Exception { String input = "src/test/resources/openapi/reftest.yaml"; String output = "target/generated-resources/openapi/reftest.deref.yaml"; - OpenApiDeref.fix(input, output); + new OpenApiDeref(input, output); // read output file into memory String outputContent = new String(Files.readAllBytes(Paths.get(output)), StandardCharsets.UTF_8); assertNotNull(outputContent); @@ -25,7 +25,7 @@ void testDerefYaml() throws Exception { void testDerefJson() throws Exception { String input = "src/test/resources/openapi/reftest.yaml"; String output = "target/generated-resources/openapi/reftest.deref.json"; - OpenApiDeref.fix(input, output); + new OpenApiDeref(input, output); // read output file into memory String outputContent = new String(Files.readAllBytes(Paths.get(output)), StandardCharsets.UTF_8); assertNotNull(outputContent); @@ -38,7 +38,7 @@ void testFailToParse() { String input = "src/test/resources/openapi/headers/okapi-token.yaml"; String output = "target/generated-resources/openapi/okapi-token.deref.json"; assertThrows(java.io.IOException.class, () -> { - OpenApiDeref.fix(input, output); + new OpenApiDeref(input, output); }); } @@ -47,7 +47,7 @@ void testNoFile() { String input = "src/test/resources/openapi/headers/no-file-en.yaml"; String output = "target/generated-resources/openapi/no-file.deref.json"; assertThrows(java.io.IOException.class, () -> { - OpenApiDeref.fix(input, output); + new OpenApiDeref(input, output); }); } From 109d8fa2bff12878e1f4e26d238f0ea27b3d4f1e Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 15 Aug 2025 17:49:16 +0200 Subject: [PATCH 10/23] Revert "package private constructor" This reverts commit 30f4d4fda02b3be70291ea9b6343bd38d8b1aa63. --- .../src/main/java/org/folio/tlib/OpenApiDeref.java | 2 +- .../src/main/java/org/folio/tlib/OpenApiDerefMojo.java | 2 +- .../src/test/java/org/folio/tlib/OpenApiDerefTest.java | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java index f5c44c0..6022f60 100644 --- a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java +++ b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java @@ -16,7 +16,7 @@ * Class for resolving OpenAPI $ref references. */ public class OpenApiDeref { - OpenApiDeref(String inputPath, String outputPath) throws IOException { + static void fix(String inputPath, String outputPath) throws IOException { ParseOptions parseOptions = new ParseOptions(); parseOptions.setResolve(true); SwaggerParseResult result = new OpenAPIV3Parser().readLocation(inputPath, null, parseOptions); diff --git a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java index ccfd106..f17651d 100644 --- a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java +++ b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java @@ -30,7 +30,7 @@ public void execute() throws MojoExecutionException { getLog().info("Dereferencing OpenAPI: " + input + " -> " + output); try { // I want to generate all leading directories in output path - new OpenApiDeref(input, output); + OpenApiDeref.fix(input, output); getLog().info("Dereferenced OpenAPI written to: " + output); } catch (IOException e) { throw new MojoExecutionException("Failed to dereference OpenAPI spec", e); diff --git a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java index 0687e75..09f8785 100644 --- a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java +++ b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java @@ -13,7 +13,7 @@ class OpenApiDerefTest { void testDerefYaml() throws Exception { String input = "src/test/resources/openapi/reftest.yaml"; String output = "target/generated-resources/openapi/reftest.deref.yaml"; - new OpenApiDeref(input, output); + OpenApiDeref.fix(input, output); // read output file into memory String outputContent = new String(Files.readAllBytes(Paths.get(output)), StandardCharsets.UTF_8); assertNotNull(outputContent); @@ -25,7 +25,7 @@ void testDerefYaml() throws Exception { void testDerefJson() throws Exception { String input = "src/test/resources/openapi/reftest.yaml"; String output = "target/generated-resources/openapi/reftest.deref.json"; - new OpenApiDeref(input, output); + OpenApiDeref.fix(input, output); // read output file into memory String outputContent = new String(Files.readAllBytes(Paths.get(output)), StandardCharsets.UTF_8); assertNotNull(outputContent); @@ -38,7 +38,7 @@ void testFailToParse() { String input = "src/test/resources/openapi/headers/okapi-token.yaml"; String output = "target/generated-resources/openapi/okapi-token.deref.json"; assertThrows(java.io.IOException.class, () -> { - new OpenApiDeref(input, output); + OpenApiDeref.fix(input, output); }); } @@ -47,7 +47,7 @@ void testNoFile() { String input = "src/test/resources/openapi/headers/no-file-en.yaml"; String output = "target/generated-resources/openapi/no-file.deref.json"; assertThrows(java.io.IOException.class, () -> { - new OpenApiDeref(input, output); + OpenApiDeref.fix(input, output); }); } From f7db6525311cbd06dae8ebb17607ab32adec1c1f Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 15 Aug 2025 17:53:05 +0200 Subject: [PATCH 11/23] private constructor --- .../src/main/java/org/folio/tlib/OpenApiDeref.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java index 6022f60..723b319 100644 --- a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java +++ b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java @@ -16,6 +16,10 @@ * Class for resolving OpenAPI $ref references. */ public class OpenApiDeref { + private OpenApiDeref() { + throw new IllegalStateException("OpenApiDeref"); + } + static void fix(String inputPath, String outputPath) throws IOException { ParseOptions parseOptions = new ParseOptions(); parseOptions.setResolve(true); From 3ae1c6df474af76797fffe4867259e70c1941fc7 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 19 Aug 2025 16:51:54 +0200 Subject: [PATCH 12/23] Unused deps --- core/pom.xml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 37fda7f..4404e28 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,16 +12,6 @@ io.vertx vertx-core - - com.fasterxml.jackson.core - jackson-databind - 2.17.1 - - - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - 2.17.1 - io.vertx vertx-web From 6af400dea8c11c272d1f26fd035fe2fc2bd0f50f Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 19 Aug 2025 16:52:00 +0200 Subject: [PATCH 13/23] vertx 5.0.3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 781e1e6..71e58c0 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ UTF-8 6.3.0-SNAPSHOT - 5.0.2 + 5.0.3 openapi-deref-plugin From 791882e1074066ed77ac3c143fe5686ef3857025 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 20 Aug 2025 12:31:17 +0200 Subject: [PATCH 14/23] Update some deps + note on swagger-parser version --- openapi-deref-plugin/pom.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openapi-deref-plugin/pom.xml b/openapi-deref-plugin/pom.xml index d991f18..5bfbf9a 100644 --- a/openapi-deref-plugin/pom.xml +++ b/openapi-deref-plugin/pom.xml @@ -21,22 +21,23 @@ com.fasterxml.jackson.core jackson-databind - 2.17.1 + 2.18.2 com.fasterxml.jackson.dataformat jackson-dataformat-yaml - 2.17.1 + 2.18.2 io.swagger.parser.v3 swagger-parser + 2.1.22 org.apache.maven.plugin-tools maven-plugin-annotations - 3.12.0 + 3.15.1 provided @@ -47,7 +48,7 @@ org.apache.maven maven-plugin-api - 3.9.6 + 3.9.11 provided From f790b929ac7512062fb2e7d09028d31d8bd4e123 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 20 Aug 2025 12:53:04 +0200 Subject: [PATCH 15/23] Upgrade to swagger-parser 2.1.32 --- openapi-deref-plugin/pom.xml | 3 +-- .../src/test/java/org/folio/tlib/OpenApiDerefTest.java | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openapi-deref-plugin/pom.xml b/openapi-deref-plugin/pom.xml index 5bfbf9a..6ce0486 100644 --- a/openapi-deref-plugin/pom.xml +++ b/openapi-deref-plugin/pom.xml @@ -31,8 +31,7 @@ io.swagger.parser.v3 swagger-parser - - 2.1.22 + 2.1.32 org.apache.maven.plugin-tools diff --git a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java index 09f8785..a119485 100644 --- a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java +++ b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java @@ -18,7 +18,7 @@ void testDerefYaml() throws Exception { String outputContent = new String(Files.readAllBytes(Paths.get(output)), StandardCharsets.UTF_8); assertNotNull(outputContent); // check that $ref is not found anywhere - assertFalse(outputContent.contains("$ref")); + assertFalse(outputContent.contains("$ref: headers")); } @Test @@ -30,7 +30,7 @@ void testDerefJson() throws Exception { String outputContent = new String(Files.readAllBytes(Paths.get(output)), StandardCharsets.UTF_8); assertNotNull(outputContent); // check that $ref is not found anywhere - assertFalse(outputContent.contains("$ref")); + assertFalse(outputContent.contains("$ref: headers")); } @Test From 6644fd8a2b8ff5f8df4c0597fb19796c254cbc58 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 20 Aug 2025 13:29:04 +0200 Subject: [PATCH 16/23] Get JSON refs to work again --- .../main/resources/openapi/schemas/error.json | 27 +++++++++++++++++++ .../resources/openapi/schemas/errors.json | 19 +------------ .../resources/openapi/schemas/parameter.json | 18 +++++++++++++ .../resources/openapi/schemas/parameters.json | 15 +---------- .../main/resources/openapi/tenant-2.0.yaml | 2 +- .../src/main/resources/openapi/books-1.0.yaml | 24 +++++++---------- .../main/resources/openapi/schemas/books.json | 20 +------------- 7 files changed, 59 insertions(+), 66 deletions(-) create mode 100644 core/src/main/resources/openapi/schemas/error.json create mode 100644 core/src/main/resources/openapi/schemas/parameter.json diff --git a/core/src/main/resources/openapi/schemas/error.json b/core/src/main/resources/openapi/schemas/error.json new file mode 100644 index 0000000..97a99a9 --- /dev/null +++ b/core/src/main/resources/openapi/schemas/error.json @@ -0,0 +1,27 @@ +{ + "description": "An error", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Error message text" + }, + "type": { + "type": "string", + "description": "Error message type" + }, + "code": { + "type": "string", + "description": "Error message code" + }, + "parameters": { + "type": "object", + "description": "Error message parameters", + "$ref": "parameters.json" + } + }, + "additionalProperties": false, + "required": [ + "message" + ] +} diff --git a/core/src/main/resources/openapi/schemas/errors.json b/core/src/main/resources/openapi/schemas/errors.json index 43e7dd9..b358d4f 100644 --- a/core/src/main/resources/openapi/schemas/errors.json +++ b/core/src/main/resources/openapi/schemas/errors.json @@ -7,24 +7,7 @@ "type": "array", "items": { "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Error message text" - }, - "type": { - "type": "string", - "description": "Error message type" - }, - "code": { - "type": "string", - "description": "Error message code" - } - }, - "additionalProperties": false, - "required": [ - "message" - ] + "$ref": "error.json" } }, "total_records": { diff --git a/core/src/main/resources/openapi/schemas/parameter.json b/core/src/main/resources/openapi/schemas/parameter.json new file mode 100644 index 0000000..a1a8f55 --- /dev/null +++ b/core/src/main/resources/openapi/schemas/parameter.json @@ -0,0 +1,18 @@ +{ + "description": "List of key/value parameters of an error", + "type": "object", + "properties": { + "key": { + "description": "The key for this parameter", + "type": "string" + }, + "value": { + "description": "The value of this parameter", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "key" + ] +} diff --git a/core/src/main/resources/openapi/schemas/parameters.json b/core/src/main/resources/openapi/schemas/parameters.json index 5a7f993..8188d05 100644 --- a/core/src/main/resources/openapi/schemas/parameters.json +++ b/core/src/main/resources/openapi/schemas/parameters.json @@ -3,20 +3,7 @@ "type": "array", "items": { "type": "object", - "properties": { - "key": { - "description": "The key for this parameter", - "type": "string" - }, - "value": { - "description": "The value of this parameter", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "key" - ] + "$ref": "parameter.json" }, "additionalProperties": false } diff --git a/core/src/main/resources/openapi/tenant-2.0.yaml b/core/src/main/resources/openapi/tenant-2.0.yaml index 3c9d2c3..c295818 100644 --- a/core/src/main/resources/openapi/tenant-2.0.yaml +++ b/core/src/main/resources/openapi/tenant-2.0.yaml @@ -94,7 +94,7 @@ components: content: application/json: schema: - $ref: "#/components/schemas/errors" + $ref: schemas/errors.json examples: response: value: examples/errors.sample diff --git a/mod-example/src/main/resources/openapi/books-1.0.yaml b/mod-example/src/main/resources/openapi/books-1.0.yaml index ec1fd98..297815f 100644 --- a/mod-example/src/main/resources/openapi/books-1.0.yaml +++ b/mod-example/src/main/resources/openapi/books-1.0.yaml @@ -5,9 +5,9 @@ info: paths: /books: parameters: - - $ref: 'headers/okapi-tenant.yaml' - - $ref: 'headers/okapi-token.yaml' - - $ref: 'headers/okapi-url.yaml' + - $ref: headers/okapi-tenant.yaml + - $ref: headers/okapi-token.yaml + - $ref: headers/okapi-url.yaml post: description: Create book operationId: postBook @@ -15,7 +15,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/book" + $ref: schemas/book.json required: true responses: "204": @@ -42,16 +42,16 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/books" + $ref: schemas/books.json "400": $ref: "#/components/responses/trait_400" "500": $ref: "#/components/responses/trait_500" /books/{id}: parameters: - - $ref: 'headers/okapi-tenant.yaml' - - $ref: 'headers/okapi-token.yaml' - - $ref: 'headers/okapi-url.yaml' + - $ref: headers/okapi-tenant.yaml + - $ref: headers/okapi-token.yaml + - $ref: headers/okapi-url.yaml - in: path name: id required: true @@ -68,7 +68,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/book" + $ref: schemas/book.json "400": $ref: "#/components/responses/trait_400" "404": @@ -110,8 +110,4 @@ components: schema: type: string example: Internal server error, contact administrator - schemas: - books: - $ref: 'schemas/books.json' - book: - $ref: 'schemas/book.json' + diff --git a/mod-example/src/main/resources/openapi/schemas/books.json b/mod-example/src/main/resources/openapi/schemas/books.json index 49a4bfa..84803e1 100644 --- a/mod-example/src/main/resources/openapi/schemas/books.json +++ b/mod-example/src/main/resources/openapi/schemas/books.json @@ -7,25 +7,7 @@ "type": "array", "items": { "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Title identifier", - "format": "uuid" - }, - "title": { - "type": "string", - "description": "Title text" - }, - "indexTitle": { - "type": "string", - "description": "Indexed title" - } - }, - "required": [ - "id", "title" - ], - "additionalProperties": false + "$ref": "book.json" } } }, From d89fa07d87c57603bdbc52011ce7c7731490d58a Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 20 Aug 2025 16:15:06 +0200 Subject: [PATCH 17/23] Plugin: input with glob patterns; default values --- core/pom.xml | 17 ++-------- .../java/org/folio/tlib/api/Tenant2Api.java | 2 +- .../test/java/org/folio/tlib/api/EchoApi.java | 2 +- mod-example/pom.xml | 13 -------- .../tlib/example/service/BookService.java | 2 +- .../java/org/folio/tlib/OpenApiDeref.java | 29 +++++++++++++++++ .../java/org/folio/tlib/OpenApiDerefMojo.java | 26 ++++++++++++---- .../java/org/folio/tlib/OpenApiDerefTest.java | 31 +++++++++++++++++++ 8 files changed, 85 insertions(+), 37 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 4404e28..a628f50 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -151,15 +151,6 @@ - - - ${project.build.directory}/generated-resources - - **/* - - - - org.folio @@ -172,10 +163,6 @@ dereference generate-resources - - ${project.basedir}/src/main/resources/openapi/tenant-2.0.yaml - ${project.build.directory}/generated-resources/openapi/tenant-2.0.deref.yaml - dereference-echo @@ -184,8 +171,8 @@ generate-resources - ${project.basedir}/src/test/resources/openapi/echo.yaml - ${project.build.directory}/echo.deref.yaml + ${project.basedir}/src/test/resources/openapi/*.yaml + ${project.build.directory}/test-classes/openapi diff --git a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java index cdab9e8..188876a 100644 --- a/core/src/main/java/org/folio/tlib/api/Tenant2Api.java +++ b/core/src/main/java/org/folio/tlib/api/Tenant2Api.java @@ -273,7 +273,7 @@ private void handlers(Vertx vertx, RouterBuilder routerBuilder) { */ @Override public Future createRouter(Vertx vertx) { - return OpenAPIContract.from(vertx, "openapi/tenant-2.0.deref.yaml") + return OpenAPIContract.from(vertx, "openapi/tenant-2.0.yaml") .map(contract -> { RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); handlers(vertx, routerBuilder); diff --git a/core/src/test/java/org/folio/tlib/api/EchoApi.java b/core/src/test/java/org/folio/tlib/api/EchoApi.java index 002205f..12f2670 100644 --- a/core/src/test/java/org/folio/tlib/api/EchoApi.java +++ b/core/src/test/java/org/folio/tlib/api/EchoApi.java @@ -31,7 +31,7 @@ static void handleError(RoutingContext ctx, int status, String msg) { @Override public Future createRouter(Vertx vertx) { - return OpenAPIContract.from(vertx, "target/echo.deref.yaml") + return OpenAPIContract.from(vertx, "openapi/echo.yaml") .map(contract -> { RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); // https://vertx.io/docs/vertx-web/java/#_limiting_body_size diff --git a/mod-example/pom.xml b/mod-example/pom.xml index 05f6033..cb7038a 100644 --- a/mod-example/pom.xml +++ b/mod-example/pom.xml @@ -125,15 +125,6 @@ - - - ${project.build.directory}/generated-resources - - **/* - - - - org.folio @@ -146,10 +137,6 @@ dereference generate-resources - - ${project.basedir}/src/main/resources/openapi/books-1.0.yaml - ${project.build.directory}/generated-resources/openapi/books-1.0.deref.yaml - diff --git a/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java b/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java index c4c0a4d..e176c18 100644 --- a/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java +++ b/mod-example/src/main/java/org/folio/tlib/example/service/BookService.java @@ -30,7 +30,7 @@ public class BookService implements RouterCreator, TenantInitHooks { @Override public Future createRouter(Vertx vertx) { - return OpenAPIContract.from(vertx, "openapi/books-1.0.deref.yaml") + return OpenAPIContract.from(vertx, "openapi/books-1.0.yaml") .map(contract -> { RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); handlers(vertx, routerBuilder); diff --git a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java index 723b319..d3db716 100644 --- a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java +++ b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDeref.java @@ -11,6 +11,11 @@ import io.swagger.v3.parser.core.models.SwaggerParseResult; import java.io.File; import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; /** * Class for resolving OpenAPI $ref references. @@ -20,6 +25,30 @@ private OpenApiDeref() { throw new IllegalStateException("OpenApiDeref"); } + static List mapFilesWithPattern(String inputPattern, String outputPath) + throws IOException { + File inputDir = new File(inputPattern).getParentFile(); + if (inputDir == null) { + throw new IOException("no path in " + inputPattern); + } + Path dirPath = inputDir.toPath(); + DirectoryStream.Filter filter = entry -> + java.nio.file.FileSystems.getDefault() + .getPathMatcher("glob:" + new File(inputPattern).getName()) + .matches(entry.getFileName()); + + List files = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(dirPath, filter)) { + for (Path entry : stream) { + String inputFile = entry.toFile().getAbsolutePath(); + files.add(inputFile); + String outputFile = inputFile.replace(inputDir.getAbsolutePath(), outputPath); + files.add(outputFile); + } + } + return files; + } + static void fix(String inputPath, String outputPath) throws IOException { ParseOptions parseOptions = new ParseOptions(); parseOptions.setResolve(true); diff --git a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java index f17651d..9f71457 100644 --- a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java +++ b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java @@ -1,6 +1,9 @@ package org.folio.tlib; +import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; @@ -16,24 +19,35 @@ public class OpenApiDerefMojo extends AbstractMojo { /** * Path to the input OpenAPI YAML file. */ - @Parameter(property = "input.yaml", required = true) + @Parameter(property = "input", required = false, + defaultValue = "${basedir}/src/main/resources/openapi/*.yaml") private String input; /** * Path to the output file (YAML or JSON). */ - @Parameter(property = "output.yaml", required = true) + @Parameter(property = "output", required = false, + defaultValue = "${project.build.directory}/classes/openapi") private String output; @Override public void execute() throws MojoExecutionException { getLog().info("Dereferencing OpenAPI: " + input + " -> " + output); + List files = new ArrayList<>(); try { - // I want to generate all leading directories in output path - OpenApiDeref.fix(input, output); - getLog().info("Dereferenced OpenAPI written to: " + output); + files = OpenApiDeref.mapFilesWithPattern(input, output); } catch (IOException e) { - throw new MojoExecutionException("Failed to dereference OpenAPI spec", e); + throw new MojoExecutionException("Failed to map OpenAPI files", e); + } + for (int i = 0; i < files.size(); i += 2) { + String inputFile = files.get(i); + String outputFile = files.get(i + 1); + try { + OpenApiDeref.fix(inputFile, outputFile); + } catch (IOException e) { + throw new MojoExecutionException("Failed to dereference OpenAPI file " + inputFile, e); + } + getLog().info("Dereferenced OpenAPI written to: " + outputFile); } } } diff --git a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java index a119485..54b11ae 100644 --- a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java +++ b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java @@ -2,13 +2,44 @@ import static org.junit.jupiter.api.Assertions.*; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Test; class OpenApiDerefTest { + @Test + void testMapFilesWithPattern1() throws Exception{ + List files = OpenApiDeref.mapFilesWithPattern("src/test/resources/openapi/*.yaml", "target/generated-resources/openapi"); + assertEquals(2, files.size()); + assertTrue(files.get(0).endsWith("src/test/resources/openapi/reftest.yaml"), "Input file not right: " + files.get(0)); + assertTrue(files.get(1).endsWith("target/generated-resources/openapi/reftest.yaml"), "Output file not right: " + files.get(1)); + } + + @Test + void testMapFilesWithPattern2() throws Exception { + List files = OpenApiDeref.mapFilesWithPattern("src/test/resources/openapi/*.nope", "target/generated-resources/openapi"); + assertEquals(0, files.size()); + } + + @Test + void testMapFilesWithPattern3() throws Exception { + assertThrows(IOException.class, () -> { + OpenApiDeref.mapFilesWithPattern("src1/test1/1.nope", "target/generated-resources/openapi"); + }); + } + + @Test + void testMapFilesWithPattern4() { + assertThrows(IOException.class, () -> { + OpenApiDeref.mapFilesWithPattern("onlyfilename.yaml", "target/generated-resources/openapi"); + }); + } + @Test void testDerefYaml() throws Exception { String input = "src/test/resources/openapi/reftest.yaml"; From 733662d84422ba16fe1b7cb79de692b4bad4c4e7 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 20 Aug 2025 16:32:15 +0200 Subject: [PATCH 18/23] SQ warnings --- .../src/main/java/org/folio/tlib/OpenApiDerefMojo.java | 1 - .../src/test/java/org/folio/tlib/OpenApiDerefTest.java | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java index 9f71457..ae4fce5 100644 --- a/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java +++ b/openapi-deref-plugin/src/main/java/org/folio/tlib/OpenApiDerefMojo.java @@ -1,6 +1,5 @@ package org.folio.tlib; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; diff --git a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java index 54b11ae..33867fa 100644 --- a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java +++ b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java @@ -6,7 +6,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; @@ -27,7 +26,7 @@ void testMapFilesWithPattern2() throws Exception { } @Test - void testMapFilesWithPattern3() throws Exception { + void testMapFilesWithPattern3() { assertThrows(IOException.class, () -> { OpenApiDeref.mapFilesWithPattern("src1/test1/1.nope", "target/generated-resources/openapi"); }); From af46182bf253fa9d44416c68cc0f995c9fba3d71 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 20 Aug 2025 18:46:57 +0200 Subject: [PATCH 19/23] Update API snippets; mention openapi-deref-plugin --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c5ebb40..d6155de 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,16 @@ library, not a framework, with utilities such as: ## Main Verticle -The [Vert.x OpenAPI](https://vertx.io/docs/vertx-web-openapi/java/) unlike +The [Vert.x OpenAPI](https://vertx.io/docs/vertx-openapi/java/) unlike many OpenAPI implementations does not generate any code for you. Everything happens at run-time. Only requests are validated, not responses. +The OpenAPI implementaion of Vert.x 5 does not allow external references - even +if they are local files [ref](https://vertx.io/docs/vertx-openapi/java/#_openapicontract). +If the OpenAPI spec in use has local file references the YAML must be preprocessed with the +openapi-deref-plugin. See the [openapi-deref-plugin](#plugin-openapi-deref-plugin) section +for details about handling external references in OpenAPI specifications. + Place your OpenAPI specification and auxiliary files somewhere in `resources`, such as `resources/openapi`. @@ -89,24 +95,25 @@ For an OpenAPI based implementation it could look as follows: public MyApi implements RouterCreator, TenantInitHooks { @Override public Future createRouter(Vertx vertx) { - return RouterBuilder.create(vertx, "openapi/myapi-1.0.yaml") - .map(routerBuilder -> { - handlers(vertx, routerBuilder); - return routerBuilder.createRouter(); - }); + return OpenAPIContract.from(vertx, "openapi/myapi-1.0.yaml") + .map(contract -> { + RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); + handlers(vertx, routerBuilder); + return routerBuilder.createRouter(); + }); } private void handlers(Vertx vertx, RouterBuilder routerBuilder) { routerBuilder - .operation("postTitles") // operationId in spec - .handler(ctx -> { + .getRoute("postTitles") // operationId in spec + .addHandler(ctx -> { // doesn't do anything at the moment! ctx.response().setStatusCode(204); ctx.response().end(); }); routerBuilder - .operation("getTitles") - .handler(ctx -> getTitles(vertx, ctx) + .getRoute("getTitles") + .addHandler(ctx -> getTitles(vertx, ctx) .onFailure(cause -> { ctx.response().setStatusCode(500); ctx.response().end(cause.getMessage()); @@ -128,6 +135,54 @@ The Tenant2Api implementation deals with purge (removes schema with cascade). Your implementation should only consider upgrade/downgrade. On purge, `preInit` is called, but `postInit` is not. +## Plugin openapi-deref-plugin + +The purpose of the openapi-deref-plugin is to de-reference `$ref` references in the OpenAPI +specification. The result is one YAML file with all resources embedded. If there are +only references to components inside the OpenAPI YAML file from the beginning, it is not +necessary to use this plugin. + +If the OpenAPI specification is located in `resources/openapi` (recommened), then +the minimal way to use the plugin is to use: + +``` + + org.folio + openapi-deref-plugin + 4.0.0 + + + dereference-books + + dereference + + generate-resources + + + +``` + +The configuration has the following properties: + + * `input` : glob-path for input files to search. Default value is `${basedir}/src/main/resources/openapi/*.yaml` + * `output` : output directory. Default value is `${project.build.directory}/classes/openapi`. + +As an example if there are OpenAPI specs in test resources, the `extensions` list could be extended with: + +``` + + dereference-echo + + dereference + + generate-resources + + ${project.basedir}/src/test/resources/openapi/*.yaml + ${project.build.directory}/test-classes/openapi + + +``` + ## PostgreSQL The PostgreSQL support is minimal. There's just enough to perform tenant From 05b111c5677fa1f78bca4e050bac4267e05ac0ca Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 20 Aug 2025 18:48:46 +0200 Subject: [PATCH 20/23] Next version is 4.0.0-SNAPSHOT (major) --- core/pom.xml | 4 ++-- mod-example/pom.xml | 8 ++++---- openapi-deref-plugin/pom.xml | 4 ++-- pg-testing/pom.xml | 4 ++-- pom.xml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index a628f50..f8acad2 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -3,7 +3,7 @@ org.folio folio-vertx-lib - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT vertx-lib @@ -155,7 +155,7 @@ org.folio openapi-deref-plugin - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT dereference-tenant diff --git a/mod-example/pom.xml b/mod-example/pom.xml index cb7038a..682e9ed 100644 --- a/mod-example/pom.xml +++ b/mod-example/pom.xml @@ -4,19 +4,19 @@ org.folio folio-vertx-lib - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT mod-example org.folio vertx-lib - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT org.folio vertx-lib-pg-testing - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT test @@ -129,7 +129,7 @@ org.folio openapi-deref-plugin - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT dereference-books diff --git a/openapi-deref-plugin/pom.xml b/openapi-deref-plugin/pom.xml index 6ce0486..74907ea 100644 --- a/openapi-deref-plugin/pom.xml +++ b/openapi-deref-plugin/pom.xml @@ -6,12 +6,12 @@ org.folio folio-vertx-lib - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT org.folio openapi-deref-plugin - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT maven-plugin OpenAPI Dereference Maven Plugin Maven plugin to dereference OpenAPI $ref and produce a single file diff --git a/pg-testing/pom.xml b/pg-testing/pom.xml index 6fc9eac..7767d10 100644 --- a/pg-testing/pom.xml +++ b/pg-testing/pom.xml @@ -3,7 +3,7 @@ org.folio folio-vertx-lib - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT vertx-lib-pg-testing @@ -11,7 +11,7 @@ org.folio vertx-lib - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT org.testcontainers diff --git a/pom.xml b/pom.xml index 71e58c0..fb48244 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.folio folio-vertx-lib - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT pom FOLIO Vert.x library From d2b00d9aefd36f49d527577fdacc6678d0c28398 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Thu, 21 Aug 2025 09:13:34 +0200 Subject: [PATCH 21/23] Change test spec, so that no $ref occur in output --- .../test/java/org/folio/tlib/OpenApiDerefTest.java | 8 ++------ .../src/test/resources/openapi/reftest.yaml | 14 -------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java index 33867fa..7e947c5 100644 --- a/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java +++ b/openapi-deref-plugin/src/test/java/org/folio/tlib/OpenApiDerefTest.java @@ -44,11 +44,9 @@ void testDerefYaml() throws Exception { String input = "src/test/resources/openapi/reftest.yaml"; String output = "target/generated-resources/openapi/reftest.deref.yaml"; OpenApiDeref.fix(input, output); - // read output file into memory String outputContent = new String(Files.readAllBytes(Paths.get(output)), StandardCharsets.UTF_8); assertNotNull(outputContent); - // check that $ref is not found anywhere - assertFalse(outputContent.contains("$ref: headers")); + assertFalse(outputContent.contains("$ref")); } @Test @@ -56,11 +54,9 @@ void testDerefJson() throws Exception { String input = "src/test/resources/openapi/reftest.yaml"; String output = "target/generated-resources/openapi/reftest.deref.json"; OpenApiDeref.fix(input, output); - // read output file into memory String outputContent = new String(Files.readAllBytes(Paths.get(output)), StandardCharsets.UTF_8); assertNotNull(outputContent); - // check that $ref is not found anywhere - assertFalse(outputContent.contains("$ref: headers")); + assertFalse(outputContent.contains("$ref")); } @Test diff --git a/openapi-deref-plugin/src/test/resources/openapi/reftest.yaml b/openapi-deref-plugin/src/test/resources/openapi/reftest.yaml index 4d5b73d..b622ca9 100644 --- a/openapi-deref-plugin/src/test/resources/openapi/reftest.yaml +++ b/openapi-deref-plugin/src/test/resources/openapi/reftest.yaml @@ -17,21 +17,7 @@ paths: text/plain: schema: type: string - "400": - $ref: "#/components/responses/trait_400" components: - responses: - trait_400: - description: Bad request - content: - text/plain: - schema: - type: string - example: Invalid JSON in request - application/json: - schema: - type: object - example: {"error":"Invalid JSON in request"} parameters: okapi_tenant: name: X-Okapi-Tenant From 89a830022a86d7e0c88173ea99f5cde6e200bc11 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Thu, 21 Aug 2025 09:43:04 +0200 Subject: [PATCH 22/23] Fix launch of mod-example --- mod-example/pom.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mod-example/pom.xml b/mod-example/pom.xml index 682e9ed..8080175 100644 --- a/mod-example/pom.xml +++ b/mod-example/pom.xml @@ -23,6 +23,10 @@ io.vertx vertx-core + + io.vertx + vertx-launcher-application + io.vertx vertx-web @@ -155,7 +159,7 @@ - io.vertx.core.Launcher + org.folio.okapi.common.MainLauncher org.folio.tlib.example.MainVerticle true From 42c3e77fcdb46727f306c2fba6b0fb124f7e3944 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Thu, 21 Aug 2025 14:09:40 +0200 Subject: [PATCH 23/23] Use okapi-common 7.0.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fb48244..64dd45e 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ UTF-8 - 6.3.0-SNAPSHOT + 7.0.0 5.0.3