Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
name: Java CI with Maven

on:
push:
branches:
- spring-boot-4
pull_request:

jobs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ public class AbstractRouterFunctionVisitor {
*/
protected Map<Integer, List<String>> nestedPaths = new LinkedHashMap<>();

/**
* The Nested versions, keyed by nesting level, set by {@code version(...)} predicates
* declared on {@code nest(...)} calls. Sticky for all routes within the nest level.
*/
protected Map<Integer, String> nestedVersions = new LinkedHashMap<>();

/**
* The Is or.
*/
Expand Down Expand Up @@ -189,7 +195,16 @@ public void queryParam(String name, String value) {
* @param version the version
*/
public void version(String version) {
this.version = version;
// When visited as a nest(...) predicate, currentRouterFunctionDatas is null
// (cleared by commonStartNested) and the version must apply to every route in the
// nested block. When visited as a per-route predicate, currentRouterFunctionDatas
// has been freshly initialized by route(...) and the version is one-shot.
if (this.currentRouterFunctionDatas == null && this.level > 0) {
this.nestedVersions.put(this.level, version);
}
else {
this.version = version;
}
}

/**
Expand Down Expand Up @@ -281,6 +296,7 @@ public void attributes(Map<String, Object> map) {
*/
protected void commonEndNested() {
nestedPaths.remove(this.level);
nestedVersions.remove(this.level);
this.level--;
}

Expand All @@ -296,7 +312,7 @@ protected void commonStartNested() {
* Common route.
*/
protected void commonRoute() {
String currentVersion = this.version;
String currentVersion = (this.version != null) ? this.version : currentNestedVersion();
this.version = null;
this.routerFunctionDatas.addAll(currentRouterFunctionDatas);
currentRouterFunctionDatas.forEach(routerFunctionData -> {
Expand All @@ -308,6 +324,15 @@ protected void commonRoute() {
this.attributes = new HashMap<>();
}

private String currentNestedVersion() {
if (nestedVersions.isEmpty()) {
return null;
}
// Innermost nest wins
int innermost = nestedVersions.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1);
return nestedVersions.get(innermost);
}

/**
* Calculate header.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
*
* *
* * *
* * * *
* * * * *
* * * * * * Copyright 2019-2026 the original author or authors.
* * * * * *
* * * * * * Licensed under the Apache License, Version 2.0 (the "License");
* * * * * * you may not use this file except in compliance with the License.
* * * * * * You may obtain a copy of the License at
* * * * * *
* * * * * * https://www.apache.org/licenses/LICENSE-2.0
* * * * * *
* * * * * * Unless required by applicable law or agreed to in writing, software
* * * * * * distributed under the License is distributed on an "AS IS" BASIS,
* * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * * * *
* * * *
* * *
* *
*
*/

package org.springdoc.core.fn;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link AbstractRouterFunctionVisitor}.
*
* @author oss-bot
*/
class AbstractRouterFunctionVisitorTest {

@Test
void versionFromNestPredicateAppliesToEveryRouteInTheNest() {
TestVisitor visitor = new TestVisitor();

// nest(version("v1"), route(GET "/foo").and(route(GET "/bar")))
visitor.commonStartNestedPublic();
visitor.version("v1");
visitor.routePublic(() -> visitor.path("/foo"));
visitor.routePublic(() -> visitor.path("/bar"));
visitor.commonEndNestedPublic();

List<RouterFunctionData> routes = visitor.getRouterFunctionDatas();
assertThat(routes).hasSize(2);
assertThat(routes).extracting(RouterFunctionData::getPath).containsExactly("/foo", "/bar");
assertThat(routes).extracting(RouterFunctionData::getVersion).containsExactly("v1", "v1");
}

@Test
void versionFromAndNestPredicateAppliesToEveryRouteInItsOwnNest() {
TestVisitor visitor = new TestVisitor();

// nest(version("v1"), routes).andNest(version("v2"), routes)
visitor.commonStartNestedPublic();
visitor.version("v1");
visitor.routePublic(() -> visitor.path("/foo"));
visitor.routePublic(() -> visitor.path("/bar"));
visitor.commonEndNestedPublic();

visitor.commonStartNestedPublic();
visitor.version("v2");
visitor.routePublic(() -> visitor.path("/foo"));
visitor.routePublic(() -> visitor.path("/bar"));
visitor.commonEndNestedPublic();

List<RouterFunctionData> routes = visitor.getRouterFunctionDatas();
assertThat(routes).hasSize(4);
assertThat(routes).extracting(RouterFunctionData::getVersion)
.containsExactly("v1", "v1", "v2", "v2");
}

@Test
void perRouteVersionRemainsOneShotInsideNest() {
TestVisitor visitor = new TestVisitor();

// nest(path("/api"), route().version("v1").GET("/foo").and(route().GET("/bar")))
visitor.commonStartNestedPublic();
visitor.path("/api");

visitor.routePublic(() -> {
visitor.version("v1");
visitor.path("/foo");
});
visitor.routePublic(() -> visitor.path("/bar"));

visitor.commonEndNestedPublic();

List<RouterFunctionData> routes = visitor.getRouterFunctionDatas();
assertThat(routes).hasSize(2);
assertThat(routes).extracting(RouterFunctionData::getVersion).containsExactly("v1", null);
}

@Test
void innerNestVersionOverridesOuterNestVersion() {
TestVisitor visitor = new TestVisitor();

// nest(version("v1"), nest(version("v2"), route))
visitor.commonStartNestedPublic();
visitor.version("v1");

visitor.commonStartNestedPublic();
visitor.version("v2");
visitor.routePublic(() -> visitor.path("/foo"));
visitor.commonEndNestedPublic();

// After the inner nest ends, the outer version applies again
visitor.routePublic(() -> visitor.path("/bar"));
visitor.commonEndNestedPublic();

List<RouterFunctionData> routes = visitor.getRouterFunctionDatas();
assertThat(routes).extracting(RouterFunctionData::getPath).containsExactly("/foo", "/bar");
assertThat(routes).extracting(RouterFunctionData::getVersion).containsExactly("v2", "v1");
}

@Test
void topLevelPerRouteVersionRemainsOneShot() {
TestVisitor visitor = new TestVisitor();

// route().version("v0").GET("/top") followed by route().GET("/plain")
visitor.routePublic(() -> {
visitor.version("v0");
visitor.path("/top");
});
visitor.routePublic(() -> visitor.path("/plain"));

List<RouterFunctionData> routes = visitor.getRouterFunctionDatas();
assertThat(routes).extracting(RouterFunctionData::getVersion).containsExactly("v0", null);
}

/**
* Exposes the relevant {@code protected} hooks so tests can simulate the call order a
* real {@code RouterFunctions.Visitor} produces.
*/
private static class TestVisitor extends AbstractRouterFunctionVisitor {

void commonStartNestedPublic() {
commonStartNested();
}

void commonEndNestedPublic() {
commonEndNested();
}

void routePublic(Runnable predicateVisits) {
this.currentRouterFunctionDatas = new ArrayList<>();
predicateVisits.run();
commonRoute();
}
}
}