Skip to content

URL mapping group defaults support#15526

Merged
codeconsole merged 4 commits intoapache:7.1.xfrom
codeconsole:url-mapping-group-defaults
Mar 27, 2026
Merged

URL mapping group defaults support#15526
codeconsole merged 4 commits intoapache:7.1.xfrom
codeconsole:url-mapping-group-defaults

Conversation

@codeconsole
Copy link
Copy Markdown
Contributor

@codeconsole codeconsole commented Mar 23, 2026

Dramatically simplifies group url mappings by allowing parameter inheritance.

e.g.

group "/api", namespace: 'api', controller: 'defaultCtrl', {
    "/special"(controller: 'specialCtrl', action: 'handle')  // overrides controller
    "/normal"(action: 'index')                                // inherits controller
}

you can even do

group "/members", namespace: 'api', controller: 'member', {
    "/$action?"()  // supports / and action resolution
    "/$username"(action: 'profile') 
}

@testlens-app

This comment has been minimized.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for group-level default mapping parameters in the URL mappings DSL, so child mappings can inherit controller/namespace/etc. from a surrounding group(...) and override them selectively.

Changes:

  • Introduces a new group overload intended to accept default mapping parameters and apply them to child mappings.
  • Extends MetaMappingInfo with a groupDefaults map to support inheritance across nested groups/mappings.
  • Adds documentation and a new Spock spec covering inheritance/override behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
grails-web-url-mappings/src/test/groovy/org/grails/web/mapping/GroupDefaultsSpec.groovy Adds tests for group defaults inheritance/override and nested group inheritance.
grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/MetaMappingInfo.groovy Stores groupDefaults on mapping metadata to enable inheritance.
grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/DefaultUrlMappingEvaluator.java Implements group-defaults support and inheritance during mapping evaluation.
grails-doc/src/en/guide/theWebLayer/urlmappings/mappingToControllersAndActions.adoc Documents the new “Group Defaults” feature with examples and supported keys.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

uri = parentResource.uri.concat(uri);
}
super.group(defaults, uri, mappings);
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This override has the same argument-order issue as the parent overload. For nested groups, Groovy will call group(String, Map, Closure) when using group "/x", controller:'c', { ... }, but this method only matches group(Map, String, Closure), causing nested group defaults to fail at runtime.

Suggested change
}
}
@Override
public void group(String uri, Map<String, Object> defaults, Closure<?> mappings) {
if (parentResource != null) {
uri = parentResource.uri.concat(uri);
}
super.group(uri, defaults, mappings);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Having a Map<String, Object> default first as arguments makes it a Named argument collector.

Comment on lines +454 to +482
private void applyGroupDefaults(MetaMappingInfo mappingInfo, Map<String, Object> defaults) {
if (defaults == null || defaults.isEmpty()) {
return;
}
// Merge defaults with any inherited group defaults
if (mappingInfo.getGroupDefaults() == null) {
mappingInfo.setGroupDefaults(new HashMap<>(defaults));
} else {
mappingInfo.getGroupDefaults().putAll(defaults);
}
if (defaults.containsKey(CONTROLLER)) {
mappingInfo.setController(defaults.get(CONTROLLER));
}
if (defaults.containsKey(ACTION)) {
mappingInfo.setAction(defaults.get(ACTION));
}
if (defaults.containsKey(NAMESPACE)) {
mappingInfo.setNamespace(defaults.get(NAMESPACE));
}
if (defaults.containsKey(PLUGIN)) {
mappingInfo.setPlugin(defaults.get(PLUGIN));
}
if (defaults.containsKey(VIEW)) {
mappingInfo.setView(defaults.get(VIEW));
}
if (defaults.containsKey(HTTP_METHOD)) {
mappingInfo.setHttpMethod(defaults.get(HTTP_METHOD).toString());
}
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyGroupDefaults currently only updates MetaMappingInfo. When URL mappings are evaluated from a Script (binding != null), controller/action/namespace defaults are resolved from the script Binding (see getVariableFromNamedArgsOrBinding), not from MetaMappingInfo, so these defaults may be ignored in the common URLMappings.groovy path. To make group defaults effective for Script-based mappings, either (1) push defaults into the Binding for the duration of the group closure (and restore afterward), or (2) update the variable resolution logic to fall back to the defaultValue when the Binding doesn't define the key.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +34
void "group defaults are inherited by child mappings"() {
given:
def holder = getUrlMappingsHolder {
group "/api", namespace: 'api', controller: 'resource', {
"/list"(action: 'list')
"/show"(action: 'show')
}
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests exercise DefaultUrlMappingEvaluator.evaluateMappings(Closure) (binding == null). Since production URLMappings.groovy evaluation typically goes through the Script/binding path (evaluateMappings(Resource/Class)), consider adding at least one test that evaluates a Script-based mappings resource/class and verifies group defaults inheritance there as well (otherwise the feature may pass tests but fail in real apps).

Copilot uses AI. Check for mistakes.
Comment on lines +438 to +447
public void group(Map<String, Object> defaults, String uri, Closure<?> mappingsClosure) {
try {
var parentResource = new ParentResource(null, uri, true, true);
parentResources.push(parentResource);
var mappingInfo = pushNewMetaMappingInfo();
applyGroupDefaults(mappingInfo, defaults);
var builder = new UrlGroupMappingRecursionBuilder(this, parentResource);
mappingsClosure.setDelegate(builder);
mappingsClosure.setResolveStrategy(Closure.DELEGATE_FIRST);
mappingsClosure.call();
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter order of this overload doesn't match the DSL call style shown in the PR description/tests (group "/api", namespace: ..., { ... }), which Groovy desugars to group(String, Map, Closure). With the current signature (group(Map, String, Closure)), this will result in a MissingMethodException at runtime. Consider changing the overload to group(String uri, Map<String,Object> defaults, Closure<?> mappingsClosure) (and adjust the recursion builder override accordingly).

Copilot uses AI. Check for mistakes.
uri = parentResource.uri.concat(uri);
}
super.group(defaults, uri, mappings);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Having a Map<String, Object> default first as arguments makes it a Named argument collector.

@testlens-app

This comment has been minimized.

@testlens-app
Copy link
Copy Markdown

testlens-app bot commented Mar 27, 2026

✅ All tests passed ✅

⚠️ TestLens detected flakiness ⚠️

Test Summary

Check Project/Task Test Runs
CI - Groovy Joint Validation Build / build_grails :grails-testing-support-dbcleanup-postgresql:test PostgresDatabaseCleanerFunctionalSpec ❌ ✅

🏷️ Commit: 37658a2
▶️ Tests: 44111 executed
⚪️ Checks: 35/35 completed


Learn more about TestLens at testlens.app.

Copy link
Copy Markdown
Contributor

@matrei matrei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks solid and very useful, thank you @codeconsole !

@codeconsole codeconsole merged commit cb0c9a6 into apache:7.1.x Mar 27, 2026
37 of 38 checks passed
@jdaugherty jdaugherty deleted the url-mapping-group-defaults branch March 29, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants