-
Notifications
You must be signed in to change notification settings - Fork 58
App Developers: Cytoscape Function Best Practices
When developing apps that enable automation, observing best practices in coding and design can greatly improve the accessibility of that automation and its interaction with other apps and services, and aid in tracing any errors during testing and at runtime.
This page documents best practices for app developers who are enabling automation features as Cytoscape Functions. For Cytoscape Commands, see Cytoscape Command Best Practices.
A resource path identifies the REST endpoint associated with an API.
To call your endpoint, the caller appends it to the CyREST base URL and uses the result in an actual REST call as shown below:
Resource path: /cyndex2/v1/networks/{suid}
CyREST base URL: http://localhost:1234
Actual REST call: http://localhost:1234/cyndex2/v1/networks/{suid}
Additionally, if the resource path conforms to the standard REST document model, it can contain object references (e.g., {suid}) that resolve to actual Cytoscape objects.
You should use the JAX-RS @Path
annotation to specify the resource path(s) served by your App.
In the simplest case, a resource path identifies a single function in your app, and the resource path becomes synonymous with a single operation. However, you can arrange for your app to provide different functions for the same resource path, corresponding to different REST commands (e.g., GET, PUT, DELETE). For example, GET could return the resource, PUT could update the resource, and DELETE could remove the resource.
You should use the JAX-RS @GET
, @PUT
, and similar annotations to establish the app's Java function that responds for each HTTP command.
You can understand more about resources via a REST resource book or by inspecting the existing Cytoscape Swagger document accessible via Cytoscape's Help --> Automation --> CyREST API menu item.
The paths you choose for your resources should be unique in the CyREST API; a path to any of your resources should not map to any existing path in CyREST, including the paths of other apps.
Violating this requirement could result in resources not being registered, or throwing runtime errors.
You should make your app's namespace unique by using the app name as the root of all paths served by the app. For example, the CyREST Best Practices Sample App uses the namespace cyrestbestpractices as the root of all its paths).
Since you may decide to evolve your app's REST APIs in future implementations, it is good practice to ensure that future versions of your REST resources can co-exist with the resources you've implemented in the present day.
To ensure this, you should include a version number in your resource (e.g., v1). This way, you can add new resources under a new version (e.g., v2) and still use the same path elements without breaking existing API contracts -- a resource v2/classroom/teacher can change its functionality without impacting v1/classroom/teacher clients.
This version number can be treated as a Major Version according to the Semantic Versioning standard: the functionality of and data models used by this version of a resource must be compatible in subsequent releases. If a change is made that is incompatible (for example, a new parameter is required in the resource request), the version number of the resource should incremented.
Your function should return results or errors in the standardized CIResponse and CI Error data models described below. These models are shared between Cytoscape Cyber-infrastructure services, such as Diffusion and CyNDEx, and allow the responses from those services to be uniformly handled by those calling them. There are a number of ways to return these models, documented in detail in the Building CI Responses wiki page.
In this section, we describe the model and its various parts.
CIResponse
is a standardized data model that is used in multiple Cytoscape Cyberinfrastructure services to return the results of an operation execution.
The model follows the JSON format below:
{
"data": ...,
"errors": [
{
"status": 0,
"type": "string",
"message": "string",
"link": "string"
}
]
}
For reference, the equivalent code for a Java class that defines CIResponse
is shown in the snippet below:
public class CIResponse<T> {
public T data;
public List<CIError> errors;
}
public class CIError {
public Integer status;
public String type;
public String message;
public URI link;
}
If an app operation succeeds, it should return a value as valid JSON in the data
field. If it returns no value, the data
field should be empty. Either way, the errors
array should be empty.
Developers should also be aware that there are a number of data models provided by Cytoscape itself; if you are exporting Cytoscape data such as CyNetwork or CyNode, it is best practice to use one of the JSON models described in the Standard Cytoscape JSON Models wiki page.
If an app operation fails, it should return one or more entry in the errors
array, and the contents of the data
field should be treated as invalid if it is not empty. Even if invalid, non-empty data
should always be parsable JSON.
If the function produces errors
entries, each entry must satisfy the following JSON structure and requirements:
{
"status": 500,
"type": “urn:cytoscape:ci:(service):(service version):(resource):errors:(error #)”,
"message": "The thing with the thing broke",
"link": "/logresource",
}
-
status
is an HTTP status code describing the overall error cause. -
type
is a unique, machine-readable URN, structured to indicate the service of the originating error as well as the resource name and a resource specific code. For example, the Cytoscape Diffusion app returns ‘urn:cytoscape:ci:diffusion-app:v1:diffuse:errors:1’ in this field if a network isn’t found while calling a resource the developer has labelled ‘diffuse’. Note that error# can be any alphanumeric value, andtype
values should be documented as part of the Function interface. -
message
is a short human readable message explaining the condition described intype
. -
link
is a link to a log resource, possibly with filters, that can offer more insight into the nature of the error.
Callers that interpret an errors
entry should focus on the type
field instead of the message
field -- wording of the message
field may vary, while the content of the type
field is intended for comparison leading to branching.
If an error occurs in an operation, it should return the CIResponse
model containing an array of error (e.g., CIError
) elements, with the first element of the array describing the error condition.
If the operation failed because of an error encountered in called services or libraries (e.g., Diffusion), the array should also contain the error element returned by that service or library. If there is a chain of failures (e.g., the operation called a service, which called another service), the array should contain contain an error element for each failure in the chain, with the original failure being the last in the list. This practice makes error tracing in complex apps easier.
For example, an app processing a request may call a service and receive a response that looks like the following:
{
"data" : ...,
"errors":[
{
"status":"418",
"type":"urn:cytoscape:ci:foo_service:v1:bar_resource:1",
"message":"Error decoding token from stream, EOF",
"link":"http://server.io/foo_service/log"
}
]
}
At this point, if the request to the app cannot succeed, the app should produce an errors
array containing its own response plus errors from called services. This response would look similar to the following -- with the app's error readable as errors[0]
:
{
"data" : ...,
"errors":[
{
"status": 500,
"type": "urn:cytoscape:ci:foo-app:v1:bar:15",
"message": "Request to service failed",
"link": "file:/Users/hodor/CytoscapeConfiguration/3/framework-cytoscape.log"
},
{
"status":"418",
"type":"urn:cytoscape:ci:foo-service:v1:bar-resource:1",
"message":"Error decoding token from stream, EOF",
"link":"http://server.io/foo_service/log"
}
]
}
All app automation operations should return meaningful, human interpretable errors when they fail. Returning a stack trace, or worse, returning a generic 500 error with no information at all, should be avoided at all costs. The CIResponse model was designed to coherently report errors and lead to an understanding of root causes, and the use of that model is strongly encouraged.
CyREST recognizes Swagger annotations, and uses them to create rich and interactive documentation. Using Swagger to annotate your Cytoscape Functions is a highly encouraged best practice and is covered in detail in the Swagger Best Practices wiki page.
After implementing one or several CyREST functions, it is advised to use the checklist below to evaluate if your function follows best practices in output and documentation.
Links in checklist headings will direct you to any relevant portions of the Best Practices document.
- Does every path to a function in your app use a namespace that references your app?
- Is every path to a function in your app unique?
- Does every path to a function in your app use correct versioning?
- Do your apps functions use data models that can be expressed as POJOs for input and output?
- Are all your output functions wrapped in a CI Response?
- Do your app functions report errors using CI Errors?
- Are your app functions documented in accordance with Swagger Best Practices?