refapp
Version:
Parse Refract JSON to API Reference Jquery SPA
654 lines (597 loc) • 19.4 kB
JavaScript
/**
* @author E-Com Club <ti@e-com.club>
* @license MIT
*/
(function ($) {
'use strict'
// save request samples
var transaction = 0
var Req = [{}]
var Res = [{}]
var nodeValue = function (node) {
if (typeof node === 'object' && node.content !== undefined) {
return node.content
} else {
return node
}
}
var elementMeta = function (element, prop) {
// metadata from API Element object
if (typeof element === 'object' && element !== null) {
var meta = element.meta
if (typeof meta === 'object' && meta !== null) {
// valid meta object
if (!prop) {
return meta
} else if (meta[prop]) {
var node = Array.isArray(meta[prop]) ? meta[prop][0] : meta[prop]
if (node) {
return nodeValue(node)
} else {
return meta[prop]
}
}
}
}
// not found
if (prop) {
return ''
} else {
// empty object
return {}
}
}
var handleHeaders = function (headers, obj) {
// request or response HTTP headers
// parse to array of nested objects
// { key: value }
if (typeof headers === 'object') {
headers = headers.content
if (Array.isArray(headers)) {
// reset
obj.headers = {}
for (var i = 0; i < headers.length; i++) {
var header = headers[i]
try {
obj.headers[header.content.key.content] = header.content.value.content
} catch (e) {
console.error('Malformed HTTP header object', header, e)
}
}
}
}
}
var consume = function (refract, anchor, options, $body, $list, parent, endpoint) {
var i, doIfDeep, className, title
// check refract object
if (typeof refract === 'object' && refract !== null) {
// treat API Element object
// Ref.: https://api-elements.readthedocs.io/en/latest/element-definitions.html
var type = refract.element
var content = refract.content
// API Element attributes
var attr = refract.attributes
// get request and response objects
var req = Req[transaction]
var res = Res[transaction]
if (type !== 'httpResponse') {
// treat attributes first
if (typeof attr === 'object' && attr !== null) {
if (typeof nodeValue(attr.method) === 'string') {
req.method = nodeValue(attr.method)
}
if (typeof nodeValue(attr.href) === 'string') {
// endpoint pattern
req.href = nodeValue(attr.href)
}
if (attr.headers) {
// request HTTP headers
handleHeaders(attr.headers, req)
}
if (attr.hrefVariables) {
// URL params
// parse to array of nested objects
// { key, type, value, description, required }
var params = attr.hrefVariables
if (typeof params === 'object') {
params = params.content
if (Array.isArray(params)) {
// reset
req.params = []
for (i = 0; i < params.length; i++) {
var param = params[i]
try {
var paramObject = {
key: param.content.key.content,
// boolean required
required: !(param.attributes.typeAttributes[0] === 'optional')
}
if (param.content.value) {
paramObject.value = param.content.value.content
} else {
paramObject.value = ''
}
// optional param type and description
if (param.meta) {
paramObject.type = elementMeta(param, 'title') || ''
paramObject.description = elementMeta(param, 'description') || ''
} else {
paramObject.type = paramObject.description = ''
}
// add to request params
req.params.push(paramObject)
} catch (e) {
console.error('Malformed URL param object', param, e)
}
}
}
}
}
}
switch (type) {
case 'copy':
if (typeof content === 'string') {
// Markdown string
// append to parent body element
$body.append('<div class="pb-2">' + options.mdParser(content) + '</div>')
}
break
case 'category':
case 'resource':
className = elementMeta(refract, 'classes')
title = elementMeta(refract, 'title')
var id = anchor + title.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, '-')
var $li
if (title !== '') {
// show category title
var head
switch (className) {
case 'api':
head = 1
break
case 'resourceGroup':
head = 2
break
default:
head = 3
}
// add title to body DOM
$body.append($('<h' + head + '>', {
'class': 'my-3',
html: $('<a>', {
'class': 'anchor-link text-body',
href: '#' + id,
text: title
}),
id: id
}))
if (head <= 2) {
$body.append('<hr>')
}
$li = $('<li>', {
html: $('<a>', {
href: '#' + id,
text: title
})
})
// add to anchors list
$list.append($li)
doIfDeep = function () {
// create new deeper list for subresources
var $ul = $('<ul>')
$li.append($ul)
// new block for category
var $div = $('<div>', {
'class': 'mb-5'
})
$body.append($div)
// change body and list DOM elements
$body = $div
$list = $ul
}
}
if (req.href) {
// repass default resource endpoint
endpoint = req.href
}
break
case 'transition':
// new card block to API request
title = elementMeta(refract, 'title')
var $card = $('<div>', {
'class': 'card-body',
html: '<h5 class="card-title">' + title + '</h5>'
})
$body.append($('<a>', {
href: 'javascript:;',
'class': 'mt-2 card',
html: $card,
// send request and response objects
click: (function (i) {
return function () { options.actionCallback(Req[i], Res[i]) }
}(transaction))
}))
doIfDeep = function () {
// change body DOM element
$body = $card
}
// set request title
req.title = title
break
case 'httpRequest':
if (req.method) {
var color
switch (req.method) {
case 'POST':
color = 'success'
break
case 'PATCH':
color = 'warning'
break
case 'PUT':
color = 'secondary'
break
case 'DELETE':
color = 'danger'
break
case 'GET':
color = 'info'
break
default:
color = 'light'
}
// styling action card
$body.parent().addClass('text-white bg-' + color)
// show request method
.find('h3,h4,h5').append($('<small>', {
'class': 'text-monospace ml-1 float-right',
text: req.method
}))
}
break
case 'asset':
if (typeof content === 'string') {
// body content
var obj
switch (parent) {
case 'httpRequest':
// request body string
obj = req
break
case 'httpResponse':
// response body
obj = res
break
}
if (obj) {
className = elementMeta(refract, 'classes')
if (className === 'messageBodySchema') {
// JSON Schema
obj.schema = content
} else {
obj.body = content
}
}
}
break
case 'parseResult':
if (Array.isArray(content)) {
// fix root API Element
return content[0]
}
}
} else {
// sample response
if (attr.headers) {
// response HTTP headers
handleHeaders(attr.headers, res)
} else {
// no headers
res.headers = []
}
// HTTP status
if (attr.statusCode) {
res.status = parseInt(attr.statusCode, 10)
} else {
res.status = 200
}
// preset next request and response
// persist request URI
Req.push({ href: (endpoint || req.href) })
Res.push({})
}
// check each child element one by one
if (Array.isArray(content) && content.length) {
if (doIfDeep) {
doIfDeep()
}
// create new deeper list for subresources
for (i = 0; i < content.length; i++) {
// recursion
consume(content[i], anchor, options, $body, $list, type, endpoint)
}
}
if (type === 'httpResponse') {
// pass to next req and res object
transaction++
}
}
// all done
return null
}
// set globally
window.consumeRefract = consume
// window.apiElementMeta = elementMeta
}(jQuery))
;/**
* refapp
* @author E-Com Club <ti@e-com.club>
* @license MIT
*/
(function ($) {
'use strict'
// require 'partials/consume-refract.js'
/* global consumeRefract */
// setup as jQuery plugin
$.fn.refapp = function (refracts, Options) {
// main DOM element
var $app = this
// default options object
var options = {
// styles
asideClasses: '',
articleClasses: '',
// base URL hash
baseHash: '/',
// parse Markdown to HTML
mdParser: function (md) { return md },
// optional callback function for loaded refracts
refractCallback: null,
// callback function for endoint actions
actionCallback: function (req, res) { console.log(req, res) }
}
if (Options) {
Object.assign(options, Options)
}
// random base ID for elements
var elId = Math.floor(Math.random() * (9999 - 1000)) + 1000
// create DOM elements
// main app Components
var $article = $('<article>', {
'class': options.articleClasses
})
var $list = $('<div>', {
'class': 'list-group my-3 mr-md-5 pr-lg-3 pr-xl-5 ref-resources'
})
var $resources = []
var $ol = $('<ol>', {
'class': 'ref-anchors'
})
var $aside = $('<aside>', {
'class': options.asideClasses,
html: [
'<h5>Summary</h5>',
$ol,
'<h5>Reference</h5>',
$list
]
})
// console.log(this)
// console.log(refract)
// current resource anchor
var baseHash = options.baseHash
var currentAnchor, waitingHash
$(window).on('hashchange', function () {
if (currentAnchor && !(new RegExp('^#' + currentAnchor).test(location.hash))) {
// resource changed
// try to route
route()
}
})
var route = function () {
var hash = location.hash
if (hash) {
if (hash.slice(baseHash.length + 1).indexOf('/') === -1) {
// should have at least one bar
window.location.hash = hash + '/'
return route()
}
// test refract fragment route
for (var i = 0; i < $resources.length; i++) {
var $link = $resources[i]
if (new RegExp('^#' + $link.data('anchor')).test(hash)) {
// found
if (!$link.hasClass('active')) {
// save current hash for further update
waitingHash = hash
// start routing
$link.click()
}
return true
}
}
// rewrite Apiary default hashes
var parts = hash.match(/^#(reference|introduction)\/(.*)$/)
if (parts) {
window.location.hash = '#' + baseHash + parts[2]
// route again
return route()
}
}
// not routed
return false
}
// get each refract fragment
if (Array.isArray(refracts)) {
var firstRefract = true
var processRefract = function (Refract, anchor) {
// reset DOM
$ol.slideUp(200, function () {
$(this).html('')
// fade article content
$article.fadeOut(200, function () {
$(this).html('')
// start treating Refract JSON (Drafter output)
// API Elements format
/* Reference
https://github.com/apiaryio/drafter
https://api-elements.readthedocs.io/en/latest/
*/
// consume refract tree
var refract = Object.assign({}, Refract)
while (refract) {
// root API Element fixed
refract = consumeRefract(refract, anchor, options, $article, $ol)
/*
if (!options.apiTitle) {
// try to set API title
options.apiTitle = apiElementMeta(refract, 'title')
}
*/
}
// show content again
$article.fadeIn('slow', function () {
if (firstRefract) {
firstRefract = false
} else {
// scroll to content
$('html, body').animate({ scrollTop: $article.offset().top - 20 }, 'slow')
}
if (typeof options.refractCallback === 'function') {
// send refract object
options.refractCallback(Refract)
}
})
$ol.slideDown('slow', function () {
if (waitingHash) {
if (waitingHash !== location.hash) {
var $link = $(this).find('a[href="' + waitingHash + '"]')
if ($link.length) {
setTimeout(function () {
// need to call native DOM click()
// https://stackoverflow.com/questions/34174134
$link[0].click()
}, 100)
}
}
// reset
waitingHash = null
}
})
// set links to new browser tab
$article.find('a').filter(function () {
var attr = $(this).attr('href')
return (attr.charAt(0) !== '#' && attr !== 'javascript:;')
}).attr('target', '_blank')
})
})
}
var requestFailed = function (jqxhr, textStatus, err) {
// AJAX error
alert('Cannot GET Refract JSON: ' + textStatus)
console.error(err)
}
var getRefract = function (i, anchor) {
// try to GET JSON file
var url = refracts[i].src
if (typeof url === 'string' && url !== '') {
$.getJSON(url, function (data) { processRefract(data, anchor) })
.fail(requestFailed)
} else {
console.error(new Error('Invalid or undefined src string on refract (' + i + '), ignored'))
}
}
// list all fragments
for (var i = 0; i < refracts.length; i++) {
if (typeof refracts[i] === 'object' && refracts[i] !== null) {
var title = refracts[i].title
if (title) {
title = typeof title === 'object' ? title.content : title
if (typeof title === 'string' && title.trim() !== '') {
// generate anchor for this recfract fragment
var anchor = baseHash + title.toLowerCase().replace(/\s/g, '-') + '/'
$resources.push($('<a>', {
'class': 'list-group-item list-group-item-action',
href: 'javascript:;',
text: title,
'data-anchor': anchor,
click: (function (i, anchor) {
// local vars
return function () {
// clear last active
$list.find('a.active').removeClass('active')
$(this).addClass('active')
// update content
getRefract(i, anchor)
// scroll to top
$('html, body').animate({ scrollTop: 0 }, 'slow', 'swing', function () {
// update current anchor
currentAnchor = anchor
// update URL hash
window.location.hash = '#' + anchor
})
}
}(i, anchor))
}))
}
}
// add resources to list DOM
$list.append($resources)
}
}
}
// create collapsable elements for navs
var divId = 'ref-anchors-' + elId
$aside.addClass('collapse d-md-block').attr('id', divId)
var $sidebar = [
$('<a>', {
'class': 'btn btn-xl btn-outline-primary btn-block d-md-none mb-3',
'data-toggle': 'collapse',
'aria-expanded': 'false',
'aria-control': divId,
href: '#' + divId,
role: 'button',
html: '<i class="ti-angle-down mr-1"></i> Content'
}),
$aside
]
// Reference App body HTML
var body = []
// optional API title
if (options.apiTitle) {
body.push($('<h1>', {
'class': 'mt-3 mb-4 text-muted',
text: options.apiTitle
}))
}
body.push($article)
// update DOM
$app.html($('<div>', {
'class': 'container',
// compose Reference App layout
html: $('<div>', {
'class': 'row',
html: [
$('<div>', {
'class': 'col-md-5 col-xl-4 ref-sidebar',
html: [
$('<section>', {
'class': 'py-4 sticky-top',
html: $sidebar
})
]
}),
$('<div>', {
'class': 'col-md-7 col-xl-8 ref-body',
html: body
})
]
})
}))
// first route
if (!route()) {
// start with the first refract fragment
$resources[0].click()
}
}
}(jQuery))