Skip to content

Commit 19edd3f

Browse files
committed
First commit
0 parents  commit 19edd3f

File tree

7 files changed

+367
-0
lines changed

7 files changed

+367
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
routes.json

.jshintrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"esversion": 6
3+
}

README.md

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# api-to-object
2+
3+
## Features
4+
5+
- All types of requests and methods supported
6+
- Validation on each parameter of every request
7+
8+
## Usage
9+
10+
#### Create a routes.json file, it's structure is defined in routes-sample.json file
11+
12+
Say, config.json file looks like
13+
```
14+
{
15+
"defines": {
16+
"constants": {
17+
"host": "localhost",
18+
"port": 3000
19+
},
20+
"params":
21+
"token": {
22+
"type": "String",
23+
"required": false,
24+
"validation": "",
25+
"description": "ID token obtained from google login"
26+
},
27+
"username": {
28+
"type": "String",
29+
"required": true,
30+
"validation": "",
31+
"description": ""
32+
},
33+
"password": {
34+
"type": "String",
35+
"required": true,
36+
"validation": "",
37+
"description": ""
38+
}
39+
}
40+
},
41+
"api": {
42+
"authorization": {
43+
"admin-login": {
44+
"url": "/api/admin/login",
45+
"method": "POST",
46+
"params": {
47+
"username": null,
48+
"password": null
49+
}
50+
}
51+
},
52+
"user": {
53+
"me": {
54+
"url": "/api/me",
55+
"methods": "GET",
56+
"params": {
57+
token: null,
58+
}
59+
}
60+
}
61+
}
62+
}
63+
64+
```
65+
66+
You need to specify details like url, parameters, method etc for each
67+
api endpoint, and also all parameters are to be defined in defines.params
68+
section of the config file.
69+
70+
Main script can be written as:
71+
72+
```
73+
const fs = require('fs');
74+
const routes = fs.readFileSync('routes.json', 'utf8');
75+
const myApi = require('api-to-object')(routes);
76+
77+
myApi.authorization.adminLogin({
78+
username: 'admin',
79+
password: 'cool'
80+
}).then((res) => {
81+
// res contains the response from api
82+
console.log(res);
83+
}).catch((err) => {
84+
// Error is thrown if anything goes in the process or
85+
// The response status is greater than 399
86+
});
87+
```
88+
89+
### Things to remember
90+
- The script traverses deeper until it finds a block which defines url, consider that
91+
as api endpoint
92+
- Nested routes in config are considered in returned API object
93+
e.g. authorization.adminLogin.
94+
- We can have arbitrary level of nesting in config file.
95+
- API methods are automatically converted from dashed to camelCase e.g.
96+
if config file defines admin-login then it will be converted into adminLogin

index.js

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
const fs = require('fs');
2+
const util = require('./util');
3+
const http = require('http');
4+
const https = require('https');
5+
const querystring = require('querystring');
6+
7+
class Client {
8+
constructor(routes) {
9+
if(!routes) {
10+
throw new Error("Routes config is required");
11+
}
12+
if(typeof routes == 'string') {
13+
try {
14+
routes = JSON.parse(routes);
15+
}
16+
catch(e) {
17+
throw new Error("Error parsing config file", e);
18+
}
19+
}
20+
this.routes = routes;
21+
this.setupRoutes();
22+
}
23+
24+
setupRoutes() {
25+
let routes = this.routes;
26+
this.generateApi(routes.api);
27+
}
28+
29+
generateApi(routes, histRoutes = "") {
30+
let self = this;
31+
let parts = histRoutes.split('/');
32+
// Nested API, we need up update self to point last object
33+
if(parts.length > 1) {
34+
35+
for(let i = 1; i < parts.length; i++) {
36+
self = self[util.toCamelCase(parts[i])];
37+
}
38+
}
39+
Object.keys(routes).forEach((key) => {
40+
// Key name will become the intermediate method
41+
// for all the endpoints
42+
let sub = routes[key];
43+
let curRoute = histRoutes + '/' + key;
44+
let cur = util.toCamelCase(key);
45+
if(sub.url) {
46+
// Consider end of recursion when url is present in sub
47+
self[cur] = (obj) => {
48+
return new Promise((resolve, reject) => {
49+
try {
50+
let body = this.validateParams(obj, sub.params);
51+
let options = {
52+
host: this.routes.defines.constants.host,
53+
port: this.routes.defines.constants.port,
54+
path: sub.url,
55+
method: sub.method,
56+
headers: {
57+
'content-type': sub.contentType || 'application/json'
58+
}
59+
};
60+
61+
// Sets the query parameters on get request
62+
if(options.method === 'GET') {
63+
options.path += '?' + querystring.stringify(body);
64+
}
65+
66+
// User https if api is hosted on https
67+
let transport = (options.port === 443) ? https: http;
68+
69+
let req = transport.request(options, (res) => {
70+
res.setEncoding("utf8");
71+
let data = "";
72+
res.on("data", function(chunk) {
73+
data += chunk;
74+
});
75+
res.on("error", function(err) {
76+
console.log('Error occured in http request', err);
77+
});
78+
res.on("end", function() {
79+
console.log('Request ended');
80+
if (res.statusCode >= 400 && res.statusCode < 600 || res.statusCode < 10) {
81+
reject(new Error(data));
82+
} else {
83+
if(res.headers['content-type'].indexOf('application/json') !== -1) {
84+
try {
85+
res.data = JSON.parse(data);
86+
}
87+
catch(e) {
88+
reject(e);
89+
}
90+
}
91+
else
92+
res.data = data;
93+
resolve(res);
94+
}
95+
});
96+
});
97+
let hasBody ='get|delete|head'.indexOf(sub.method.toLowerCase()) === -1;
98+
if(hasBody) {
99+
console.log('writing to request, ', JSON.stringify(body));
100+
req.write(JSON.stringify(body));
101+
}
102+
req.end();
103+
}
104+
catch(e){
105+
reject(e);
106+
}
107+
});
108+
};
109+
}
110+
else {
111+
// Dig deeper (recursion)
112+
if(cur.length)
113+
self[cur] = {};
114+
this.generateApi(routes[key], curRoute);
115+
}
116+
117+
});
118+
}
119+
120+
validateParams(obj, paramsList) {
121+
// Validates provided parameters with that of
122+
// required parameters for request
123+
let params = Object.keys(paramsList);
124+
125+
for( let parameter of params) {
126+
let def = this.routes.defines.params[parameter];
127+
let val = obj[parameter];
128+
if(val) {
129+
if(typeof val != def.type.toLowerCase())
130+
throw new Error(parameter + " value incompatible");
131+
}
132+
else if(def.required) {
133+
throw new Error(parameter + " is required");
134+
}
135+
}
136+
137+
return obj;
138+
}
139+
}
140+
141+
module.exports = (config) => {
142+
return new Client(config);
143+
};

package.json

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "api-to-object",
3+
"version": "0.0.1",
4+
"description": "Converts given API into functions",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com:AnshulMalik/api-to-object.git"
12+
},
13+
"bugs": {
14+
"url": "https://github.com/AnshulMalik/api-to-object/issues"
15+
},
16+
"homepage": {
17+
"url": "https://github.com/AnshulMalik/api-to-object#readme"
18+
},
19+
"tags": [
20+
"http",
21+
"https",
22+
"api",
23+
"functions",
24+
"node",
25+
"nodejs"
26+
],
27+
"engines": {
28+
"node": ">4"
29+
},
30+
"author": "Anshul <[email protected]>",
31+
"license": "ISC"
32+
}

routes-sample.json

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
{
2+
"defines": {
3+
"constants": {
4+
"host": "localhost",
5+
"port": 3000
6+
},
7+
"params": {
8+
"token": {
9+
"type": "String",
10+
"required": false,
11+
"validation": "",
12+
"description": ""
13+
},
14+
"idToken": {
15+
"type": "String",
16+
"required": false,
17+
"validation": "",
18+
"description": "ID token obtained from google login"
19+
},
20+
"username": {
21+
"type": "String",
22+
"required": true,
23+
"validation": "",
24+
"description": ""
25+
},
26+
"password": {
27+
"type": "String",
28+
"required": true,
29+
"validation": "",
30+
"description": ""
31+
},
32+
"teamName": {
33+
"type": "String",
34+
"required": true,
35+
"validation": "",
36+
"description": ""
37+
},
38+
"eventId": {
39+
"type": "Number",
40+
"required": true,
41+
"validation": "",
42+
"description": ""
43+
}
44+
}
45+
},
46+
"api": {
47+
"authorization": {
48+
"user-login": {
49+
"url": "/api/user/login",
50+
"method": "POST",
51+
"params": {
52+
"idToken": null
53+
},
54+
"description": "Login the user"
55+
},
56+
"admin-login": {
57+
"url": "/api/admin/login",
58+
"method": "POST",
59+
"params": {
60+
"username": null,
61+
"password": null
62+
}
63+
}
64+
},
65+
"teams": {
66+
"create": {
67+
"url": "/api/teams",
68+
"method": "POST",
69+
"params": {
70+
"token": null,
71+
"teamName": null,
72+
"eventId": null
73+
}
74+
}
75+
},
76+
"events": {
77+
"get-all": {
78+
"url": "/api/events",
79+
"method": "GET",
80+
"params": {
81+
82+
}
83+
}
84+
}
85+
}
86+
}

util.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports.toCamelCase = (str) => {
2+
return str.toLowerCase().replace(/(\-[a-z])/g, (s) => {
3+
return s.replace('-', '').toUpperCase();
4+
});
5+
};

0 commit comments

Comments
 (0)