An API console for RAML (Restful Api Modeling Language) documents. The RAML Console allows browsing of API documentation and in-browser testing of API methods.
The API console is a single HTML element build on top of the Web Components specifications and powered by the Polymer library. Knowledge about how polymer works won't be necessary for using the console.
The element can be used to display documentation for the API defined in RAML. Basic usage of the element is as simple as using any other HTML element:
<api-console></api-console>
See full usage documentaiton below.
- Clone the element:
git clone https://github.com/mulesoft/api-console.git
cd api-console
- Checkout develop version
git checkout develop
- Install polymer-cli and Bower
sudo npm install -g bower polymer-cli
- Install dependencies
bower install
- Serve the element
polymer serve --open -p 8080
Default page is the element documentation. Switch to demo to see working example.
You can also append the /demo/
to the URL to switch to demo page permanently.
Install the console as a dependency of your project. We use bower for this.
bower install --save mulesoft/api-console#4.0.0
Bower will also install dependencies of the console.
For the element to be recognized by the browser as a new HTML element you have to include it in the page source.
<link rel="import" href="bower_components/api-console/api-console.html">
<body>
<api-console raml="{...}"></api-console>
</body>
How to pass RAML data to the element is described below in the Passing the RAML data section.
A full list of available configurations for the api-console
element can be found inside section Element configuration (attributes).
This step is not required if you targeting modern browsers only!
Web components are based on four new specifications (Custom elements, shadow DOM, HTML imports and HTML template) that are not fully supported in legacy browsers (like IE). Also, browser vendors still discussing the HTML imports specification so it's not implemented in Edge and Firefox yet.
If you planning to target these browsers you must include a polyfill for Web Components. The polyfill library is already included into your project (giving you have installed the element using bower
).
Example use of the polyfill library:
<head>
...
<script>
(function() {
'use strict';
var onload = function() {
// For native Imports, manually fire WebComponentsReady so user code
// can use the same code path for native and polyfill'd imports.
if (!window.HTMLImports) {
document.dispatchEvent(
new CustomEvent('WebComponentsReady', {bubbles: true})
);
}
};
var webComponentsSupported = (
'registerElement' in document &&
'import' in document.createElement('link') &&
'content' in document.createElement('template')
);
if (!webComponentsSupported) {
var script = document.createElement('script');
script.async = true;
script.src = '/bower_components/webcomponentsjs/webcomponents-lite.min.js';
script.onload = onload;
document.head.appendChild(script);
} else {
onload();
}
})();
</script>
</head>
...
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
<script>
window.Polymer = {
dom: 'shadow' // this is optional
};
// Load webcomponentsjs polyfill if browser does not support native Web Components
(function() {
'use strict';
var onload = function() {
// For native Imports, manually fire WebComponentsReady so user code
// can use the same code path for native and polyfill'd imports.
if (!window.HTMLImports) {
document.dispatchEvent(
new CustomEvent('WebComponentsReady', {bubbles: true})
);
}
};
var webComponentsSupported = (
'registerElement' in document &&
'import' in document.createElement('link') &&
'content' in document.createElement('template')
);
if (!webComponentsSupported) {
var script = document.createElement('script');
script.async = true;
script.src = '/bower_components/webcomponentsjs/webcomponents-lite.min.js';
script.onload = onload;
document.head.appendChild(script);
} else {
onload();
}
})();
</script>
<link rel="import" href="bower_components/api-console/api-console.html">
</head>
<body>
<api-console raml="{...}"></api-console>
</body>
</html>
Web components are asynchronous by nature. It means that elements import, registering them in the DOM and finally initializing the element is made asynchronously. Therefore you can't expect the element to work right after loading the page (as regular HTML elements does). Consider following example:
<raml-js-parser json></raml-js-parser>
<script>
var parser = document.querySelector('raml-js-parser');
parser.loadApi(apiFileUrl);
</script>
Running this code on page load will throw a TypeError
with the message: parser.loadApi is not a function
.
It's because at the time of execution of the script block the browser knows nothing about the raml-js-parser
element. At the time it is an instance of HTMLUnknownElement
.
The browser hast to execute the import first and then the Polymer library has to register the HTML element called raml-js-parser
.
To run the code properly you have to listen for the WebComponentsReady
event. It's fired when the elements are ready to use.
<raml-js-parser json></raml-js-parser>
<script>
function init() {
var parser = document.querySelector('raml-js-parser');
parser.loadApi(apiFileUrl);
};
window.addEventListener('WebComponentsReady', init);
</script>
The API console web component requires JavaScript object produced by the raml-js-parser and raml-js-enhancer elements. Parsing and enhancing RAML is not part of the api-console
element and must be performed separately as described below.
Use the raml-js-parser
element to parse YAML data or to download RAML from remote location. Note: You may also use our raml-js-parser-2 node library as it gives the same output.
Then you must use the raml-js-enhancer
element to produce data output that is recognizable by the api-console
. The enhancer creates a common data structure and expands RAML types (flattens it's structure so a type doesn't have complex inheritance structure). Elements used to build the API Console expects the JSON object to contain complete data about a method / endpoint / type / security scheme and so on. They will not look into nor have access to the data in the root of RAML definition. The enhancer replaces objects into arrays (adding a key
property to each item) so it can be used in a templating systems. Also example
property of the RAML is always translated to examples
array. Finally the enhancer creates fullUrl
property on each HTTP method so the console don't need to compute it each time you open the documentation page.
<raml-js-parser json></raml-js-parser>
<raml-json-enhance></raml-json-enhance>
<script>
var parser = document.querySelector('raml-js-parser');
parser.addEventListener('api-parse-ready', function(e) {
var enhacer = document.querySelector('raml-json-enhance');
enhacer.json = e.detail.json.specification;
});
window.addEventListener('raml-json-enhance-ready', function(e) {
// The e.detail.json contains the final JavaScript object
console.log(e.detail.json);
});
function init() {
parser.loadApi(apiFileUrl);
};
window.addEventListener('WebComponentsReady', init);
</script>
The parsing and enhancing costs a lot depending on RAML structure and number of files included. Therefore it is a good idea to do it once and cache the results. Then, when the user visit the page again restore cached JSON object and pass it as the api-console
parameter (see below).
The basic method to tell the API console what to display is to use the raml
attribute. It accepts the JavaScript object produced by the parser and the enhancer described above.
<api-console></api-console>
<script>
function init() {
var apiConsole = document.querySelector('api-console');
var json = getRamlJsObject();
apiConsole.json = json;
};
window.addEventListener('WebComponentsReady', init);
</script>
The <api-console>
element also have a convenient json-file
attribute that you can set to point to a file containing the parsed and enhanced JSON data.
<api-console json-file="/path/to/api.json"></api-console>
The API console uses the raml-aware element internally. It can be used to pass the RAML data to the console if direct access to the element is not possible. This way the RAML data can be set for the element even if the elements don't have direct access to each others (e.g. in shadow DOM).
See the raml-aware documentation page for more details.
<raml-aware scope="main-raml"></raml-aware>
<api-console aware="main-raml"></api-console>
window.addEventListener('raml-json-enhance-ready', function(e) {
var aware = document.querySelector('raml-aware');
aware.raml = e.detail.json;
});
parser.loadApi(urlToApi);
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
<title>My API documentation</title>
<script>
window.Polymer = {
dom: 'shadow'
};
// Load webcomponentsjs polyfill if browser does not support native Web Components
(function() {
'use strict';
var onload = function() {
// For native Imports, manually fire WebComponentsReady so user code
// can use the same code path for native and polyfill'd imports.
if (!window.HTMLImports) {
document.dispatchEvent(
new CustomEvent('WebComponentsReady', {bubbles: true})
);
}
};
var webComponentsSupported = (
'registerElement' in document &&
'import' in document.createElement('link') &&
'content' in document.createElement('template')
);
if (!webComponentsSupported) {
var script = document.createElement('script');
script.async = true;
script.src = '/bower_components/webcomponentsjs/webcomponents-lite.min.js';
script.onload = onload;
document.head.appendChild(script);
} else {
onload();
}
})();
</script>
<link rel="import" href="bower_components/api-console/api-console.html">
<!-- Below polyfills are in the api console dependencies. -->
<link rel="import" href="bower_components/fetch-polyfill/fetch-polyfill.html">
<link rel="import" href="bower_components/promise-polyfill/promise-polyfill.html">
</head>
<body>
<api-console></api-console>
<script>
function fetchApiData() {
// api.json contains cached results of parsing the RAML spec.
return fetch('./api.json')
.then(function(response) {
if (response.ok) {
return response.json();
}
});
}
function notifyInitError(message) {
alert('No API for you this time. ' + message);
}
function init() {
fetchApiData()
.then(function(json) {
if (json) {
var apiConsole = document.querySelector('api-console');
apiConsole.json = json;
} else {
notifyInitError('Data not available.');
}
})
.catch(function(cause) {
notifyInitError(cause.message);
})
};
window.addEventListener('WebComponentsReady', init);
</script>
</body>
</html>
Attribute | Description | Type |
---|---|---|
raml |
The JavaScript object or equivalent JSON object representing the RAML structure as a JavaScript object. | Object or String |
json-file |
Path to a file with JSON data that should be read and contents of it should be set to the raml attribute |
String |
path |
Currently selected path in the view. See section Controlling the view for more information. | String |
aware |
If passing data by using the raml-aware element, it is the name as the scope attribute used in the aware. |
String |
page |
Currently selected top level view of the console. It can be either docs or request . The later is the "try it screen". |
String |
narrow |
By setting this attribute it will tell the API console to render a "narrow" view. This is a mobile like view (navigation is hidden in a drawer, some views are simplified for narrow screens) that will be presented event if the screen size is wide. This is helpful when inserting the element as a sidebar of your web page. Node that the narrow property will be set automatically on mobile devices |
Boolean |
The main stylesheet for the element is the api-console-styles.html
file that resists in this repo.
The stylesheet contains CSS variables and mixins definitions that are used by the inner elements.
Styles documentation for any element used in the console can be find in it's documentation page in the
elements catalog.
Theming is currently not supported.
The <api-console>
element includes the UI for the user and can be controlled from within the
element. However it exposes few properties that can be used to control element's behavior programmatically.
For example path
property can be used to control to navigate through the RAML structure.
So, to display a request form for a particular endpoint of the API you can set a path
property to:
<api-console path="resources.0.method.1"></api-console>
Example above will display second method from first resource in the resources tree.
You can set attribute display
to request
to display a request panel for this method. By default
it is set to docs
.
There's no easy way to deal with CORS. In the API Console ecosystem there is an extension for Chrome
(and soon for Firefox) which will proxy the request without CORS limitations. The user, when using
selected browsers) will see the install extension banner in the request editor. After installing the
extension all traffic from the console will be redirected to the extension to get the endpoint
response.
The console listens for the api-console-extension-installed
event that is fired from the
extension's content script. Once received the console will send an event to the extension
when the user make the HTTP request. The element responsible for the communication with the extension
is api-console-ext-comm.
Other ways to deal with CORS are coming. File an issue report in the repo if you can help with this issue.
When a user runs the request from the "try it" screen the API Console will send the api-console-request
custom event. If your application can handle transport for the HTTP request (by providing proxy or other solution) you should listen for this event and cancel it by calling event.preventDefault()
.
If the event was cancelled then the API Console will listen for the api-console-response
custom
event that should contain response details. Otherwise the console will use build in fallback function to get the resource using Fetch API / XHR.
Event's detail
object will contain following properties
Property | Type | Description |
---|---|---|
url |
String | The request URL |
method |
String | The HTTP method |
headers |
String | HTTP headers string to send with the message |
payload |
String | Body to send |
This event must be fired when the hosting app finish the request. It must contain generated Request and Response object as defined in the Fetch specification. The API console has polyfill for the Fetch API included.
Property | Type | Description |
---|---|---|
request |
Object | The request object as defined in the Fetch API spec. |
response |
Object | The response object as defined in the Fetch API spec. |
isXhr |
Boolean | Default to true . Indicated if the transport method doesn't support advanced timings and redirects information. See request-panel documentation for detailed description. |
error |
Error | When the request / response is errored (request.ok equals false ) then the error object should be set with the human readable message that will be displayed to the user. |
// Start time of executing the request
var startTime;
// Initial request data passed by the event.
var requestData;
/**
* Creates a Headers object based on the HTTP headers string.
*
* @param {String} headers HTTP headers.
* @return {Headers} Parsed headers object.
*/
createHeaders: function(headers) {
if (!headers) {
return new Headers();
}
var result = new Headers();
var list = headers.split('\n').map(function(line) {
var _parts = line.split(':');
var _name = _parts[0];
var _value = _parts[1];
_name = _name ? _name.trim() : null;
_value = _value ? _value.trim() : null;
if (!_name || !_value) {
return null;
}
return {
name: _name,
value: _value
};
}).filter(function(item) {
return !!item;
});
list.forEach(function(item) {
result.append(item.name, item.value);
});
return result;
}
/**
* Creates a request object from the event's request data.
*
* @param {Object} data Latest request data as in the `api-console-request` object event.
* @return {Request} The Request object.
*/
function createRequest(data) {
var init = {
method: data.method,
mode: 'cors'
};
if (data.headers) {
init.headers = createHeaders(data.headers);
}
if (['GET', 'HEAD'].indexOf(data.method) !== -1) {
data.payload = undefined;
} else {
if (data.payload) {
init.body = data.payload;
}
}
return new Request(data.url, init);
}
/**
* Creates a response object from the response data.
* If the response is invalid then returned Response object will be errored.
*
* @param {XMLHttpRequest} xhr The XHR object used to make a connection.
* @return {Response} The response object.
*/
function createResponse(xhr) {
var status = xhr.status;
if (!status || status < 200) {
return Response.error();
}
var init = {
status: status,
statusText: xhr.statusText
};
var headers = xhr.getAllResponseHeaders();
if (headers) {
init.headers = createHeaders(headers);
}
try {
return new Response(xhr.responseText, init);
} catch (e) {
return Response.error();
}
}
// General error handler.
function errorHandler(e) {
var loadTime = performance.now() - startTime;
var request = createRequest(requestData);
var detail = {
request: request,
response: Response.error(),
loadingTime: loadTime,
isXhr: true,
error: new Error('Resource is unavailable')
};
var event = new CustomEvent('api-console-response', {
cancelable: false,
bubbles: true,
composed: true,
detail: detail
});
document.body.dispatchEvent(event);
}
// Handler for load event
function loadHandler(e) {
var loadTime = performance.now() - startTime;
var request = createRequest(requestData);
var response = createResponse(e.target);
var detail = {
request: request,
response: response,
loadingTime: loadTime,
isXhr: true
};
if (!response.ok) {
detail.error = new Error('Resource is unavailable');
}
var event = new CustomEvent('api-console-response', {
cancelable: false,
bubbles: true,
composed: true,
detail: detail
});
document.body.dispatchEvent(event);
}
// Handler to the event, sends the request
function consoleRequestHandler(e) {
requestData = e.detail;
var xhr = new XMLHttpRequest();
xhr.open(requestData.method, requestData.url, true);
if (requestData.headers) {
requestData.headers.split('\n').forEach(function(header) {
var data = header.split(':');
var name = data[0].trim();
var value = '';
if (data[1]) {
value = data[1].trim();
}
try {
xhr.setRequestHeader(name, value);
} catch (e) {
console.log('Can\'t set header ' + name ' in the XHR call.');
}
});
}
xhr.addEventListener('load', loadHandler);
xhr.addEventListener('error', errorHandler);
xhr.addEventListener('timeout', errorHandler);
try {
startTime = performance.now();
xhr.send(requestData.payload);
} catch (e) {
errorHandler(e);
}
}
window.addEventListener('api-console-request', consoleRequestHandler);
The api-console
must either be explicitly sized, or contained by the explicitly
sized parent. Parent container also has to be positioned relatively
(position: relative
CSS property). "Explicitly sized", means it either has
an explicit CSS height property set via a class or inline style, or else is
sized by other layout means (e.g. the flex layout or absolute positioning).