Explorar el Código

refactor include handling, handlebars defaults. add README.

tags/2.0.1
Jonathan Cobb hace 4 años
padre
commit
fe442f58eb
Se han modificado 10 ficheros con 341 adiciones y 68 borrados
  1. +12
    -11
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunner.java
  2. +0
    -27
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptIncludeClasspathHandler.java
  3. +33
    -1
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptIncludeHandler.java
  4. +3
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptIncludeHandlerBase.java
  5. +261
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/README.md
  6. +6
    -6
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/SimpleApiRunnerListener.java
  7. +1
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/main/ScriptMainBase.java
  8. +10
    -0
      wizard-server-test/src/main/java/org/cobbzilla/wizardtest/resources/AbstractResourceIT.java
  9. +14
    -22
      wizard-server-test/src/main/java/org/cobbzilla/wizardtest/resources/ApiModelTestBase.java
  10. +1
    -1
      wizard-server/src/main/java/org/cobbzilla/wizard/server/RestServerConfigurationFilter.java

+ 12
- 11
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunner.java Ver fichero

@@ -91,21 +91,22 @@ public class ApiRunner {

private Map<String, ApiClientBase> alternateApis = new HashMap<>();

private ApiRunnerListener listener;
@Getter @Setter private ApiScriptIncludeHandler includeHandler;
private ApiRunnerListener listener = new ApiRunnerListenerBase("default");
@Getter @Setter private ApiScriptIncludeHandler includeHandler = new ApiScriptIncludeHandlerBase();

protected final Map<String, Object> ctx = new ConcurrentHashMap<>();
public Map<String, Object> getContext () { return ctx; }

@Getter(lazy=true) private final Handlebars handlebars = initHandlebars();
protected Handlebars initHandlebars() {
final Handlebars hb = new Handlebars(new HandlebarsUtil("api-runner(" + api + ")"));
HandlebarsUtil.registerUtilityHelpers(hb);
HandlebarsUtil.registerCurrencyHelpers(hb);
HandlebarsUtil.registerDateHelpers(hb);
HandlebarsUtil.registerJurisdictionHelpers(hb, SimpleJurisdictionResolver.instance);
HandlebarsUtil.registerJavaScriptHelper(hb, StandardJsEngine::new);
return hb;
@Getter(lazy=true) private final Handlebars handlebars = standardHandlebars(new Handlebars(new HandlebarsUtil("api-runner(" + api + ")")));

public static Handlebars standardHandlebars(Handlebars hbs) {
HandlebarsUtil.registerUtilityHelpers(hbs);
HandlebarsUtil.registerDateHelpers(hbs);
HandlebarsUtil.registerCurrencyHelpers(hbs);
HandlebarsUtil.registerJavaScriptHelper(hbs, StandardJsEngine::new);
HandlebarsUtil.registerJurisdictionHelpers(hbs, SimpleJurisdictionResolver.instance);
HandlebarsUtil.registerJavaScriptHelper(hbs, StandardJsEngine::new);
return hbs;
}

protected final Map<String, Class> storeTypes = new HashMap<>();


+ 0
- 27
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptIncludeClasspathHandler.java Ver fichero

@@ -1,27 +0,0 @@
package org.cobbzilla.wizard.client.script;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;

import java.nio.file.Paths;

import static org.cobbzilla.util.io.StreamUtil.stream2string;

@Accessors(chain=true)
public class ApiScriptIncludeClasspathHandler implements ApiScriptIncludeHandler {

@Getter @Setter private String includePrefix;
@Getter @Setter private String commonPath;

@Override public String include(String path) {
final String fileName = path + ".json";
try {
return stream2string(Paths.get(getIncludePrefix(), fileName).toString());
} catch (IllegalArgumentException e) {
if (getCommonPath() == null) throw e;
return stream2string(Paths.get(getCommonPath(), fileName).toString());
}
}

}

+ 33
- 1
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptIncludeHandler.java Ver fichero

@@ -1,7 +1,39 @@
package org.cobbzilla.wizard.client.script;

import org.cobbzilla.util.string.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;

import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.shortErrorString;
import static org.cobbzilla.util.io.StreamUtil.loadResourceAsString;
import static org.cobbzilla.util.io.StreamUtil.stream2string;

public interface ApiScriptIncludeHandler {

String include (String path);
Logger log = LoggerFactory.getLogger(ApiScriptIncludeHandler.class);
List<String> DEFAULT_INCLUDE_PATHS = Arrays.asList("", "models", "tests", "include", "models/tests", "models/include");

default List<String> getIncludePaths() { return DEFAULT_INCLUDE_PATHS; }

default String include (String path) {
final String fileName = path + ".json";
for (String inc : getIncludePaths()) {
try {
return stream2string(Paths.get(inc, fileName).toString());
} catch (Exception e) {
try {
return loadResourceAsString(inc + "/" + fileName);
} catch (Exception e2) {
log.debug("include(" + path + "): not found in " + inc+": (e="+shortErrorString(e)+", e2="+shortErrorString(e2)+")");
}
}
}
return die("include("+path+"): not found anywhere in "+ StringUtil.toString(getIncludePaths()));
}

}

+ 3
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptIncludeHandlerBase.java Ver fichero

@@ -0,0 +1,3 @@
package org.cobbzilla.wizard.client.script;

public class ApiScriptIncludeHandlerBase implements ApiScriptIncludeHandler {}

+ 261
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/README.md Ver fichero

@@ -0,0 +1,261 @@
# ApiRunner
ApiRunner is a set of Java tools to facilitate integration testing of REST APIs that use the cobbzilla-wizard framework
for running the API and populating a data model.

ApiRunner uses a declarative approach to API testing, while allowing highly dynamic behavior and state management.

## JUnit
The easiest way to use ApiRunner is to create a JUnit test class that is a subclass of `ApiModelTestBase`, and
call modelTest, for example:

```code
import org.apache.commons.io.FileUtils;
import org.cobbzilla.wizard.server.config.factory.ConfigurationSource;
import org.cobbzilla.wizardtest.resources.AbstractResourceIT;
import org.junit.Test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class FooTest extends AbstractResourceIT {

@Override protected ConfigurationSource getConfigurationSource() {
return () -> new FileInputStream("/path/to/config.yml");
}

@Test public void testSomeApiStuff () throws Exception { runScript("basic_test"); }
@Test public void testSomeOtherApiStuff () throws Exception { runScript("some_dir/another_test"); }
}
```

When JUnit runs, it will call methods testSomeApiStuff and testSomeOtherApiStuff.
For each of these tests, `runScript` will run the ApiScripts contained in each file. The default resolution is to append `.json` and
try to load from the current classloader. In the above, the ApiRunner would expect to find `basic_test.json` in the base resource directory
and `some_dir/another_test.json` in
The default "relative path" for test scripts is `models/tests`, and must be visible on the classpath (via `ClassLoader.getResourceAsStream`, using the JUnit class's class-loader).

Subclasses can override the default include resolution process, for example to load from a directory on the filesystem
instead of the classpath:
```code
import org.apache.commons.io.FileUtils;
...
public class FooTest extends AbstractResourceIT {
...
protected String resolveInclude(String path) {
try {
return FileUtils.readFileToString(new File("/tmp/my_tests/"+path+".json"), "UTF-8");
} catch (IOException e) {
throw new IllegalStateException("resolveInclude("+path+"): "+e, e);
}
}
}
```
# ApiScript
An ApiScript is a JSON file of type ApiScript[]. The root element is an array ApiScript objects.

The array may be empty, in which case the running the script is a "no-op".

If the script array contains ApiScript objects, each such object represents an API request/response, and some additional associated data.

It's design supports the idea that simple things should be easy, and complex things should be possible.

# ApiScriptRequest
The `request` portion of an ApiScript contains minimally a `uri` field. This is taken to be relative to an API "base"
which is set elsewhere in the framework, and provided as a context variable to the ApiRunner.

## GET requests
Just specify the `uri` property, nothing else. ApiRunner will execute the GET request against the API, and return the
JSON object, optionally storing it in a context variable if the `store` property is set on the response.

{"request": "/me"}

The ApiRunner will verify that the response had a 200 status, but otherwise does nothing with the response.

## POST requests
To `POST` data, add an `entity` property:

{
"request" {
"uri": "/account/info",
"entity": {
"username": "jsmith123",
"some_prop": "some_value"
}
}
}
}

## Other requests
For requests with a method that is not `GET` or `POST`, add a `method` property.
The method name is case-insensitive, use capitals or lowercase at your preference.

For example:

{
"request" {
"uri": "/account/preferences",
"method": "put",
"entity": [
{"favorite_fruit": "apple"},
{"best_movie": "Logan's Run"}
]
}
}
}

## ApiSession
As the ApiRunner runs, each connection to the backend API server is done in the context of a session, or no session.
Without a session, (or if the `request.session` property is `new`), the ApiRunner will not send an API credential token
with any request.

For each ApiScript that the ApiRunner runs, a single session will be used. The ApiScript can specify a
`request.session` of `new` to indicate that any previous session should not be used, and the request should be made
without any authentication added.
Once a session is established, or set, the corresponding session token will be sent to the API server,
until some subsequent ApiScript sets the `request.session` property to a different session, or to `new`.
A session can be established by an ApiScript in two ways:

### Set session from an ApiScriptResponse
{
"request" {
"uri": "/login",
"method": "put",
"entity": [
{"username": "jsmith123"},
{"password": "B8F8UJ7PZX93AF9M4EJ8O9QXY"}
]
}
},
"response": {
"sessionName": "userSession",
"session": "token"
}
}

In the above, if the `/login` request succeeds, then the ApiRunner will create a new API session and continue to use
that session for future requests, until changed.
The session will be saved by the ApiRunner under the name `userSession`. The `"session": "token"` part means that, in the JSON returned from the
`/login` request, the `token` property contains the session token.

In order to ensure this token is returned to the server in the appropriate header, API tests requests require
small subclass of `ApiClientBase` to tell ApiRunner which HTTP header to use for sending the session token.
Do this by overriding the `getApi` method and providing your subclass which has a `getTokenHeader` method defined.
```code
import org.cobbzilla.wizard.client.ApiClientBase;
...

public class FooTest extends AbstractResourceIT {

@Override public ApiClientBase getApi() {
return new ApiClientBase(super.getApi()) {
@Override public String getTokenHeader() { return "X-MyApp-Session"; }
};
}
```

If session management requires more complex processing, override the `beforeSend` method in your client class for full control:
```code
...
public class FooTest extends AbstractResourceIT {

@Override public ApiClientBase getApi() {
return new ApiClientBase(super.getApi()) {
@Override protected HttpRequestBase beforeSend(HttpRequestBase request) {
// ... adjust request as needed / add authentication
return request;
}
};
}
```

### Set session by name in an ApiScriptRequest
An ApiScriptRequest can set the session for a request. When the session is set, it will be used on subesquent requests,
unless they specify a different session, or a new session.

For example, let's say the current session was created at login and named `userSession`. Then our test script created
a second user and logged in as them, and saved it with the session name `secondUser`. The ApiRunner's current session
is thus `secondUser`, but later in the test we want to make an API call as the first user.

A GET using a named session can switch between sessions:
{
"request" {
"uri": "/account/something",
"session": "userSession"
}
}
}

Setting the `response.session` property to `userSession` means that ApiRunner will make the GET request using the
authentication credentials for that session, instead of the `secondUser` session.

## ApiScriptResponse
Whereas an ApiScriptRequest represents instructions to ApiRunner to do actively very specific things to an API server,
ApiScriptResponse reads a response and applies various tests and checks to verify that everything is as expected before continuing.

Things that will cause ApiRunner to fail an ApiScript:

* By default, only HTTP status 200 is considered successful.
* If `response.status` (integer) was set, and the HTTP status received does not match its value
* If `response.okStatuses` (array of integers) was set, and the HTTP status received does not match any of these values
* If any of the tests in the `check` return `false` or throw an exception

### The `check` tests
The `response.check` property, if present, is an array of tests. Each test is a JavaScript expression. The variables
available within the JavaScript context are:

* All objects that have been saved via the `response.store` property
* All variables in the Map returned from getConfiguration().getServerEnvironment() in the JUnit test class (a subclass of `AbstractResourceIT`)
* A special variable called `configuration` references the object returned by getConfiguration()
* A special variable called `json` references the object returned by the API for the current ApiScript.

### Handlebars + JavaScript
ApiRunner has a dual-context system: Handlebars *and* JavaScript are used to give tests the flexibility and expressive power
that serious REST API testing demands.

Handlebars is applied to all properties in the ApiScript before it is run.
For example `request.url` will often contain variables from previous ApiScripts, and you can also use Handlebars within
the JavaScript `check` conditions.

The `check` conditions are JavaScript expressions, which are evaluated as booleans.
Any test that returns false or throws an exception will cause the JUnit test to fail.

Note that the `json` variable is not available in Handlebars contexts used in the `request` section, since it references the response object.

Let's say we previously fetched the current user via `{"request":{"uri":"/me"}, "response":{"store":"someUser"}}`,
so the current user is saved in the `someUser` variable. Then we fetch the user via the `/users/` API, using the id,
and verify the username is the same.

{
"request": { "uri": "/users/{{someUser.uuid}}/" },
"response": {
"store": "checkUser",
"check": [ {"condition": "checkUser.getName() === someUser.getName()"} ]
}
}, // ...more script items...


Because we can also use Handlebars within JavaScript, an equivalent of above the would be:

{
"request": { "uri": "/users/{{someUser.id}}/" },
"response": {
"store": "checkUser",
"check": [
{"condition": "'{{checkUser.name}}' === '{{someUser.name}}'"}
]
}
}, // ...more script items...

The Handlebars is evaluated first, so the JavaScript test is now a comparison of two string literals.

Handlebars object notation is less verbose than Java/JavaScript, especially for heavily nested objects.
Using Handlebars expressions within the JavaScript tests can often aid in test clarity.


+ 6
- 6
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/SimpleApiRunnerListener.java Ver fichero

@@ -24,6 +24,7 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.system.Sleep.sleep;
import static org.cobbzilla.util.time.TimeUtil.parseDuration;
import static org.cobbzilla.wizard.client.script.ApiRunner.standardHandlebars;
import static org.cobbzilla.wizard.main.ScriptMainBase.SLEEP;
import static org.cobbzilla.wizard.main.ScriptMainBase.handleSleep;

@@ -44,7 +45,7 @@ public class SimpleApiRunnerListener extends ApiRunnerListenerBase {
private ApiClientBase currentApi() { return getApiRunner().getCurrentApi(); }

@Getter(lazy=true) private final Handlebars handlebars = initHandlebars();
protected Handlebars initHandlebars() { return new Handlebars(new HandlebarsUtil(getClass().getSimpleName())); }
protected Handlebars initHandlebars() { return standardHandlebars(new Handlebars(new HandlebarsUtil(getClass().getSimpleName()))); }

@Override public void beforeScript(String before, Map<String, Object> ctx) throws Exception {
if (before == null) return;
@@ -88,9 +89,9 @@ public class SimpleApiRunnerListener extends ApiRunnerListenerBase {
// todo: allow listeners to have access to the runner, or at least the current/correct API
// we are getting 404 because we're sending the wrong token (default API instead of remote)
private boolean handleAwaitUrl(String arg, Map<String, Object> ctx) {
final String[] parts = arg.split("\\s+");
final String[] parts = HandlebarsUtil.apply(getHandlebars(), arg, ctx).split("\\s+");
if (parts.length < 3) return die(AWAIT_URL+": no URL and/or timeout specified");
final String url = formatUrl(parts[1], ctx);
final String url = formatUrl(parts[1]);
final long timeout = parseDuration(parts[2]);
final long checkInterval = (parts.length >= 4) ? parseDuration(parts[3]) : DEFAULT_AWAIT_URL_CHECK_INTERVAL;
final String jsCondition = (parts.length >= 5) ? parseJs(parts, 4) : "true";
@@ -122,7 +123,7 @@ public class SimpleApiRunnerListener extends ApiRunnerListenerBase {
private boolean handleVerifyUnreachable(String arg, Map<String, Object> ctx) {
final String[] parts = arg.split("\\s+");
if (parts.length < 2) return die(VERIFY_UNREACHABLE+": no URL specified");
final String url = formatUrl(parts[1], ctx);
final String url = formatUrl(parts[1]);
final long connectTimeout = (parts.length >= 3) ? parseDuration(parts[2]) : DEFAULT_VERIFY_UNAVAILABLE_TIMEOUT;
final long socketTimeout = (parts.length >= 4) ? parseDuration(parts[3]) : connectTimeout;

@@ -153,9 +154,8 @@ public class SimpleApiRunnerListener extends ApiRunnerListenerBase {
}
}

private String formatUrl(String url, Map<String, Object> ctx) {
private String formatUrl(String url) {
final ApiClientBase currentApi = currentApi();
url = HandlebarsUtil.apply(getHandlebars(), url, ctx);
if (!url.startsWith("http://") && !url.startsWith("https://")) {
if (!url.startsWith("/") && !currentApi.getBaseUri().endsWith("/")) url = "/" + url;
url = currentApi.getBaseUri() + url;


+ 1
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/main/ScriptMainBase.java Ver fichero

@@ -272,6 +272,7 @@ public abstract class ScriptMainBase<OPT extends ScriptMainOptionsBase>
api.setCaptureHeaders(getOptions().isCaptureHeaders());
return new ApiRunner(api, getScriptListener()).setIncludeHandler(this); }


@Override public String include(String path) {
final OPT options = getOptions();
final String envInclude = getPathEnvVar() == null ? null : System.getenv(getPathEnvVar());


+ 10
- 0
wizard-server-test/src/main/java/org/cobbzilla/wizardtest/resources/AbstractResourceIT.java Ver fichero

@@ -6,6 +6,8 @@ import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.http.HttpStatusCodes;
import org.cobbzilla.util.json.JsonUtil;
import org.cobbzilla.wizard.client.ApiClientBase;
import org.cobbzilla.wizard.client.script.ApiRunner;
import org.cobbzilla.wizard.client.script.ApiRunnerListenerBase;
import org.cobbzilla.wizard.server.RestServer;
import org.cobbzilla.wizard.server.RestServerConfigurationFilter;
import org.cobbzilla.wizard.server.RestServerHarness;
@@ -29,6 +31,7 @@ import java.util.concurrent.atomic.AtomicReference;

import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.io.StreamUtil.loadResourceAsStringOrDie;
import static org.cobbzilla.util.io.StreamUtil.stream2string;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.reflect.ReflectionUtil.getFirstTypeParam;
@@ -187,6 +190,13 @@ public abstract class AbstractResourceIT<C extends PgRestServerConfiguration, S
protected boolean createSqlIndexes () { return false; }
protected String[] getSqlPostScripts() { return getConfiguration().getSqlConstraints(createSqlIndexes()); }

// default resolution
protected String resolveInclude(String path) { return loadResourceAsStringOrDie(path+".json"); }

public void runScript(String script) throws Exception {
new ApiRunner(getApi(), new ApiRunnerListenerBase(getClass().getName())).run(script);
}

@Override public void beforeStop(RestServer<C> server) {}
@Override public void onStop(RestServer<C> server) {}



+ 14
- 22
wizard-server-test/src/main/java/org/cobbzilla/wizardtest/resources/ApiModelTestBase.java Ver fichero

@@ -5,9 +5,10 @@ import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.collection.SingletonList;
import org.cobbzilla.util.io.FileUtil;
import org.cobbzilla.util.jdbc.UncheckedSqlException;
import org.cobbzilla.util.string.StringUtil;
import org.cobbzilla.util.system.Sleep;
import org.cobbzilla.wizard.client.ApiClientBase;
import org.cobbzilla.wizard.client.script.ApiRunner;
import org.cobbzilla.wizard.client.script.ApiRunnerListenerBase;
import org.cobbzilla.wizard.client.script.ApiRunnerMultiListener;
import org.cobbzilla.wizard.client.script.ApiScriptIncludeHandler;
import org.cobbzilla.wizard.model.entityconfig.ModelSetupListener;
@@ -21,7 +22,6 @@ import org.junit.Before;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
@@ -33,10 +33,7 @@ import java.util.concurrent.atomic.AtomicReference;
import static java.lang.System.identityHashCode;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.io.FileUtil.temp;
import static org.cobbzilla.util.io.FileUtil.writeStringOrDie;
import static org.cobbzilla.util.io.StreamUtil.stream2string;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate;
import static org.cobbzilla.util.system.CommandShell.execScript;
import static org.cobbzilla.wizard.model.entityconfig.ModelSetup.modelHash;
@@ -47,9 +44,16 @@ public abstract class ApiModelTestBase<C extends PgRestServerConfiguration, S ex
extends ApiDocsResourceIT<C, S>
implements ApiScriptIncludeHandler {

protected abstract String getModelPrefix();
protected abstract String getEntityConfigsEndpoint();
protected abstract ApiRunner getApiRunner();
protected String getModelPrefix() { return "models/"; }
protected String getEntityConfigsEndpoint() { return "/ec"; }

protected String getBaseUri() { return getConfiguration().getApiUriBase(); }

@Getter private final AtomicReference<ApiRunner> _defaultRunner = new AtomicReference<>();

protected ApiRunner getApiRunner() {
return new ApiRunner(new ApiClientBase(getBaseUri()), new ApiRunnerListenerBase(getBaseUri()));
}

@Override public boolean useTestSpecificDatabase () { return true; }

@@ -275,19 +279,7 @@ public abstract class ApiModelTestBase<C extends PgRestServerConfiguration, S ex
apiRunner.run(include(name));
}

@Override public String include(String path) {
final String fileName = path + ".json";
for (String inc : getIncludePaths()) {
try {
return stream2string(Paths.get(inc, fileName).toString());
} catch (Exception e) {
log.debug("include(" + path + "): not found in " + inc);
}
}
return die("include("+path+"): not found anywhere in "+StringUtil.toString(getIncludePaths()));
}

protected List<String> getIncludePaths() {
@Override public List<String> getIncludePaths() {
return new SingletonList<>(getModelPrefix() + (getModelPrefix().endsWith("/") ? "" : File.separator) + "tests");
}



+ 1
- 1
wizard-server/src/main/java/org/cobbzilla/wizard/server/RestServerConfigurationFilter.java Ver fichero

@@ -4,6 +4,6 @@ import org.cobbzilla.wizard.server.config.RestServerConfiguration;

public interface RestServerConfigurationFilter<C extends RestServerConfiguration> {

C filterConfiguration(C configuration);
default C filterConfiguration(C configuration) { return configuration; }

}

Cargando…
Cancelar
Guardar