Comparing version 1.0.0 to 1.0.1
51
index.ts
import {Request} from "./src/main/core/Request"; | ||
import {Method, Http4jsRequest} from "./src/main/core/HttpMessage"; | ||
import {HttpHandler} from "./src/main/core/HttpMessage"; | ||
import {routes} from "./src/main/core/RoutingHttpHandler"; | ||
import {Response} from "./src/main/core/Response"; | ||
import {httpClient} from "./src/main/core/Client"; | ||
import {HttpClient} from "./src/main/core/Client"; | ||
import {Body} from "./src/main/core/Body"; | ||
import {Uri} from "./src/main/core/Uri"; | ||
let req: Http4jsRequest = new Request(Method.GET, "/path"); | ||
let handler = (req: Request) => { | ||
return new Response(new Body(Buffer.from(`${req.method} to ${req.uri} with headers ${req.headers}`))) | ||
let bodyString = `<h1>${req.method} to ${req.uri.href} with headers ${Object.keys(req.headers)}</h1>`; | ||
return new Response(200, new Body(Buffer.from(bodyString))) | ||
}; | ||
routes("/path", handler).asServer(3000).start(); | ||
let options = { | ||
host: 'localhost', | ||
port: 3000, | ||
method: 'post', | ||
path: '/path', | ||
headers: {tom: ["pwns", "rocks", "smells"]} | ||
let headerFilter = (handler: HttpHandler) => { | ||
return (req: Request) => { | ||
return handler(req.setHeader("filter", "1")); | ||
} | ||
}; | ||
httpClient().get(options).then(succ => console.log(succ)); | ||
let moreRoutes = routes("/bob/{id}", "POST", (req) => { return new Response(201, new Body("created a " + req.path))}); | ||
routes("/path", "GET", handler) | ||
.withHandler("/tom", "GET", handler) | ||
.withRoutes(moreRoutes) | ||
.withFilter(headerFilter) | ||
.asServer(3000).start(); | ||
let getRequest = new Request("GET", Uri.of("http://localhost:3000/path/tom")).setHeader("tom", "rules"); | ||
let postRequest = new Request("GET", Uri.of("http://localhost:3000/path/tom")).setHeader("tom", "rules"); | ||
let client = HttpClient; | ||
client(getRequest).then(succ => { | ||
console.log("body string"); | ||
console.log(succ.body.bodyString()); | ||
console.log("headers"); | ||
console.log(succ.headers); | ||
}); | ||
client(postRequest).then(succ => { | ||
console.log("body string"); | ||
console.log(succ.body.bodyString()); | ||
console.log("headers"); | ||
console.log(succ.headers); | ||
}); | ||
{ | ||
"name": "http4js", | ||
"version": "1.0.0", | ||
"version": "1.0.1", | ||
"description": "", | ||
"main": "index.js", | ||
"main": "dist/index.js", | ||
"types" : "dist/index.d.ts", | ||
"scripts": { | ||
"test": "mocha src/test/**", | ||
"build": "tsc", | ||
"app": "tsc; node index.js" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/tomshacham/http4js.git" | ||
}, | ||
"author": "", | ||
@@ -11,0 +17,0 @@ "license": "ISC", |
## Http4js | ||
A port of http4k: a lightweight _toolkit_ to allow in memory functional testing and to simplify working with HTTP. | ||
A port of [http4k](https://github.com/http4k/http4k): a lightweight _toolkit_ to allow in memory functional testing and to simplify working with HTTP. | ||
#### To run: | ||
`tsc; node index.js` | ||
`tsc index.ts --target es5; node index.js` | ||
@@ -16,3 +16,3 @@ #### To test: | ||
**In order to run test in idea/webstorm**, you may need to: | ||
**In order to run tests in idea/webstorm**, you may need to: | ||
@@ -27,4 +27,7 @@ ``` | ||
- client catching errors and supporting all methods | ||
- chaining and nesting filters | ||
- uri has path and host and protocol and scheme and authority | ||
- code: | ||
- extract form data | ||
- ordering of filters | ||
- other client verbs | ||
- write docs | ||
- publish as library to npm |
import * as http from 'http'; | ||
import {Response} from "./Response"; | ||
import {Body} from "./Body"; | ||
import {Method} from "./HttpMessage"; | ||
export function httpClient() { | ||
return new HttpClient(); | ||
export function HttpClient(request) { | ||
if (request.method == "GET") { | ||
return get(request) | ||
} else { | ||
return post(request) | ||
} | ||
} | ||
export function HttpClient() { | ||
this.get = (request) => { | ||
let options = {}; | ||
options["hostname"] = "localhost"; | ||
options["port"] = 3000; | ||
options["path"] = "/"; | ||
options["headers"] = request.headers; | ||
options["method"] = Method[request.method]; | ||
function get(request): Promise<Response> { | ||
let options = request.uri.asRequest; | ||
options.headers = request.headers; | ||
return new Promise(succ => { | ||
http.request(options, (res) => { | ||
let chunks = []; | ||
res.on('data', (chunk) => { | ||
chunks.push(chunk); | ||
}); | ||
res.on('end', () => { | ||
let body = new Body(Buffer.concat(chunks)); | ||
let response = new Response(body).setHeaders(res.headers); | ||
succ(response); | ||
}); | ||
}).end(); | ||
}); | ||
}; | ||
return new Promise(succ => { | ||
http.request(options, (res) => { | ||
let chunks = []; | ||
res.on('data', (chunk) => { | ||
chunks.push(chunk); | ||
}); | ||
res.on('end', () => { | ||
let body = new Body(Buffer.concat(chunks)); | ||
let response = new Response(res.statusCode, body).setHeaders(res.headers); | ||
return succ(response); | ||
}); | ||
}).end(); | ||
}); | ||
}; | ||
this.post = (request) => { | ||
let options = {}; | ||
options["hostname"] = "localhost"; | ||
options["port"] = 3000; | ||
options["path"] = "/"; | ||
options["headers"] = request.headers; | ||
options["method"] = Method[request.method]; | ||
function post(request): Promise<Response> { | ||
let options = request.uri.asRequest; | ||
options.headers = request.headers; | ||
options.method = request.method; | ||
return new Promise(succ => { | ||
let clientRequest = http.request(options, (res) => { | ||
let chunks = []; | ||
res.on('data', (chunk) => { | ||
chunks.push(chunk); | ||
}); | ||
res.on('end', () => { | ||
let body = new Body(Buffer.concat(chunks)); | ||
let response = new Response(body).setHeaders(res.headers); | ||
succ(response); | ||
}); | ||
return new Promise(succ => { | ||
let clientRequest = http.request(options, (res) => { | ||
let chunks = []; | ||
res.on('data', (chunk) => { | ||
chunks.push(chunk); | ||
}); | ||
clientRequest.write(request.body.bodyString()); | ||
clientRequest.end(); | ||
res.on('end', () => { | ||
let body = new Body(Buffer.concat(chunks)); | ||
let response = new Response(res.statusCode, body).setHeaders(res.headers); | ||
return succ(response); | ||
}); | ||
}); | ||
} | ||
clientRequest.write(request.body.bodyString()); | ||
clientRequest.end(); | ||
}); | ||
} | ||
@@ -27,7 +27,2 @@ import {Body} from "./Body"; | ||
export enum Method { | ||
GET, | ||
POST | ||
} | ||
export interface Http4jsRequest extends HttpMessage { | ||
@@ -34,0 +29,0 @@ method: string |
@@ -1,5 +0,5 @@ | ||
import {Http4jsRequest, Method} from "./HttpMessage"; | ||
import {Headers} from "./Headers"; | ||
import {Http4jsRequest} from "./HttpMessage"; | ||
import {Body} from "./Body"; | ||
import {Uri} from "./Uri"; | ||
import {isNullOrUndefined} from "util"; | ||
@@ -12,6 +12,6 @@ export class Request implements Http4jsRequest { | ||
body: Body; | ||
private queries = {}; | ||
queries = {}; | ||
constructor( | ||
method: Method, | ||
method: string, | ||
uri: Uri | string, | ||
@@ -21,3 +21,3 @@ body: Body = new Body(new Buffer("")), | ||
) { | ||
this.method = method.toString(); | ||
this.method = method; | ||
if (typeof uri == "string") { | ||
@@ -30,5 +30,16 @@ this.uri = Uri.of(uri); | ||
this.headers = headers ? headers : {}; | ||
this.queries = this.getQueryParams(); | ||
return this; | ||
} | ||
private getQueryParams(): object { | ||
if (isNullOrUndefined(this.uri.query)) return {}; | ||
let pairs = this.uri.query.split("&"); | ||
pairs.map(pair => { | ||
let split = pair.split("="); | ||
this.queries[split[0]] = split[1] | ||
}); | ||
return this.queries; | ||
} | ||
setUri(uri: Uri | string): Request { | ||
@@ -81,12 +92,11 @@ if (typeof uri == "string") { | ||
query(name: string, value: string): Request { | ||
let queries = Object.keys(this.queries); | ||
if (queries.length > 0) { | ||
this.uri.uriString += `&${name}=${value}`; | ||
} else { | ||
this.uri.uriString += `?${name}=${value}`; | ||
} | ||
this.queries[name] = value; | ||
this.uri = this.uri.withQuery(name, value); | ||
return this; | ||
} | ||
getQuery(name: string) : string { | ||
return this.queries[name]; | ||
} | ||
} |
import {HttpMessage} from "./HttpMessage"; | ||
import {Headers} from "./Headers"; | ||
import {Body} from "./Body"; | ||
import {Uri} from "./Uri"; | ||
interface Http4jsResponse extends HttpMessage { | ||
} | ||
export interface Http4jsResponse extends HttpMessage {} | ||
@@ -13,9 +11,12 @@ export class Response implements Http4jsResponse { | ||
body: Body; | ||
status: number; | ||
pathParams: object; | ||
constructor(body: Body = new Body("")) { | ||
constructor(status: number = 200, body: Body = new Body("")) { | ||
this.body = body; | ||
this.status = status; | ||
} | ||
getHeader(name: string): string { | ||
return this.headers[name]; | ||
return this.headers[name.toLowerCase()]; | ||
} | ||
@@ -39,3 +40,3 @@ | ||
allHeaders(headers: Headers): Response { | ||
allHeaders(headers: object): Response { | ||
return undefined; | ||
@@ -42,0 +43,0 @@ } |
@@ -1,2 +0,1 @@ | ||
import * as http from "http"; | ||
import {Response} from "./Response"; | ||
@@ -6,43 +5,21 @@ import {Http4jsRequest, HttpHandler} from "./HttpMessage"; | ||
import {Body} from "./Body"; | ||
import {Http4jsServer, Server} from "./Server"; | ||
import {Uri} from "./Uri"; | ||
interface Router { | ||
match(request: Http4jsRequest): Response | ||
} | ||
interface RoutingHttpHandler extends Router { | ||
export interface RoutingHttpHandler { | ||
withFilter(filter: (HttpHandler) => HttpHandler): RoutingHttpHandler | ||
// withBasePath(path: String): RoutingHttpHandler | ||
asServer(port: number): Http4jsServer | ||
match(request: Http4jsRequest): Response | ||
} | ||
interface Http4jsServer { | ||
server; | ||
port: number; | ||
start(): void | ||
stop(): void | ||
export function routes(path: string, method: string, handler: HttpHandler): ResourceRoutingHttpHandler { | ||
return new ResourceRoutingHttpHandler(path, method, handler); | ||
} | ||
class BasicServer implements Http4jsServer { | ||
server; | ||
port: number; | ||
constructor(port: number) { | ||
this.port = port; | ||
this.server = http.createServer(); | ||
return this; | ||
} | ||
start(): void { | ||
this.server.listen(this.port) | ||
} | ||
stop(): void { | ||
this.server.close() | ||
} | ||
export function getTo(path: string, handler: HttpHandler): ResourceRoutingHttpHandler { | ||
return new ResourceRoutingHttpHandler(path, "GET", handler); | ||
} | ||
export function routes(path: string, handler: HttpHandler): ResourceRoutingHttpHandler { | ||
return new ResourceRoutingHttpHandler(path, handler); | ||
export function postTo(path: string, handler: HttpHandler): ResourceRoutingHttpHandler { | ||
return new ResourceRoutingHttpHandler(path, "POST", handler); | ||
} | ||
@@ -54,16 +31,38 @@ | ||
private path: string; | ||
private handler: HttpHandler; | ||
private handler: object; | ||
private handlers: object = {}; | ||
private filters: Array<any> = []; | ||
constructor(path: string, handler: HttpHandler) { | ||
constructor(path: string, | ||
method: string, | ||
handler: HttpHandler) { | ||
this.path = path; | ||
this.handler = handler; | ||
let verbToHandler = {verb: method, handler: handler}; | ||
this.handler = verbToHandler; | ||
this.handlers[path] = verbToHandler; | ||
} | ||
withFilter(filter: (HttpHandler) => HttpHandler): RoutingHttpHandler { | ||
return new ResourceRoutingHttpHandler(this.path, filter(this.handler)); | ||
withRoutes(routes: ResourceRoutingHttpHandler): ResourceRoutingHttpHandler { | ||
for (let path of Object.keys(routes.handlers)) { | ||
let existingPath = this.path != "/" ? this.path : ""; | ||
let nestedPath = existingPath + path; | ||
this.handlers[nestedPath] = routes.handlers[path] | ||
} | ||
return this; | ||
} | ||
withFilter(filter: (HttpHandler) => HttpHandler): ResourceRoutingHttpHandler { | ||
this.filters.push(filter); | ||
return this; | ||
} | ||
withHandler(path: string, method: string, handler: HttpHandler): ResourceRoutingHttpHandler { | ||
let existingPath = this.path != "/" ? this.path : ""; | ||
let nestedPath = existingPath + path; | ||
this.handlers[nestedPath] = {verb: method, handler: handler}; | ||
return this; | ||
} | ||
asServer(port: number): Http4jsServer { | ||
this.server = new BasicServer(port); | ||
this.server = new Server(port); | ||
this.server.server.on("request", (req, res) => { | ||
@@ -77,7 +76,5 @@ const {headers, method, url} = req; | ||
}).on('end', () => { | ||
let body = new Body(Buffer.concat(chunks)); | ||
let inMemoryRequest = new Request(method, url, body, headers); | ||
let response = this.match(inMemoryRequest); | ||
res.writeHead(200, response.headers); | ||
res.end(response.bodyString()); | ||
let response = this.createInMemResponse(chunks, method, url, headers); | ||
res.writeHead(response.status, response.headers); | ||
res.end(response.body.bytes); | ||
}) | ||
@@ -89,14 +86,30 @@ }); | ||
match(request: Http4jsRequest): Response { | ||
let handler = this.handler; | ||
let path = this.path; | ||
if (request.uri.match(path)) { | ||
return handler(request); | ||
let paths = Object.keys(this.handlers); | ||
let exactMatch = paths.find(handlerPath => { | ||
return request.uri.match(handlerPath) && this.handlers[handlerPath].verb == request.method | ||
}); | ||
let fuzzyMatch = paths.find(handlerPath => { | ||
return Uri.of(handlerPath).match(request.uri.path) && this.handlers[handlerPath].verb == request.method | ||
}); | ||
let match = exactMatch || fuzzyMatch; | ||
if (match) { | ||
let handler = this.handlers[match].handler; | ||
let filtered = this.filters.reduce((acc, next) => { return next(acc) }, handler); | ||
let response = filtered(request); | ||
if (match.includes("{")) response.pathParams = Uri.of(match).extract(request.uri.path).matches; | ||
return response; | ||
} else { | ||
let body = new Body(Buffer.from(`${request.method} to ${request.uri.uriString} did not match route ${path}`)); | ||
return new Response(body); | ||
let notFoundBody = `${request.method} to ${request.uri.template} did not match routes ${paths.join(" // ")}`; | ||
let body = new Body(notFoundBody); | ||
return new Response(404, body); | ||
} | ||
} | ||
private createInMemResponse(chunks: Array<any>, method: any, url: any, headers: any): Response { | ||
let body = new Body(Buffer.concat(chunks)); | ||
let inMemRequest = new Request(method, url, body, headers); | ||
return this.match(inMemRequest); | ||
} | ||
} | ||
@@ -0,26 +1,34 @@ | ||
let URI = require('url'); | ||
export class Uri { | ||
template: string; | ||
uriString: string; | ||
path: string; | ||
scheme: string; | ||
authority: string; | ||
protocol: string; | ||
auth: string; | ||
query: string; | ||
fragment: string; | ||
private matches: object = {}; | ||
private pathParamMatchingRegex: RegExp = new RegExp(/\{(\w+)\}/); | ||
hostname: string; | ||
port: string; | ||
href: string; | ||
template: string; | ||
asRequest: object; | ||
matches: object = {}; | ||
private pathParamMatchingRegex: RegExp = new RegExp(/\{(\w+)\}/g); | ||
private pathParamCaptureTemplate: string = "([\\w\\s]+)"; | ||
constructor(uri: string) { | ||
this.template = uri; | ||
this.uriString = encodeURI(uri); | ||
let rfc3986 = new RegExp(/^(?:([^:\/?#]+):)?(?:\/\/([^\/?#]*))?([^?#]*)(?:\\?([^#]*))?(?:#(.*))?/); | ||
let [wtfIsThis, scheme,authority,path,query,fragment] = uri.split(rfc3986); | ||
this.scheme = scheme; | ||
this.authority = authority; | ||
this.path = path; | ||
this.query = query; | ||
this.fragment = fragment; | ||
constructor(template: string) { | ||
let uri = URI.parse(template); | ||
this.asRequest = uri; | ||
this.template = uri.pathname; | ||
this.protocol = uri.protocol; | ||
this.auth = uri.auth; | ||
this.hostname = uri.hostname; | ||
this.path = uri.path; | ||
this.port = uri.port; | ||
this.query = uri.query; | ||
this.href = uri.href; | ||
} | ||
static of (uri: string): Uri { | ||
static of(uri: string): Uri { | ||
return new Uri(uri) | ||
@@ -34,6 +42,10 @@ } | ||
extract (uri: string): Uri { | ||
let decoded = decodeURI(uri); | ||
let pathParamName = this.pathParamMatchingRegex.exec(this.template)[1]; | ||
this.matches[pathParamName] = this.uriTemplateToPathParamCapturingRegex().exec(decoded)[1]; | ||
extract(uri: string): Uri { | ||
let decodedUri = decodeURI(uri); | ||
let pathParamNames = this.template.match(this.pathParamMatchingRegex) | ||
.map(it => it.replace("{", "").replace("}", "")); | ||
let pathParams = this.uriTemplateToPathParamCapturingRegex().exec(decodedUri); | ||
pathParamNames.map( (name, i) => { | ||
this.matches[name] = pathParams[i+1] | ||
}); | ||
return this; | ||
@@ -46,2 +58,10 @@ } | ||
withQuery(name: string, value: string): Uri { | ||
if (this.query && this.query.length > 0){ | ||
return Uri.of(this.href + `&${name}=${value}`) | ||
} else { | ||
return Uri.of(this.href + `?${name}=${value}`) | ||
} | ||
} | ||
private uriTemplateToPathParamCapturingRegex (): RegExp { | ||
@@ -48,0 +68,0 @@ return new RegExp(this.template.replace( |
import * as assert from "assert"; | ||
import {equal} from "assert"; | ||
import {Request} from "../../main/core/Request"; | ||
import {Method} from "../../main/core/HttpMessage"; | ||
import {Body} from "../../main/core/Body"; | ||
@@ -11,6 +10,6 @@ | ||
equal( | ||
new Request(Method.GET, "/") | ||
new Request("GET", "/") | ||
.setUri("/tom") | ||
.uri | ||
.uriString, | ||
.template, | ||
"/tom") | ||
@@ -21,3 +20,3 @@ }); | ||
equal( | ||
new Request(Method.GET, "/") | ||
new Request("GET", "/") | ||
.setBody(new Body("body boy")) | ||
@@ -30,3 +29,3 @@ .bodyString(), | ||
equal( | ||
new Request(Method.GET, "/") | ||
new Request("GET", "/") | ||
.setBodystring("tommy boy") | ||
@@ -39,8 +38,8 @@ .bodyString(), | ||
equal( | ||
new Request(Method.GET, "/tom") | ||
new Request("GET", "/tom") | ||
.query("tom", "tosh") | ||
.query("ben", "bosh") | ||
.uri | ||
.uriString, | ||
"/tom?tom=tosh&ben=bosh") | ||
.query, | ||
"tom=tosh&ben=bosh") | ||
}); | ||
@@ -50,3 +49,3 @@ | ||
equal( | ||
new Request(Method.GET, "some/url") | ||
new Request("GET", "some/url") | ||
.setHeader("tom", "smells") | ||
@@ -59,3 +58,3 @@ .getHeader("tom"), | ||
assert.deepEqual( | ||
new Request(Method.GET, "some/url") | ||
new Request("GET", "some/url") | ||
.setHeader("tom", "smells") | ||
@@ -70,3 +69,3 @@ .setHeader("tom", "smells more") | ||
equal( | ||
new Request(Method.GET, "some/url") | ||
new Request("GET", "some/url") | ||
.setHeader("tom", "smells") | ||
@@ -80,3 +79,3 @@ .replaceHeader("tom", "is nice") | ||
equal( | ||
new Request(Method.GET, "some/url") | ||
new Request("GET", "some/url") | ||
.setHeader("tom", "smells") | ||
@@ -83,0 +82,0 @@ .removeHeader("tom") |
import * as assert from "assert"; | ||
import {equal, deepEqual} from "assert"; | ||
import {Request} from "../../main/core/Request"; | ||
import {Method} from "../../main/core/HttpMessage"; | ||
import {httpClient} from "../../main/core/Client"; | ||
import {routes} from "../../main/core/RoutingHttpHandler"; | ||
import {equal} from "assert"; | ||
import {Response} from "../../main/core/Response"; | ||
@@ -62,4 +58,4 @@ import {Body} from "../../main/core/Body"; | ||
undefined); | ||
}) | ||
}); | ||
}); |
@@ -8,3 +8,3 @@ import {equal} from "assert"; | ||
equal( | ||
Uri.of("/tom/is the sugar/goodness").uriString, | ||
Uri.of("/tom/is the sugar/goodness").template, | ||
"/tom/is%20the%20sugar/goodness"); | ||
@@ -38,15 +38,9 @@ }); | ||
Uri.of("http://localhost:3000/tom/is the hot sauce/guy").path, | ||
"/tom/is the hot sauce/guy"); | ||
"/tom/is%20the%20hot%20sauce/guy"); | ||
}); | ||
it("parses out the authority from uri string", () => { | ||
equal( | ||
Uri.of("http://localhost:3000/").authority, | ||
"localhost:3000"); | ||
}); | ||
it("parses out the scheme from uri string", () => { | ||
equal( | ||
Uri.of("http://localhost:3000/").scheme, | ||
"http"); | ||
Uri.of("http://localhost:3000/").protocol, | ||
"http:"); | ||
}); | ||
@@ -57,5 +51,5 @@ | ||
Uri.of("http://localhost:3000/?tom=foo&ben=bar").query, | ||
"?tom=foo&ben=bar"); | ||
"tom=foo&ben=bar"); | ||
}); | ||
}); |
{ | ||
"compilerOptions": { | ||
"target": "es5", | ||
"lib": ["es2015"] | ||
"lib": ["es2015"], | ||
"declaration": true, | ||
"outDir": "./dist" | ||
}, | ||
@@ -6,0 +8,0 @@ "include": [ |
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
32006
30
874
32
0