Skip to content

Instantly share code, notes, and snippets.

@latant
Last active March 29, 2019 18:35
Show Gist options
  • Save latant/e323e30fa5d1bd4bb7096c8e3c131706 to your computer and use it in GitHub Desktop.
Save latant/e323e30fa5d1bd4bb7096c8e3c131706 to your computer and use it in GitHub Desktop.
Creating REST API description object in Javascript, then automatically create the methods in the same object.
//Example API description object.
//In most requests we attach data as query params and/or json.
//Usually the fields can be optional in some requests.
const REQ = 'required'
const OPT = 'optional'
export const Api = {
users: {
me: {
GET: {}
},
DELETE: {}
},
messages: {
POST: {
json: {
id: REQ,
text: REQ
}
},
GET: {
params: {
id: REQ,
limit: REQ,
before: OPT
}
}
}
}
//1. We can create the method signature as strings automatically by exploring the object recursively.
const verbs = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']
const isVerb = (key) => verbs.includes(key.toUpperCase())
function getMethodSignatures(obj, parentURL) {
let urls = []
for (const key in obj) {
if (isVerb(key))
urls.push(key + ' ' + parentURL)
else
urls = urls.concat(getMethodSignatures(obj[key], parentURL + '/' + key))
return urls
}
}
console.log(getURLs(Api, '/')) // GET /users/me, DELETE /users, POST /messages, GET /messages
//2. We can also create the methods by extending this function. Here I am using axios
function createMethods(obj, parentURL) {
for (const key in obj) {
if (isVerb(key)) {
obj[key] = function (data) {
const config = {
method: key.toLowerCase(),
url: parentURL
}
if (data) {
if (data.params) config.params = data.params
if (data.json) config.data = data.json
}
return axios(config)
}
} else
createMethods(obj[key], parentURL + '/' + key))
}
createMethods(Api, '/')
//And now we can use the object for making API calls like this:
Api.messages.GET({
params: {
id: 20,
limit: 40
}
}).then(r => console.log(r))
Api.users.me.GET().then(r => console.log(r))
//3. We can also use the declared structure of the method in the API description object for verifying the the given parameter. We have to extend the previous function for this.
function createMethods(obj, parentURL) {
for (const key in obj) {
if (isVerb(key)) {
const expected = obj[key]
obj[key] = function (data) {
const check = checkStructure(data, expected)
if (check !== 'ok')
throw 'wrong given data by calling ' + key + ' ' + parentURL
const config = {
method: key.toLowerCase(),
url: parentURL
}
if (data) {
if (data.params) config.params = data.params
if (data.json) config.data = data.json
}
return axios(config)
}
} else
createMethods(obj[key], parentURL + '/' + key))
}
}
// the checkStructure function checks recursively if the keys of the given data is identical as defined in the API description object. It can also deal with the info that a certain field is optional or required. This function only asserts the required fields and doesn't let you give additional fields that wasn't given in the Api description.
function checkStructure(obj, expected) {
const exp = cloneObject(expected)
for (const i in obj) {
if (!(i in exp))
return 'unused: ' + i
const needsObject = typeof exp[i] == 'object'
if (obj[i].constructor === Array) {
if (needsObject)
return 'array instead of object: ' + i
else continue
}
if (typeof obj[i] == 'object') {
if (!needsObject)
return 'object instead of literal: ' + i
const checkDeeper = checkStructure(obj[i], exp[i])
if (checkDeeper !== 'ok')
return checkDeeper
} else if (needsObject) {
console.log(obj[i], typeof i, needsObject, exp[i])
return 'literal instead of object: ' + i
}
delete exp[i]
}
const expProps = requiredPropsOf(exp)
if (expProps.length > 0)
return 'required: ' + expProps.join(',')
return 'ok'
}
function cloneObject(obj) {
const clone = {}
for (const i in obj) {
const type = typeof obj[i]
if (obj[i] !== undefined && type === 'object')
clone[i] = cloneObject(obj[i])
else
clone[i] = obj[i]
}
return clone
}
function requiredPropsOf(obj) {
const result = []
for (const i in obj) {
if (obj[i] === REQ)
result.push(i)
}
return result
}
//With this, we can call methods like after Step 2, but we get error when passing data as not defined in the Api description.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment