matrei commented on PR #16275:
URL: https://github.com/apache/grails-core/pull/16275#issuecomment-5809880163

   Reviewed at head `11b3d985bf` (`feat/openapi-springdoc-8.0.x`, 19 commits) 
against `8.0.x`. Merge base `3dc36325dc`; `8.0.x` has taken 33 merges since, 
and `git merge-tree` reports no conflicts. CI is green on the head.
   
   Run locally at the head:
   
   | Module | Tests | Result |
   |---|---|---|
   | `grails-openapi` (`check`: tests, CodeNarc, Checkstyle) | 76 | pass, no 
violations |
   | `grails-test-examples-openapi` (`integrationTest`) | 10 | pass |
   | `grails-forge-core` (`OpenApiSpec`) | 3 | pass |
   | `validateDependencyVersions` for both new projects | | pass |
   
   I also ran the functional app with the default 
`"/$controller/$action?/$id?(.$format)?"` mapping added and dumped 
`/v3/api-docs` to check what an application actually gets, since the shipped 
example does not exercise that path (see 4). The document was clean: `Book` 
carried only `title`, `genre`, `id` and `version`, the expanded `/book/...` 
routes appeared alongside `/books`, every reference resolved, and every 
operation id was distinct. The `@Autowired(required = false)` property 
injection of `grailsApplication` works in a real context. I also checked the 
command object rule in `ActionAnnotations.isCommandObject` against 
`ControllerActionTransformer` and it mirrors it exactly, including the 
`Serializable` exclusion.
   
   The design is sound: derive everything from the mappings and the mapping 
context, let swagger-core resolve types, overlay the GORM constraints, and let 
the standard annotations correct the result. The findings are mostly leftovers 
from refactoring plus two behavioural gaps.
   
   ### 1. Dead code in `PersistentEntitySchemaBuilder`
   
   `retainReferenced` (`:208`), `referencedNames` (`:222`) and 
`collectReference` (`:233`) are private and nothing calls them; `buildCommand` 
walks declared fields instead. CodeNarc does not flag them because the repo 
does not enable `UnusedPrivateMethod`, but they should go, along with the 
Javadoc at `:204` that describes them.
   
   ### 2. Orphaned and stacked Javadoc from earlier refactors
   
   Groovydoc is published for this module, so these surface in the API docs. 
Each is a Javadoc block sitting on the wrong member or stacked on top of 
another block:
   
   - `UrlMappingsOpenApiCustomizer.groovy:145` describes `addExpandedMappings` 
but is attached to `addMappedOperation`; the real doc follows at `:151`.
   - `UrlMappingsOpenApiCustomizer.groovy:423` and `:427` describe a schema 
registration and a tag description method that no longer exist at that spot; 
both are stacked over the `disambiguateOperationIds` doc at `:431`.
   - `UrlMappingsOpenApiCustomizer.groovy:586` describes `asStaticName` but 
sits on `describe`.
   - `UrlMappingsOpenApiCustomizer.groovy:64` (class Javadoc) says mappings 
whose controller is only known per request "are skipped", which 
`addExpandedMappings` contradicts.
   - `PersistentEntitySchemaBuilder.groovy:143` describes 
`declaredPropertyTypes` but is stacked over the `collectTypeArguments` doc at 
`:147`.
   - `RestfulControllerActions.groovy:40` describes `DEFAULT_METHODS` but is 
attached to `DEFAULT_SUCCESS_CODE`.
   
   ### 3. An application with more than one datastore gets no schemas, silently
   
   `setMappingContextProvider` uses `getIfUnique()`, so with Hibernate and 
MongoDB together (two `MappingContext` beans) `mappingContext` is `null` and 
every operation is described without a shape. The Javadoc at `:93` acknowledges 
the "exactly one" condition, but the user gets no hint why the schemas 
vanished. Two options, in order of preference:
   
   - Iterate `provider.orderedStream()` and index the entities of every 
context, resolving the validator from the context that owns each entity. 
`indexEntitiesByController` and `constraintsFor` are the only two places that 
need the owning context.
   - Or keep the single-context rule and log at INFO when the provider returned 
nothing because it was ambiguous, and say so in the guide's limitations list.
   
   ### 4. The functional example does not cover the expansion path
   
   `grails-test-examples/openapi/.../UrlMappings.groovy` has only the 
`resources` mapping, so `addExpandedMappings`, and with it the injection of 
`grailsApplication`, is only exercised by unit specs that wire the customizer 
by hand. The PR body and guide advertise the default mapping routes as a 
headline feature. Adding `"/$controller/$action?/$id?(.$format)?" {}` to the 
example and one assertion on `/book/show/{id}` would make the functional spec 
cover it. I confirmed it passes.
   
   ### 5. A mapping that names a controller but no action is described as an 
action-less operation
   
   For `"/catalogue"(controller: 'book')` Grails dispatches to the controller's 
default action (`index`). `addMappedOperation` passes a `null` action through, 
so the operation id becomes `book_get`, a `RestfulController` falls into the 
generic 200 branch at `:736` with no paging parameters and a single-resource 
schema rather than the collection, and the action annotations are not 
consulted. Defaulting `mappedAction` to the controller's default action 
(`GrailsControllerClass.defaultAction`, `index` when unset) when the mapping 
names a controller only would make that case consistent with the rest.
   
   ### 6. Guide: nothing says the document is public
   
   `openApi.adoc` never mentions that `/v3/api-docs` and Swagger UI are served 
to anyone and enumerate every REST route, domain class and constraint. One 
sentence pointing at securing the two paths with whatever the application uses, 
or disabling them per environment (`springdoc.api-docs.enabled: false`), 
belongs under Getting Started or Customizing.
   
   ### 7. Nits
   
   - `settings.gradle:573`: `grails-test-examples-openapi` is inserted between 
`latency` and `namespaces`, breaking the alphabetical order of that list.
   - `UrlMappingsOpenApiCustomizer.groovy:83`, `:353`: 
`java.util.regex.Pattern` and `Matcher` are fully qualified inline; `:386` does 
the same with `grails.core.GrailsControllerClass`, and `ActionAnnotations` 
qualifies `io.swagger.v3.oas.annotations.Operation` and friends throughout. 
Aliased imports (as the file already does for `Parameter as ParameterModel`) 
would read better.
   - `PersistentEntitySchemaBuilder.entityNamed` scans every persistent entity 
per resolved schema. Fine at these sizes, but a name-keyed map built once in 
`build` is a one-liner.
   - The two `MediaType` fallbacks (`:738`, `:804`) emit `"application/json": 
{}` when the entity is unknown. Valid, but an operation with no content at all 
reads better than a content entry with no schema. Preference only.
   
   ### Verified as correct
   
   - `resources` really does generate `POST /books/{id}` for `update` 
(`DefaultUrlMappingEvaluator.java:1025`), so the extra `post` on `/books/{id}` 
is the mapping, not the customizer.
   - The generated `META-INF/grails-plugin.xml` carries `dependsOn 
urlMappings='version'` literally; every core plugin's descriptor does the same 
and the runtime reads the instance property, so this is not new.
   - springdoc 3.1.0 resolves against Spring Boot 4.1.1 and Framework 7.0.9 on 
the module classpath, and the functional app serves both the document and 
Swagger UI with it.
   - `disambiguateOperationIds` runs after the `_byAction` suffix is applied at 
expansion time, so the path-qualified suffix is only reached for two declared 
mappings hitting the same action, which the `RestApiMappingSpec` covers.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to