Commit 4a52a7f8 by Stéphane Leroy Committed by Spencer Gibb

Add Zuul Proxy regex serviceId to route mapping

fixes gh-693
parent 9f994870
...@@ -1093,6 +1093,26 @@ users: ...@@ -1093,6 +1093,26 @@ users:
listOfServers: example.com,google.com listOfServers: example.com,google.com
---- ----
You can provide convention between serviceId and routes using regexmapper.
It uses regular expression named group to extract variables from serviceId and inject them
into a route pattern.
.application.yml
[source,yaml]
----
zuul:
regexMapper:
enabled: true
servicePattern: "(?<name>^.+)-(?<version>v.+$)"
routePattern: "${version}/${name}"
----
This means that a serviceId "myusers-v1" will be mapped to route "/v1/myusers/**".
Any regular expression is accepted but all named group must be present in both servicePattern and routePattern.
If servicePattern do not match a serviceId, the default behavior is used. In exemple above,
a serviceId "myusers" will be mapped to route "/myusers/**" (no version detected)
These feature is disable by default and is only applied to discovered services.
To add a prefix to all mappings, set `zuul.prefix` to a value, such as To add a prefix to all mappings, set `zuul.prefix` to a value, such as
`/api`. The proxy prefix is stripped from the request before the `/api`. The proxy prefix is stripped from the request before the
request is forwarded by default (switch this behaviour off with request is forwarded by default (switch this behaviour off with
......
...@@ -21,6 +21,7 @@ import org.springframework.boot.actuate.endpoint.Endpoint; ...@@ -21,6 +21,7 @@ import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.trace.TraceRepository; import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cloud.client.actuator.HasFeatures; import org.springframework.cloud.client.actuator.HasFeatures;
import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.DiscoveryClient;
...@@ -32,8 +33,11 @@ import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEven ...@@ -32,8 +33,11 @@ import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEven
import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator; import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ServiceRouteMapper;
import org.springframework.cloud.netflix.zuul.filters.SimpleServiceRouteMapper;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter; import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter;
import org.springframework.cloud.netflix.zuul.filters.regex.RegExServiceRouteMapper;
import org.springframework.cloud.netflix.zuul.filters.route.RestClientRibbonCommandFactory; import org.springframework.cloud.netflix.zuul.filters.route.RestClientRibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory; import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter; import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter;
...@@ -66,6 +70,9 @@ public class ZuulProxyConfiguration extends ZuulConfiguration { ...@@ -66,6 +70,9 @@ public class ZuulProxyConfiguration extends ZuulConfiguration {
@Autowired @Autowired
private ServerProperties server; private ServerProperties server;
@Autowired
private ServiceRouteMapper serviceRouteMapper;
@Override @Override
public HasFeatures zuulFeature() { public HasFeatures zuulFeature() {
return HasFeatures.namedFeature("Zuul (Discovery)", ZuulProxyConfiguration.class); return HasFeatures.namedFeature("Zuul (Discovery)", ZuulProxyConfiguration.class);
...@@ -75,7 +82,7 @@ public class ZuulProxyConfiguration extends ZuulConfiguration { ...@@ -75,7 +82,7 @@ public class ZuulProxyConfiguration extends ZuulConfiguration {
@Override @Override
public ProxyRouteLocator routeLocator() { public ProxyRouteLocator routeLocator() {
return new ProxyRouteLocator(this.server.getServletPrefix(), this.discovery, return new ProxyRouteLocator(this.server.getServletPrefix(), this.discovery,
this.zuulProperties); this.zuulProperties, serviceRouteMapper);
} }
@Bean @Bean
...@@ -86,8 +93,8 @@ public class ZuulProxyConfiguration extends ZuulConfiguration { ...@@ -86,8 +93,8 @@ public class ZuulProxyConfiguration extends ZuulConfiguration {
// pre filters // pre filters
@Bean @Bean
public PreDecorationFilter preDecorationFilter() { public PreDecorationFilter preDecorationFilter(ProxyRouteLocator routeLocator) {
return new PreDecorationFilter(routeLocator(), return new PreDecorationFilter(routeLocator,
this.zuulProperties.isAddProxyHeaders()); this.zuulProperties.isAddProxyHeaders());
} }
...@@ -118,6 +125,27 @@ public class ZuulProxyConfiguration extends ZuulConfiguration { ...@@ -118,6 +125,27 @@ public class ZuulProxyConfiguration extends ZuulConfiguration {
} }
@Configuration @Configuration
@ConditionalOnProperty(name = "zuul.regexMapper.enabled", matchIfMissing = false)
protected static class RegexServiceRouteMapperConfiguration {
@Bean
public ServiceRouteMapper serviceRouteMapper(ZuulProperties props) {
return new RegExServiceRouteMapper(props.getRegexMapper().getServicePattern(),
props.getRegexMapper().getRoutePattern());
}
}
@Configuration
@ConditionalOnMissingBean(ServiceRouteMapper.class)
protected static class SimpleServiceRouteMapperConfiguration {
@Bean
public ServiceRouteMapper serviceRouteMapper() {
return new SimpleServiceRouteMapper();
}
}
@Configuration
@ConditionalOnClass(Endpoint.class) @ConditionalOnClass(Endpoint.class)
protected static class RoutesEndpointConfiguration { protected static class RoutesEndpointConfiguration {
......
...@@ -55,6 +55,8 @@ public class ProxyRouteLocator implements RouteLocator { ...@@ -55,6 +55,8 @@ public class ProxyRouteLocator implements RouteLocator {
private String servletPath; private String servletPath;
private ServiceRouteMapper serviceRouteMapper;
public ProxyRouteLocator(String servletPath, DiscoveryClient discovery, public ProxyRouteLocator(String servletPath, DiscoveryClient discovery,
ZuulProperties properties) { ZuulProperties properties) {
if (StringUtils.hasText(servletPath)) { // a servletPath is passed explicitly if (StringUtils.hasText(servletPath)) { // a servletPath is passed explicitly
...@@ -75,11 +77,17 @@ public class ProxyRouteLocator implements RouteLocator { ...@@ -75,11 +77,17 @@ public class ProxyRouteLocator implements RouteLocator {
} }
} }
} }
this.serviceRouteMapper = new SimpleServiceRouteMapper();
this.discovery = discovery; this.discovery = discovery;
this.properties = properties; this.properties = properties;
} }
public ProxyRouteLocator(String servletPath, DiscoveryClient discovery,
ZuulProperties properties, ServiceRouteMapper serviceRouteMapper) {
this(servletPath, discovery, properties);
this.serviceRouteMapper = serviceRouteMapper;
}
public void addRoute(String path, String location) { public void addRoute(String path, String location) {
this.staticRoutes.put(path, new ZuulRoute(path, location)); this.staticRoutes.put(path, new ZuulRoute(path, location));
resetRoutes(); resetRoutes();
...@@ -196,7 +204,7 @@ public class ProxyRouteLocator implements RouteLocator { ...@@ -196,7 +204,7 @@ public class ProxyRouteLocator implements RouteLocator {
for (String serviceId : services) { for (String serviceId : services) {
// Ignore specifically ignored services and those that were manually // Ignore specifically ignored services and those that were manually
// configured // configured
String key = "/" + serviceId + "/**"; String key = "/" + mapRouteToService(serviceId) + "/**";
if (staticServices.containsKey(serviceId) if (staticServices.containsKey(serviceId)
&& staticServices.get(serviceId).getUrl() == null) { && staticServices.get(serviceId).getUrl() == null) {
// Explicitly configured with no URL, cannot be ignored // Explicitly configured with no URL, cannot be ignored
...@@ -238,6 +246,10 @@ public class ProxyRouteLocator implements RouteLocator { ...@@ -238,6 +246,10 @@ public class ProxyRouteLocator implements RouteLocator {
return values; return values;
} }
protected String mapRouteToService(String serviceId) {
return this.serviceRouteMapper.apply(serviceId);
}
protected void addConfiguredRoutes(Map<String, ZuulRoute> routes) { protected void addConfiguredRoutes(Map<String, ZuulRoute> routes) {
Map<String, ZuulRoute> routeEntries = this.properties.getRoutes(); Map<String, ZuulRoute> routeEntries = this.properties.getRoutes();
for (ZuulRoute entry : routeEntries.values()) { for (ZuulRoute entry : routeEntries.values()) {
......
package org.springframework.cloud.netflix.zuul.filters;
/**
* @author Stéphane LEROY
*
* Provide a way to apply convention between routes and discovered services name.
*
*/
public interface ServiceRouteMapper {
/**
* Take a service Id (its discovered name) and return a route path.
*
* @param serviceId service discovered name
* @return route path
*/
String apply(String serviceId);
}
package org.springframework.cloud.netflix.zuul.filters;
/**
* @author Stéphane Leroy
*
* A simple passthru service route mapper.
*/
public class SimpleServiceRouteMapper implements ServiceRouteMapper {
@Override
public String apply(String serviceId) {
return serviceId;
}
}
...@@ -21,16 +21,15 @@ import java.util.LinkedHashMap; ...@@ -21,16 +21,15 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @author Dave Syer * @author Dave Syer
...@@ -57,6 +56,8 @@ public class ZuulProperties { ...@@ -57,6 +56,8 @@ public class ZuulProperties {
private boolean ignoreLocalService = true; private boolean ignoreLocalService = true;
private RegexMapper regexMapper = new RegexMapper();
@PostConstruct @PostConstruct
public void init() { public void init() {
for (Entry<String, ZuulRoute> entry : this.routes.entrySet()) { for (Entry<String, ZuulRoute> entry : this.routes.entrySet()) {
...@@ -76,6 +77,17 @@ public class ZuulProperties { ...@@ -76,6 +77,17 @@ public class ZuulProperties {
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public static class RegexMapper {
private boolean enabled = false;
private String servicePattern = "(?<name>.*)-(?<version>v.*$)";
private String routePattern = "${version}/${name}";
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ZuulRoute { public static class ZuulRoute {
private String id; private String id;
......
package org.springframework.cloud.netflix.zuul.filters.regex;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.cloud.netflix.zuul.filters.ServiceRouteMapper;
import org.springframework.util.StringUtils;
/**
* @author Stéphane Leroy
*
* This service route mapper use Java 7 RegEx named group feature to rewrite a discovered
* service Id into a route.
*
* Ex : If we want to map service Id [rest-service-v1] to /v1/rest-service/** route
* service pattern : "(?<name>.*)-(?<version>v.*$)" route pattern : "${version}/${name}"
*
* /!\ This implementation use Matcher.replaceFirst so only one match will be replace.
*/
public class RegExServiceRouteMapper implements ServiceRouteMapper {
/**
* A RegExp Pattern that extract needed information from a service ID. Ex :
* "(?<name>.*)-(?<version>v.*$)"
*/
private Pattern servicePattern;
/**
* A RegExp that refer to named groups define in servicePattern. Ex :
* "${version}/${name}"
*/
private String routePattern;
public RegExServiceRouteMapper(String servicePattern, String routePattern) {
this.servicePattern = Pattern.compile(servicePattern);
this.routePattern = routePattern;
}
/**
* Use servicePattern to extract groups and routePattern to construct the route.
*
* If there is no matches, the serviceId is returned.
*
* @param serviceId service discovered name
* @return route path
*/
@Override
public String apply(String serviceId) {
Matcher matcher = servicePattern.matcher(serviceId);
String route = matcher.replaceFirst(routePattern);
route = cleanRoute(route);
return (StringUtils.hasText(route) ? route : serviceId);
}
/**
* Route with regex and replace can be a bit messy when used with conditional named
* group. We clean here first and trailing '/' and remove multiple consecutive '/'
* @param route
* @return
*/
private String cleanRoute(final String route) {
String routeToClean = route.replaceAll("/{2,}", "/");
if (routeToClean.startsWith("/")) {
routeToClean = routeToClean.substring(1);
}
if (routeToClean.endsWith("/")) {
routeToClean = routeToClean.substring(0, routeToClean.length() - 1);
}
return routeToClean;
}
}
...@@ -16,14 +16,6 @@ ...@@ -16,14 +16,6 @@
package org.springframework.cloud.netflix.zuul.filters; package org.springframework.cloud.netflix.zuul.filters;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.given;
import static org.mockito.MockitoAnnotations.initMocks;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
...@@ -35,8 +27,17 @@ import org.springframework.cloud.client.DefaultServiceInstance; ...@@ -35,8 +27,17 @@ import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator.ProxyRouteSpec; import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator.ProxyRouteSpec;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
import org.springframework.cloud.netflix.zuul.filters.regex.RegExServiceRouteMapper;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.given;
import static org.mockito.MockitoAnnotations.initMocks;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @author Dave Syer * @author Dave Syer
...@@ -518,6 +519,34 @@ public class ProxyRouteLocatorTests { ...@@ -518,6 +519,34 @@ public class ProxyRouteLocatorTests {
assertMapping(routesMap, MYSERVICE); assertMapping(routesMap, MYSERVICE);
} }
@Test
public void testRegExServiceRouteMapperNoServiceIdMatches() {
given(this.discovery.getServices()).willReturn(Collections.singletonList(MYSERVICE));
RegExServiceRouteMapper regExServiceRouteMapper = new RegExServiceRouteMapper(properties.getRegexMapper().getServicePattern(),
properties.getRegexMapper().getRoutePattern());
ProxyRouteLocator routeLocator = new ProxyRouteLocator("/", this.discovery,
this.properties, regExServiceRouteMapper);
Map<String, String> routesMap = routeLocator.getRoutes();
assertNotNull("routesMap was null", routesMap);
assertFalse("routesMap was empty", routesMap.isEmpty());
assertMapping(routesMap, MYSERVICE);
}
@Test
public void testRegExServiceRouteMapperServiceIdMatches() {
given(this.discovery.getServices()).willReturn(Collections.singletonList("rest-service-v1"));
RegExServiceRouteMapper regExServiceRouteMapper = new RegExServiceRouteMapper(properties.getRegexMapper().getServicePattern(),
properties.getRegexMapper().getRoutePattern());
ProxyRouteLocator routeLocator = new ProxyRouteLocator("/", this.discovery,
this.properties, regExServiceRouteMapper);
Map<String, String> routesMap = routeLocator.getRoutes();
assertNotNull("routesMap was null", routesMap);
assertFalse("routesMap was empty", routesMap.isEmpty());
assertMapping(routesMap, "rest-service-v1", "v1/rest-service");
}
protected void assertMapping(Map<String, String> routesMap, String serviceId) { protected void assertMapping(Map<String, String> routesMap, String serviceId) {
assertMapping(routesMap, serviceId, serviceId); assertMapping(routesMap, serviceId, serviceId);
......
package org.springframework.cloud.netflix.zuul.filters.regex;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.StaticServerList;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.RoutesEndpoint;
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.netflix.zuul.filters.regex.RegExServiceRouteMapperIntegrationTests.SERVICE_ID;
/**
* @author Stéphane Leroy
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SampleCustomZuulProxyApplication.class)
@WebIntegrationTest(value = { "spring.application.name=regex-test-application",
"spring.jmx.enabled=true" }, randomPort = true)
@TestPropertySource(properties = { "eureka.client.enabled=false",
"zuul.regexMapper.enabled=true",
"zuul.regexMapper.servicePattern=(?<domain>^.+)-(?<name>.+)-(?<version>v.+$)",
"zuul.regexMapper.routePattern=${version}/${domain}/${name}" })
@DirtiesContext
public class RegExServiceRouteMapperIntegrationTests {
protected static final String SERVICE_ID = "domain-service-v1";
@Value("${local.server.port}")
private int port;
@Autowired
private ProxyRouteLocator routes;
@Autowired
private RoutesEndpoint endpoint;
@Test
public void getRegexMappedService() {
endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/v1/domain/service/get/1",
HttpMethod.GET, new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Get 1", result.getBody());
}
@Test
public void getStaticRoute() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port);
endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/self/get/1", HttpMethod.GET,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Get 1", result.getBody());
}
}
@Configuration
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
@RibbonClient(value = SERVICE_ID, configuration = SimpleRibbonClientConfiguration.class)
class SampleCustomZuulProxyApplication {
@Bean
public DiscoveryClient discoveryClient() {
DiscoveryClient discoveryClient = mock(DiscoveryClient.class);
List<String> services = new ArrayList<>();
services.add(SERVICE_ID);
when(discoveryClient.getServices()).thenReturn(services);
return discoveryClient;
}
@RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
public String get(@PathVariable String id) {
return "Get " + id;
}
public static void main(String[] args) {
SpringApplication.run(SampleCustomZuulProxyApplication.class, args);
}
}
@Configuration
class SimpleRibbonClientConfiguration {
@Value("${local.server.port}")
private int port = 0;
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server("localhost", this.port));
}
}
package org.springframework.cloud.netflix.zuul.filters.regex;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
/**
* @author Stéphane Leroy
*/
public class RegExServiceRouteMapperTests {
/**
* Service pattern that follow convention {domain}-{name}-{version}. The name is
* optional
*/
public static final String SERVICE_PATTERN = "(?<domain>^\\w+)(-(?<name>\\w+)-|-)(?<version>v\\d+$)";
public static final String ROUTE_PATTERN = "${version}/${domain}/${name}";
@Test
public void test_return_mapped_route_if_serviceid_matches() {
RegExServiceRouteMapper toTest = new RegExServiceRouteMapper(SERVICE_PATTERN,
ROUTE_PATTERN);
assertEquals("service version convention", "v1/rest/service",
toTest.apply("rest-service-v1"));
}
@Test
public void test_return_serviceid_if_no_matches() {
RegExServiceRouteMapper toTest = new RegExServiceRouteMapper(SERVICE_PATTERN,
ROUTE_PATTERN);
// No version here
assertEquals("No matches for this service id", "rest-service",
toTest.apply("rest-service"));
}
@Test
public void test_route_should_be_cleaned_before_returned() {
// Messy patterns
RegExServiceRouteMapper toTest = new RegExServiceRouteMapper(SERVICE_PATTERN
+ "(?<nevermatch>.)?", "/${version}/${nevermatch}/${domain}/${name}/");
assertEquals("No matches for this service id", "v1/domain/service",
toTest.apply("domain-service-v1"));
assertEquals("No matches for this service id", "v1/domain",
toTest.apply("domain-v1"));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment