Skip to content
This repository was archived by the owner on Apr 8, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "deversifi-api-docs",
"version": "0.0.9",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "parcel src/index.html",
Expand All @@ -12,6 +12,8 @@
"dependencies": {
"@babel/polyfill": "^7.8.3",
"highlight-words-core": "^1.2.2",
"lodash": "^4.17.21",
"lodash.mapvalues": "^4.6.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might as well include the entire lodash

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was doing this before noticing that we've done this type of fine grained dep injection so I figured out there might be a reason although I am not sure. Do you think the weight different will be negligible if we import everything ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, especially in our usecase a few more KBs of lodash won't really have a major effect

"lodash.merge": "^4.6.2",
"prismjs": "^1.23.0",
"react": "^16.12.0",
Expand Down
6 changes: 5 additions & 1 deletion src/spec/forEachEndpoint.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export function forEachEndpoint(spec, cb) {
for (const path in spec.paths) {
for (const method in spec.paths[path]) {
const methods = Object.keys(spec.paths[path])
// TODO: Multiple methods per path was flaky
// hence the choice of taking the first vs. all
if (methods.length > 0) {
const method = methods[0]
cb(spec.paths[path][method], path, method);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/spec/getBodyExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function getExampleFromSchema(schema) {
case 'number':
return schema.example;
case 'object':
return mapValues(schema.properties, getExampleFromSchema);
return schema.example || mapValues(schema.properties, getExampleFromSchema);
}
}

Expand Down
85 changes: 66 additions & 19 deletions src/spec/getEndpoints.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,72 @@
import mapValues from 'lodash.mapvalues'
import {forEachEndpoint} from './forEachEndpoint';
import {getBodyExample} from './getBodyExample';
import {makeJsCode, makeWsJsCode} from './makeJsCode';
import {makePythonCode, makeWsPythonCode} from './makePythonCode';
import {makeCppCode, makeWsCppCode} from './mapeCppCode';
import {makeCurlCode} from './makeCurlCode';
import {makeWscatCode} from './makeWscatCode';
import { getParamExample } from './getParamExample';
import { sortBy } from 'lodash';

export function getEndpoints(spec) {
export function getEndpoints(spec, categoriesToExclude) {
const endpoints = [];
forEachEndpoint(spec, (entry, orgPath, method) => {
if(!entry.tags) return;
const categoryName = entry.tags && entry.tags[0];
if(categoriesToExclude.includes(categoryName.toLowerCase())) return;
// workaround for unique ws socket endpoint
const path = orgPath.replace(/{ws-uid-\d+}/g, '');

const responses = getResponses(entry);
const calls = getCalls(spec, entry, path, method);
const parameters = getParameters(entry);
const headers = getHeaders(entry);
const body = getBodyExample(entry);
const responsesDetails = getResponsesDetails(entry);
const protocol = method === 'ws' ? 'wss' : 'https';

const requestDetails = getRequestDetails(entry)

const endpoint = {
method: method.toUpperCase(),
title: entry.title,
title: entry.title || path,
name: entry.operationId,
link: '#' + entry.operationId,
path: `${protocol}://${spec.host}${path}`,
description: entry.summary, // TODO: markdown?
calls,
responses,
parameters,
headers,
body: body ? JSON.stringify(body, null, 4) : undefined,
responsesDetails,
requestDetails
};

if (method === 'ws') {
const wscat = makeWscatCode(spec, entry, path);
endpoint.wscat = wscat;
} else {
const curl = makeCurlCode(spec, entry, path, method);
endpoint.curl = curl;
}

endpoints.push(endpoint);
});
return endpoints;
}

function getParameters(entry) {
if (!entry.parameters) return;
return entry.parameters.filter(parameter => parameter.in !== 'body');
return entry.parameters.filter(parameter => parameter.in !== 'body' && parameter.in !== 'header');
}

function getHeaders(entry) {
if (!entry.parameters) return;
return entry.parameters.filter(parameter => parameter.in === 'header');
}

function getCalls(spec, entry, path, method) {
return [
{
language: 'bash',
name: method === 'ws' ? 'wscat' : 'cURL',
content: method === 'ws'
? makeWscatCode(spec, entry, path)
: makeCurlCode(spec, entry, path, method)
},
{
language: 'js',
name: 'JavaScript',
Expand All @@ -76,17 +87,52 @@ function getCalls(spec, entry, path, method) {
content: method === 'ws'
? makeWsCppCode(spec, entry, path)
: makeCppCode(spec, entry, path, method)
},
}
];
}

function buildExample(schema) {
const schemaWideExample = getParamExample(schema)
if (schemaWideExample !== undefined) {
return schemaWideExample
}
if (schema.properties) {
return mapValues(schema.properties, prop => {
const propExample = getParamExample(prop) || buildExample(prop)
if (propExample === undefined) {
console.warn('Missing example in one of object properties', schema, prop)
throw new Error('Missing example in one of object properties', {schema, prop})
}
return propExample
})
} else if (schema.items) {
return [ buildExample(schema.items) ]
}

}

function getResponses(entry) {
const responses = [];
const hasDefault = Object.keys(entry.responses).includes('default');

if (!entry.responses) {
console.warn('No responses for entry', entry)
}

const hasDefault = entry.responses.default &&
entry.responses.default.schema

for (const key in entry.responses) {
const response = entry.responses[key];
// Overlay
let example = response.examples && response.examples['application/json'];

// Examples directly from Joi
if (!example && response.schema) {
try {
example = buildExample(response.schema)
} catch (e) {}
}

example = example && JSON.stringify(example, null, 2);

if (hasDefault && key === '200') {
Expand All @@ -101,14 +147,15 @@ function getResponses(entry) {
// TODO: details
});
}
return responses;
return sortBy(responses, response => response.code !== 'default');
}

function getResponsesDetails(entry) {
// TODO: currently docs don't support model $ref,
// all default responses are defined manually in overlay

const hasDefault = Object.keys(entry.responses).includes('default');
if (!entry.responses) {
console.warn('No responses for entry', entry)
}
const hasDefault = entry.responses.default &&
entry.responses.default.schema;

if (!hasDefault) {
entry.responses.default = entry.responses['200'];
Expand Down
15 changes: 15 additions & 0 deletions src/spec/getExtraHeaders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@


// Extracts headers to be added in examples and code snippets
// In this case, we want to add authorization headers so example queries

import { getParamExample } from './getParamExample'

export function getExtraHeaders(headerParams = []) {
const extraHeaders = {}
headerParams.forEach(header => {
['authorization'].includes(header.name)
extraHeaders[header.name] = getParamExample(header)
})
return extraHeaders
}
9 changes: 9 additions & 0 deletions src/spec/getHeadersWithExtras.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { getExtraHeaders } from './getExtraHeaders';

export function getHeadersWithExtras(extraHeaderParams) {
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
...getExtraHeaders(extraHeaderParams)
}
}
15 changes: 15 additions & 0 deletions src/spec/getParamExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

export function getParamExample(params = {}) {
if (params.hasOwnProperty('example')) {
return params['example']
}
if (params.hasOwnProperty('x-example')) {
return params['x-example']
}
if (params.hasOwnProperty('default')) {
return params.default
}
if (params.enum && params.enum.length > 0) {
return params.enum[0]
}
}
32 changes: 28 additions & 4 deletions src/spec/getSidebar.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
import {forEachEndpoint} from './forEachEndpoint';

export function getSidebar(spec) {

var categoriesOrder = [
'platform',
'wallet',
'trading',
'amm',
'market',
'governance',
'dlm',
'account',
'public',
'integration'
];

const getCategoryWeight = (category) => {
return categoriesOrder.length - categoriesOrder.indexOf(category.name.toLowerCase())
};

export function getSidebar(spec, categoriesToExclude) {
const categories = new Map();
forEachEndpoint(spec, (entry) => {
forEachEndpoint(spec, (entry, orgPath, method) => {
if(!entry.tags) return;
const categoryName = entry.tags && entry.tags[0];
const category = categories.get(categoryName) || {name: categoryName, items: []};
category.items.push({
title: entry.title,
title:
entry.title || // From swagger overlay JSON when entered manually
entry.description || // From 'notes' field of dvf-pub-api route options if exists
`${method.toUpperCase()} ${orgPath}`, // GET/POST <url> by default
name: entry.operationId,
link: '#' + entry.operationId,
});
categories.set(categoryName, category);
});
return [...categories.values()];
return [...categories.values()]
.filter(category => !categoriesToExclude.includes(category.name.toLowerCase()))
.sort((categoryA, categoryB) => getCategoryWeight(categoryB) - getCategoryWeight(categoryA))
}
42 changes: 34 additions & 8 deletions src/spec/index.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@

import swaggerData from './swagger-v11--submitted.json';
import swaggerMarketData from './swagger-market-data.json';
import swaggerOverlay from './swagger-overlay.json';
import {preprocess} from './preprocess';
import merge from 'lodash.merge';
import request from 'request';

// Swagger URL to be fetched from prod
const swaggerJsonUrl = process.env.SWAGGER_URL || 'https://api.deversifi.com/v1/trading/docs/swagger.json'
// But endpoints to be reached on staging for "Try Endpoint" buttons
const endpointsHost = process.env.ENDPOINTS_HOST || 'api.stg.deversifi.com'

const parseRefs = (properties, definitions) => {
Object.keys(properties).forEach(propKey => {
// if sub param is object check its subtree
if (properties[propKey].type === 'object' && properties[propKey].properties) {
parseRefs(properties[propKey].properties, definitions)
} else if (properties[propKey].type === 'array') {
parseRefs(properties[propKey], definitions)
// if has $ref, replace with definition
} else if (properties[propKey].$ref) {
properties[propKey] = definitions[properties[propKey].$ref.replace('#/definitions/', '')]
const definitionKey = properties[propKey].$ref.replace('#/definitions/', '')
properties[propKey] = definitions[definitionKey]
// also parse definition if it has subtrees
parseRefs(properties[propKey])
parseRefs(properties[propKey], definitions)
}
})
}

const conciliation = data => {
const {paths, definitions} = data;
parseRefs(definitions, definitions)
Object.keys(paths).forEach(pathKey => {
Object.keys(paths[pathKey]).forEach(methodKey => {
const item = paths[pathKey][methodKey] || {};
Expand All @@ -31,12 +40,16 @@ const conciliation = data => {
parameters.forEach(param => {
if (param.schema && param.schema.$ref) {
param.schema = definitions[param.schema.$ref.replace('#/definitions/', '')];

parseRefs(param.schema.properties, definitions)
}
});

Object.keys(responses).forEach(responseKey => {
const response = responses[responseKey]
if (!response) {
console.warn(`Ignoring key ${responseKey} for responses because it is empty`)
return
}
const {schema} = responses[responseKey];
if (schema && schema.$ref) {
responses[responseKey].schema = definitions[schema.$ref.replace('#/definitions/', '')];
Expand All @@ -48,8 +61,21 @@ const conciliation = data => {
return data;
};

export function loadSpec() {
const spec = conciliation(swaggerData);
const fullSpec = merge(spec, swaggerMarketData, swaggerOverlay);
return preprocess(fullSpec);
export function loadSpecAsync() {
return new Promise((resolve, reject) => {
return request(swaggerJsonUrl, function (error, response, body) {
if (error) {
return reject(error);
}
const fetchedSwaggerData = JSON.parse(body);
const swaggerDataToUse = {
...fetchedSwaggerData,
host: endpointsHost,
schemes: ['https']
};
const spec = conciliation(swaggerDataToUse);
const fullSpec = merge(spec, swaggerMarketData, swaggerOverlay);
return resolve(preprocess(fullSpec));
});
});
}
Loading