Skip to content
This repository has been archived by the owner on Nov 9, 2020. It is now read-only.

Commit

Permalink
Add Swagger 2.0 API doc/export
Browse files Browse the repository at this point in the history
A user can start a com.vmware.xenon.swagger.SwaggerDescriptorService
on a host and a swagger 2.0 description is served by default on /discovery/swagger.

* add new xenon-swagger module containing a single stateless service
* add enumValues to PropertyDescription describing an ENUM
* add kind to PropertyDescription describing a PODO
* add latest release of Swagger-UI as custom ui of swagger
* documentKind is used to refer to swagger Models
* fix bug in ServiceDocumentDescription.Builder that assigns a kind of
  java:lang:Number to Number-typed fields (like in NumericRange)

TODOs:
* read swagger annotations to provide more details and docs: currently
  not possible withoug peeking into the service classes
* figure out if a service support PATCH/OPTIONS/POST...: not possible
  without peeking into the service class and looking for overriden handler
  methods
* Swagger 2.0 cannot fully describe a service interface, track this:
   OAI/OpenAPI-Specification#182

Change-Id: I9d59dd63187c769994fe8b8ce3143e119dc3cdde
  • Loading branch information
jvassev committed Mar 16, 2016
1 parent 14e5cda commit 0ebecfe
Show file tree
Hide file tree
Showing 61 changed files with 32,938 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* New Operation.createXXXX(ServiceHost h, link) helpers
to eliminate need for UriUtils.buildUri

* Option to serve a Swagger 2.0 API description of a ServiceHost

## 0.7.2

* Simplify service synchronization logic during node group
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<module>xenon-loader</module>
<module>xenon-slf4j</module>
<module>xenon-ui</module>
<module>xenon-swagger</module>
</modules>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ public static ServiceDocumentDescription buildDescription() {
public static final String REPLICATION_PHASE_COMMIT = "commit";

public static final String MEDIA_TYPE_APPLICATION_JSON = "application/json";
public static final String MEDIA_TYPE_TEXT_YAML = "text/x-yaml";
public static final String MEDIA_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream";
public static final String MEDIA_TYPE_APPLICATION_X_WWW_FORM_ENCODED = "application/x-www-form-urlencoded";
public static final String MEDIA_TYPE_TEXT_HTML = "text/html";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
Expand All @@ -30,6 +29,7 @@
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import com.vmware.xenon.common.RequestRouter.Route;
import com.vmware.xenon.common.Service.Action;
Expand Down Expand Up @@ -141,6 +141,10 @@ public enum PropertyIndexingOption {

public static class PropertyDescription {
public ServiceDocumentDescription.TypeName typeName;
/**
* Set only for PODO-typed fields.
*/
public String kind;
public Object exampleValue;
transient Field accessor;

Expand All @@ -158,6 +162,11 @@ public static class PropertyDescription {
*/
public PropertyDescription elementDescription;

/**
* Set only for enums, the set of possible values.
*/
public String[] enumValues;

public PropertyDescription() {
this.indexingOptions = EnumSet.noneOf(PropertyIndexingOption.class);
this.usageOptions = EnumSet.noneOf(PropertyUsageOption.class);
Expand Down Expand Up @@ -223,6 +232,10 @@ public ServiceDocumentDescription buildDescription(
return desc;
}

public PropertyDescription buildPodoPropertyDescription(Class<?> type) {
return buildPodoPropertyDescription(type, new HashSet<>(), 0);
}

public ServiceDocumentDescription buildDescription(
Class<? extends ServiceDocument> type,
EnumSet<Service.ServiceOption> serviceCaps) {
Expand Down Expand Up @@ -288,6 +301,10 @@ protected PropertyDescription buildPodoPropertyDescription(

fd.accessor = f;
pd.fieldDescriptions.put(f.getName(), fd);

if (fd.typeName == TypeName.PODO) {
fd.kind = Utils.buildKind(f.getType());
}
}

visited.remove(typeName);
Expand Down Expand Up @@ -328,17 +345,18 @@ protected void buildPropertyDescription(
} else if (Float.class.equals(clazz) || float.class.equals(clazz)) {
pd.typeName = TypeName.DOUBLE;
pd.exampleValue = 0.0F;
} else if (Number.class.isAssignableFrom(clazz)) {
// coerce all Numbers to double, may lose precision with BigDecimal and BigInteger
pd.typeName = TypeName.DOUBLE;
pd.exampleValue = 0.0F;
} else if (String.class.equals(clazz) || char.class.equals(clazz)) {
pd.typeName = TypeName.STRING;
pd.exampleValue = "example string";
} else if (Date.class.equals(clazz)) {
pd.exampleValue = new Date();
pd.typeName = TypeName.DATE;
} else if (URI.class.equals(clazz)) {
try {
pd.exampleValue = new URI("http://localhost:1234/some/service");
} catch (URISyntaxException ignored) {
}
pd.exampleValue = URI.create("http://localhost:1234/some/service");
pd.typeName = TypeName.URI;
} else {
isSimpleType = false;
Expand Down Expand Up @@ -422,6 +440,11 @@ protected void buildPropertyDescription(
pd.elementDescription = fd;
} else if (Enum.class.isAssignableFrom(clazz)) {
pd.typeName = TypeName.ENUM;
pd.enumValues = Arrays
.stream(clazz.getEnumConstants())
.map( o -> ((Enum)o).name())
.collect(Collectors.toList())
.toArray(new String[0]);
} else if (clazz.isArray()) {
pd.typeName = TypeName.ARRAY;

Expand Down Expand Up @@ -461,6 +484,7 @@ protected void buildPropertyDescription(
pd.elementDescription = fd;
} else {
pd.typeName = TypeName.PODO;
pd.kind = Utils.buildKind(clazz);
if (depth > 0) {
// Force indexing of all nested complex PODO fields. If the service author
// instructed expand at the root level, we will index and expand everything
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public static class Arguments {
* The default value of 10 minutes allows for 1.8M services to synchronize, given an estimate of
* 3,000 service synchronizations per second, on a three node cluster, on a local network.
*
* Synchronization starts automatically if {@link Arguments.isPeerSynchronizationEnabled} is true,
* Synchronization starts automatically if {@link Arguments#isPeerSynchronizationEnabled} is true,
* and the node group has observed a node joining or leaving (becoming unavailable)
*/
public int perFactoryPeerSynchronizationLimitSeconds = (int) TimeUnit.MINUTES.toSeconds(10);
Expand Down Expand Up @@ -307,9 +307,11 @@ public static class Arguments {
public static final String[] RESERVED_SERVICE_URI_PATHS = {
SERVICE_URI_SUFFIX_AVAILABLE,
SERVICE_URI_SUFFIX_REPLICATION,
SERVICE_URI_SUFFIX_STATS, SERVICE_URI_SUFFIX_SUBSCRIPTIONS,
SERVICE_URI_SUFFIX_STATS,
SERVICE_URI_SUFFIX_SUBSCRIPTIONS,
SERVICE_URI_SUFFIX_UI,
SERVICE_URI_SUFFIX_CONFIG, SERVICE_URI_SUFFIX_TEMPLATE };
SERVICE_URI_SUFFIX_CONFIG,
SERVICE_URI_SUFFIX_TEMPLATE };

static final Path DEFAULT_TMPDIR = Paths.get(System.getProperty("java.io.tmpdir"));
static final Path DEFAULT_SANDBOX = DEFAULT_TMPDIR.resolve("xenon");
Expand Down Expand Up @@ -5385,23 +5387,40 @@ public void queryServiceUris(String servicePath, Operation get) {
get.setBodyNoCloning(r).complete();
}

public void queryServiceUris(EnumSet<ServiceOption> options, boolean matchAllOptions,
Operation get) {
queryServiceUris(options, matchAllOptions, get, null);
}

/**
* Queries services in the AVAILABLE stage based on the provided options
* Queries services in the AVAILABLE stage based on the provided options, excluding all
* UTILITY services.
*
* matchAllOptions = true : all options must match
* matchAllOptions = false : any option must match
* @param options options that must match
* @param matchAllOptions true : all options must match, false : any option must match
* @param get
* @param exclusionOptions if not-null, exclude services that have any of the excluded options
*/
public void queryServiceUris(EnumSet<ServiceOption> options, boolean matchAllOptions,
Operation get) {
Operation get, EnumSet<ServiceOption> exclusionOptions) {
ServiceDocumentQueryResult r = new ServiceDocumentQueryResult();

for (Service s : this.attachedServices.values()) {
loop: for (Service s : this.attachedServices.values()) {
if (s.getProcessingStage() != ProcessingStage.AVAILABLE) {
continue;
}
if (s.hasOption(ServiceOption.UTILITY)) {
continue;
}

if (exclusionOptions != null) {
for (ServiceOption exOp : exclusionOptions) {
if (s.hasOption(exOp)) {
continue loop;
}
}
}

String servicePath = s.getSelfLink();

if (matchAllOptions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import com.vmware.xenon.common.Service;

public class ProcessFactoryService extends FactoryService {
public static final String SELF_LINK = ServiceUriPaths.CORE + "/processes";
public static final String SELF_LINK = ServiceUriPaths.CORE_PROCESSES;

public ProcessFactoryService() {
super(ProcessState.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class ServiceUriPaths {
public static final String GO_PROCESS_LOG = CORE_MANAGEMENT + "/go-dcp-process-log";
public static final String SYSTEM_LOG = CORE_MANAGEMENT + "/system-log";

public static final String CORE_PROCESSES = CORE + "/processes";

public static final String COORDINATED_UPDATE_FACTORY = CORE + "/coordinated-updates";
public static final String NODE_GROUP_FACTORY = CORE + "/node-groups";
public static final String DEFAULT_NODE_GROUP_NAME = "default";
Expand Down Expand Up @@ -106,4 +108,9 @@ public class ServiceUriPaths {
public static final String WS_SERVICE_LIB_JS_PATH =
Utils.buildUiResourceUriPrefixPath(WebSocketService.class) + "/ws-service-lib.js";

/**
* Swagger discovery service is started on this URI.
* @see com.vmware.xenon.swagger.SwaggerDescriptorService
*/
public static final String SWAGGER = "/discovery/swagger";
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.file.Path;
Expand All @@ -42,7 +43,6 @@
import java.util.logging.Logger;

import com.google.gson.reflect.TypeToken;

import org.junit.Assert;
import org.junit.Test;

Expand All @@ -51,9 +51,11 @@
import com.vmware.xenon.common.ServiceDocumentDescription.PropertyDescription;
import com.vmware.xenon.common.ServiceDocumentDescription.PropertyIndexingOption;
import com.vmware.xenon.common.ServiceDocumentDescription.PropertyUsageOption;
import com.vmware.xenon.common.ServiceDocumentDescription.TypeName;
import com.vmware.xenon.common.SystemHostInfo.OsFamily;
import com.vmware.xenon.common.test.VerificationHost;
import com.vmware.xenon.services.common.ExampleService.ExampleServiceState;
import com.vmware.xenon.services.common.QueryTask.NumericRange;
import com.vmware.xenon.services.common.QueryValidationTestService.QueryValidationServiceState;
import com.vmware.xenon.services.common.ServiceUriPaths;

Expand Down Expand Up @@ -810,6 +812,11 @@ private static class AnnotatedDoc extends ServiceDocument {
PropertyUsageOption.OPTIONAL})
public String opts;

@PropertyOptions(indexing = PropertyIndexingOption.EXPAND)
public Range nestedPodo;

public RoundingMode someEnum;

}

private static class TestKeyObjectValueHolder {
Expand Down Expand Up @@ -865,6 +872,38 @@ public void testAnnotationOnFields() {
assertEquals(optsDesc.indexingOptions, EnumSet.of(PropertyIndexingOption.SORT, PropertyIndexingOption.EXCLUDE_FROM_SIGNATURE));
}

@Test
public void testNestedPodosAreAssignedKinds() {
ServiceDocumentDescription desc = ServiceDocumentDescription.Builder.create()
.buildDescription(AnnotatedDoc.class);
PropertyDescription nestedPodo = desc.propertyDescriptions.get("nestedPodo");
assertEquals(Utils.buildKind(Range.class), nestedPodo.kind);

// primitives don't have a kind
PropertyDescription opt = desc.propertyDescriptions.get("opt");
assertNull(opt.kind);
}

@Test
public void testEnumValuesArePopulated() {
ServiceDocumentDescription desc = ServiceDocumentDescription.Builder.create()
.buildDescription(AnnotatedDoc.class);
PropertyDescription someEnum = desc.propertyDescriptions.get("someEnum");
PropertyDescription nestedPodo = desc.propertyDescriptions.get("nestedPodo");

assertEquals(RoundingMode.values().length, someEnum.enumValues.length);
assertNull(nestedPodo.enumValues);
}

@Test
public void testNumberFieldsCoercedToDouble() {
PropertyDescription desc = ServiceDocumentDescription.Builder
.create()
.buildPodoPropertyDescription(NumericRange.class);
assertEquals(TypeName.DOUBLE, desc.fieldDescriptions.get("min").typeName);
assertEquals(TypeName.DOUBLE, desc.fieldDescriptions.get("max").typeName);
}

@Test
public void testMergeQueryResultsWithDifferentData() {

Expand Down
5 changes: 5 additions & 0 deletions xenon-samples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
<artifactId>xenon-ui</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>xenon-swagger</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>xenon-common</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@

import java.util.logging.Level;


import io.swagger.models.Contact;
import io.swagger.models.Info;
import io.swagger.models.License;

import com.vmware.xenon.common.ServiceHost;
import com.vmware.xenon.services.common.RootNamespaceService;
import com.vmware.xenon.services.common.ServiceUriPaths;
import com.vmware.xenon.services.samples.SampleFactoryServiceWithCustomUi;
import com.vmware.xenon.services.samples.SamplePreviousEchoFactoryService;
import com.vmware.xenon.services.samples.SampleServiceWithSharedCustomUi;
import com.vmware.xenon.services.samples.SampleSimpleEchoFactoryService;
import com.vmware.xenon.swagger.SwaggerDescriptorService;
import com.vmware.xenon.ui.UiService;

/**
Expand Down Expand Up @@ -67,6 +74,27 @@ public ServiceHost start() throws Throwable {

// Start UI service
super.startService(new UiService());

// Serve Swagger 2.0 compatible API description
SwaggerDescriptorService swagger = new SwaggerDescriptorService();

// exclude some core services
swagger.setExcludedPrefixes(
"/core/transactions",
"/core/node-groups");

// Provide API metainfo
Info apiInfo = new Info();
apiInfo.setVersion("1.0.0");
apiInfo.setTitle("Xenon SampleHost");
apiInfo.setLicense(new License().name("Apache 2.0").url("https://github.com/vmware/xenon/blob/master/LICENSE"));
apiInfo.setContact(new Contact().url("https://github.com/vmware/xenon"));
swagger.setInfo(apiInfo);

// Serve swagger on default uri
SwaggerDescriptorService.startService(this, swagger);
System.out.println("Checkout swaggerUI: " + this.getPublicUri() + ServiceUriPaths.SWAGGER + "/ui");

return this;
}
}
15 changes: 15 additions & 0 deletions xenon-swagger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Building swagger-ui
1. clone https://github.com/swagger-api/swagger-ui and checkout a stable tag
2. follow instructions for building swagger-ui
3. copy all files produced by the build from dist/ to *ui/com/vmware/xenon/swagger/SwaggerDescriptorService*
4. edit the index.html by adding this lines after "$(function () {"

```javascript
var url = window.location.search.match(/url=([^&]+)/);
if (url && url.length > 1) {
url = decodeURIComponent(url[1]);
} else {
var loc = window.location;
url = loc.protocol + "//" + loc.host + "/discovery/swagger";
}
```
Loading

0 comments on commit 0ebecfe

Please sign in to comment.