Commit 9a48b8cc1e72e9454c838c3f15ed2aca4bbbb310

Authored by zichun
1 parent 10d5e4b4

feat(bootstrap): group OpenAPI spec by PBC / platform area

Adds 15 GroupedOpenApi beans that split the single giant OpenAPI
spec into per-PBC + per-platform-module focused specs selectable
from Swagger UI's top-right "Select a definition" dropdown. No
@RestController changes — all groups are defined by URL prefix
in platform-bootstrap, so adding a new PBC means touching exactly
this file (plus the controller itself). Each group stays
additive alongside the default /v3/api-docs.

**Groups shipped:**
  Platform
    platform-core     — /api/v1/auth/**, /api/v1/_meta/**
    platform-workflow — /api/v1/workflow/**
    platform-jobs     — /api/v1/jobs/**
    platform-files    — /api/v1/files/**
    platform-reports  — /api/v1/reports/**
  Core PBCs
    pbc-identity     — /api/v1/identity/**
    pbc-catalog      — /api/v1/catalog/**
    pbc-partners     — /api/v1/partners/**
    pbc-inventory    — /api/v1/inventory/**
    pbc-warehousing  — /api/v1/warehousing/**
    pbc-orders       — /api/v1/orders/** (sales + purchase together)
    pbc-production   — /api/v1/production/**
    pbc-quality      — /api/v1/quality/**
    pbc-finance      — /api/v1/finance/**
  Plug-in dispatcher
    plugins          — /api/v1/plugins/**

**Why path-prefix grouping, not package-scan grouping.**
Package-scan grouping would force OpenApiConfiguration to know
every PBC's Kotlin package name and drift every time a PBC ships
or a controller moves. Path-prefix grouping only shifts when
`@RequestMapping` changes — which is already a breaking API
change that would need review anyway. This keeps the control
plane for grouping in one file while the routing stays in each
controller.

**Why pbc-orders is one group, not split sales/purchase.**
Both controllers share the `/api/v1/orders/` prefix, and sales /
purchase are the same shape in practice — splitting them into
two groups would just duplicate the dropdown entries. A future
chunk can split if a real consumer asks for it.

**Primary group unchanged.** The default /v3/api-docs continues
to return the full merged spec (every operation in one
document). The grouped specs are additive at
/v3/api-docs/<group-name> and clients can pick whichever they
need. Swagger UI defaults to showing the first group in the
dropdown.

**Smoke-tested end-to-end against real Postgres:**
  - GET /v3/api-docs/swagger-config returns 15 groups with
    human-readable display names
  - Per-group path counts (confirming each group is focused):
    pbc-production: 10 paths
    pbc-catalog:     6 paths
    pbc-orders:     12 paths
    platform-core:   9 paths
    platform-files:  3 paths
    plugins:         1 path (dispatcher)
  - Default /v3/api-docs continues to return the full spec.

24 modules, 355 unit tests, all green.
platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/openapi/OpenApiConfiguration.kt
... ... @@ -8,6 +8,7 @@ import io.swagger.v3.oas.models.info.License
8 8 import io.swagger.v3.oas.models.security.SecurityRequirement
9 9 import io.swagger.v3.oas.models.security.SecurityScheme
10 10 import io.swagger.v3.oas.models.servers.Server
  11 +import org.springdoc.core.models.GroupedOpenApi
11 12 import org.springframework.beans.factory.ObjectProvider
12 13 import org.springframework.boot.info.BuildProperties
13 14 import org.springframework.context.annotation.Bean
... ... @@ -126,6 +127,147 @@ class OpenApiConfiguration(
126 127 )
127 128 }
128 129  
  130 + // ─── Grouped specs for the Swagger UI dropdown ───────────────────
  131 + //
  132 + // Without explicit groups, springdoc produces ONE giant spec and
  133 + // Swagger UI displays 76+ operations in a single unorganized
  134 + // list. Grouping by URL prefix gives operators + the future R1
  135 + // SPA a per-PBC filter in Swagger UI's top-right "Select a
  136 + // definition" dropdown, and lets clients fetch a focused spec
  137 + // via /v3/api-docs/<group-name> for codegen against just one
  138 + // domain area at a time.
  139 + //
  140 + // Every group is driven by its URL prefix rather than by package
  141 + // scan, because package-scan grouping would force this file (in
  142 + // platform-bootstrap) to know every PBC's Kotlin package name
  143 + // and re-order whenever a PBC is added. Path-prefix grouping
  144 + // only changes when the PBC's @RequestMapping changes — which
  145 + // is already a breaking change that would need review anyway.
  146 + //
  147 + // The primary "all" group (vibeErpOpenApi bean above) stays the
  148 + // default at /v3/api-docs. Grouped endpoints serve at
  149 + // /v3/api-docs/<group-name>.
  150 +
  151 + @Bean
  152 + fun platformCoreGroup(): GroupedOpenApi =
  153 + GroupedOpenApi.builder()
  154 + .group("platform-core")
  155 + .displayName("Platform · Core (auth, meta, metadata)")
  156 + .pathsToMatch("/api/v1/auth/**", "/api/v1/_meta/**")
  157 + .build()
  158 +
  159 + @Bean
  160 + fun platformWorkflowGroup(): GroupedOpenApi =
  161 + GroupedOpenApi.builder()
  162 + .group("platform-workflow")
  163 + .displayName("Platform · Workflow (Flowable BPMN)")
  164 + .pathsToMatch("/api/v1/workflow/**")
  165 + .build()
  166 +
  167 + @Bean
  168 + fun platformJobsGroup(): GroupedOpenApi =
  169 + GroupedOpenApi.builder()
  170 + .group("platform-jobs")
  171 + .displayName("Platform · Jobs (Quartz)")
  172 + .pathsToMatch("/api/v1/jobs/**")
  173 + .build()
  174 +
  175 + @Bean
  176 + fun platformFilesGroup(): GroupedOpenApi =
  177 + GroupedOpenApi.builder()
  178 + .group("platform-files")
  179 + .displayName("Platform · Files (object storage)")
  180 + .pathsToMatch("/api/v1/files/**")
  181 + .build()
  182 +
  183 + @Bean
  184 + fun platformReportsGroup(): GroupedOpenApi =
  185 + GroupedOpenApi.builder()
  186 + .group("platform-reports")
  187 + .displayName("Platform · Reports (JasperReports)")
  188 + .pathsToMatch("/api/v1/reports/**")
  189 + .build()
  190 +
  191 + @Bean
  192 + fun pbcIdentityGroup(): GroupedOpenApi =
  193 + GroupedOpenApi.builder()
  194 + .group("pbc-identity")
  195 + .displayName("PBC · Identity")
  196 + .pathsToMatch("/api/v1/identity/**")
  197 + .build()
  198 +
  199 + @Bean
  200 + fun pbcCatalogGroup(): GroupedOpenApi =
  201 + GroupedOpenApi.builder()
  202 + .group("pbc-catalog")
  203 + .displayName("PBC · Catalog (items, UoMs)")
  204 + .pathsToMatch("/api/v1/catalog/**")
  205 + .build()
  206 +
  207 + @Bean
  208 + fun pbcPartnersGroup(): GroupedOpenApi =
  209 + GroupedOpenApi.builder()
  210 + .group("pbc-partners")
  211 + .displayName("PBC · Partners (customers, suppliers, contacts)")
  212 + .pathsToMatch("/api/v1/partners/**")
  213 + .build()
  214 +
  215 + @Bean
  216 + fun pbcInventoryGroup(): GroupedOpenApi =
  217 + GroupedOpenApi.builder()
  218 + .group("pbc-inventory")
  219 + .displayName("PBC · Inventory (locations, balances, movements)")
  220 + .pathsToMatch("/api/v1/inventory/**")
  221 + .build()
  222 +
  223 + @Bean
  224 + fun pbcWarehousingGroup(): GroupedOpenApi =
  225 + GroupedOpenApi.builder()
  226 + .group("pbc-warehousing")
  227 + .displayName("PBC · Warehousing (stock transfers)")
  228 + .pathsToMatch("/api/v1/warehousing/**")
  229 + .build()
  230 +
  231 + @Bean
  232 + fun pbcOrdersGroup(): GroupedOpenApi =
  233 + GroupedOpenApi.builder()
  234 + .group("pbc-orders")
  235 + .displayName("PBC · Orders (sales + purchase)")
  236 + .pathsToMatch("/api/v1/orders/**")
  237 + .build()
  238 +
  239 + @Bean
  240 + fun pbcProductionGroup(): GroupedOpenApi =
  241 + GroupedOpenApi.builder()
  242 + .group("pbc-production")
  243 + .displayName("PBC · Production (work orders, routings, shop-floor)")
  244 + .pathsToMatch("/api/v1/production/**")
  245 + .build()
  246 +
  247 + @Bean
  248 + fun pbcQualityGroup(): GroupedOpenApi =
  249 + GroupedOpenApi.builder()
  250 + .group("pbc-quality")
  251 + .displayName("PBC · Quality (inspection records)")
  252 + .pathsToMatch("/api/v1/quality/**")
  253 + .build()
  254 +
  255 + @Bean
  256 + fun pbcFinanceGroup(): GroupedOpenApi =
  257 + GroupedOpenApi.builder()
  258 + .group("pbc-finance")
  259 + .displayName("PBC · Finance (journal entries, AR/AP)")
  260 + .pathsToMatch("/api/v1/finance/**")
  261 + .build()
  262 +
  263 + @Bean
  264 + fun pluginEndpointsGroup(): GroupedOpenApi =
  265 + GroupedOpenApi.builder()
  266 + .group("plugins")
  267 + .displayName("Plug-in HTTP dispatcher")
  268 + .pathsToMatch("/api/v1/plugins/**")
  269 + .build()
  270 +
129 271 companion object {
130 272 /**
131 273 * Name of the security scheme registered in Components.
... ...