Commit 9a48b8cc1e72e9454c838c3f15ed2aca4bbbb310
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.
Showing
1 changed file
with
142 additions
and
0 deletions
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. | ... | ... |