Network abstraction layer for a JavaScript JSON API client











up vote
2
down vote

favorite












I'm rewriting collection-json.js, a client for Collection+JSON. (Collection+JSON is a HATEAOS API media type built on top of JSON.)



My Goals:




  • Remove CoffeeScript

  • webpack

  • UMD so it can be used via node and via <script ...> (not there yet.)


I'm more comfortable in Ruby, though I do know JS. Still, I wouldn't say it's my native tongue.



The current challenge is the 'http' file. I have the actual HTTP calls in one module, and then the HTTP library in another. We use different abstraction patters but I believe the original author's intent was to allow the insertion of other network layers into the HTTP client, which would then allow for things like using jquery's methods or, perhaps more importantly, for inserting test-related stubs.



Here's my current code:



import fetch from 'node-fetch';

let content_type = "application/vnd.collection+json";

class HttpImplementation {

// http_callback is used by each of the verb-methods below.
// It should call `done` with (error_string_or_null, body_as_text,
// response_headers)
//
http_callback(response, done) {
if(response.status < 400) {
// strangely, response.text() returns a promise...
response.text().then((actual_response) => {
done(null, actual_response, response.headers)
})
}
else { done(response.status); }
}

get(href, options, done) {
fetch(href).then((response) => {this.http_callback(response, done)}).
catch(error => done(error));
};

post(href, options, done) {
http_options = Object.assign({}, options,
{
method: "POST", body: JSON.stringify(options.body),
headers: options.headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(this.http_callback, done));
};

put(href, options, done) {
http_options = Object.assign({}, options,
{
method: "PUT", body: JSON.stringify(options.body),
headers: headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(this.http_callback, done));
}

delete(href, options, done){
http_options = Object.assign({}, options,
{
method: "DELETE", body: JSON.stringify(options.body),
headers: headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(http_callback, done));
};
}

class InterfaceLayer {
constructor( network_layer )
{
if( network_layer == null || network_layer == undefined )
{ this.source = new HttpImplementation(); }
else
{ this.source = network_layer ; }
}

get(href, options, done) {
if(!options.headers) { options.headers = {} }
options.headers["accept"] = content_type;

this.source.get(href, options, function(error, collection, headers) {
// the previous implementation of this was async, so
// it might bear reworking once figured out
// seems like you could add a layer that would try each one
// and, on failure, try the next.

if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

post(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.post(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

put(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.put(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

delete(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.delete(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}
}

export default (new InterfaceLayer())


Sample usage in the form of a 'live test':



import Client from '../src/client.js'

describe("LIVE test from ACTUAL API", () => {
test("it gets an error when calling an invalid path", () => {
new Client('http://todo-collection-json.herokuapp.com/invalid',
(error, collection) => {
expect(error).toBe(404);
expect(collection).toBeUndefined();
});
})

test('it works with a real-life-ish example', () => {
expect.assertions();

new Client('http://todo-collection-json.herokuapp.com/collection/tasks/',
(error, collection) => {
expect(collection).not.toBeDefined();
console.log(collection);
expect(error).toBeNull();
});
})
})




Original http.coffee:




module.exports = defaults =
_get: (href, options, done=()->)->
done new Error "'GET' not implemented"

_post: (href, options, done=()->)->
done new Error "'POST' not implemented"

_put: (href, options, done=()->)->
done new Error "'PUT' not implemented"

_del: (href, options, done=()->)->
done new Error "'DELETE' not implemented"

cache:
# This is a fake cache. You should probably add a real one...
put: (key, value, time, done=()->)->
if typeof time is 'function'
done = time
time = null
done()
del: (key, done=()->)->
done()
clear: (done=()->)->
done()
get: (key, done=()->)->
done()
size: (done=()->)->
done 0
memsize: (done=()->)->
done 0
debug: ()->
true

module.exports["content-type"] = "application/vnd.collection+json"

module.exports.get = (href, options, done)->
defaults.cache.get href, (error, collection)->
# Give them the cached stuff if we have it
return done error, collection if error or collection

options.headers ||= {}
options.headers["accept"] = module.exports["content-type"]

module.exports.setOptions href, options

defaults._get href, options, (error, collection, headers)->
return done error if error
performCache collection, headers
done error, collection

module.exports.post = (href, options={}, done)->

options.headers ||= {}
options.headers["accept"] = module.exports["content-type"]
options.headers["content-type"] = module.exports["content-type"]

module.exports.setOptions href, options

defaults._post href, options, (error, collection, headers)->
# Do we cache this?
done error, collection

# Should be overridden by the client
module.exports.setOptions = (href, options)->

performCache = (collection, headers)->
# Expires
# expires = response.headers.expires
# # TODO convert to time-from-now
# # Cache-Control
# # TODO implement
# defaults.cache.put response.request.href, response.body, new Date(expires).getTime() if expires



Original index.coffee:




request = require "request"
http = require "./http"

http._get = (href, options, done)->
options.url = href
request options, (error, response, body)->
done error, body, response?.headers

http._post = (href, options, done)->
options.url = href
options.body = JSON.stringify options.body
options.method = "POST"
request options, (error, response, body)->
done error, body, response?.headers

http._put = (href, options, done)->
options.url = href
options.body = JSON.stringify options.body
request.put options, (error, response)->
done error, response

http._del = (href, options, done)->
options.url = href
request.del options, (error, response)->
done error, response

module.exports = require "./client"



Questions




  • Is this a good way to separate these two concerns?

  • Is there a much cleaner/simpler way to express all this?


I'm open to input on any part of the code, but especially this http aspect.










share|improve this question
























  • We've just taken it off hold.
    – Mast
    11 hours ago















up vote
2
down vote

favorite












I'm rewriting collection-json.js, a client for Collection+JSON. (Collection+JSON is a HATEAOS API media type built on top of JSON.)



My Goals:




  • Remove CoffeeScript

  • webpack

  • UMD so it can be used via node and via <script ...> (not there yet.)


I'm more comfortable in Ruby, though I do know JS. Still, I wouldn't say it's my native tongue.



The current challenge is the 'http' file. I have the actual HTTP calls in one module, and then the HTTP library in another. We use different abstraction patters but I believe the original author's intent was to allow the insertion of other network layers into the HTTP client, which would then allow for things like using jquery's methods or, perhaps more importantly, for inserting test-related stubs.



Here's my current code:



import fetch from 'node-fetch';

let content_type = "application/vnd.collection+json";

class HttpImplementation {

// http_callback is used by each of the verb-methods below.
// It should call `done` with (error_string_or_null, body_as_text,
// response_headers)
//
http_callback(response, done) {
if(response.status < 400) {
// strangely, response.text() returns a promise...
response.text().then((actual_response) => {
done(null, actual_response, response.headers)
})
}
else { done(response.status); }
}

get(href, options, done) {
fetch(href).then((response) => {this.http_callback(response, done)}).
catch(error => done(error));
};

post(href, options, done) {
http_options = Object.assign({}, options,
{
method: "POST", body: JSON.stringify(options.body),
headers: options.headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(this.http_callback, done));
};

put(href, options, done) {
http_options = Object.assign({}, options,
{
method: "PUT", body: JSON.stringify(options.body),
headers: headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(this.http_callback, done));
}

delete(href, options, done){
http_options = Object.assign({}, options,
{
method: "DELETE", body: JSON.stringify(options.body),
headers: headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(http_callback, done));
};
}

class InterfaceLayer {
constructor( network_layer )
{
if( network_layer == null || network_layer == undefined )
{ this.source = new HttpImplementation(); }
else
{ this.source = network_layer ; }
}

get(href, options, done) {
if(!options.headers) { options.headers = {} }
options.headers["accept"] = content_type;

this.source.get(href, options, function(error, collection, headers) {
// the previous implementation of this was async, so
// it might bear reworking once figured out
// seems like you could add a layer that would try each one
// and, on failure, try the next.

if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

post(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.post(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

put(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.put(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

delete(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.delete(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}
}

export default (new InterfaceLayer())


Sample usage in the form of a 'live test':



import Client from '../src/client.js'

describe("LIVE test from ACTUAL API", () => {
test("it gets an error when calling an invalid path", () => {
new Client('http://todo-collection-json.herokuapp.com/invalid',
(error, collection) => {
expect(error).toBe(404);
expect(collection).toBeUndefined();
});
})

test('it works with a real-life-ish example', () => {
expect.assertions();

new Client('http://todo-collection-json.herokuapp.com/collection/tasks/',
(error, collection) => {
expect(collection).not.toBeDefined();
console.log(collection);
expect(error).toBeNull();
});
})
})




Original http.coffee:




module.exports = defaults =
_get: (href, options, done=()->)->
done new Error "'GET' not implemented"

_post: (href, options, done=()->)->
done new Error "'POST' not implemented"

_put: (href, options, done=()->)->
done new Error "'PUT' not implemented"

_del: (href, options, done=()->)->
done new Error "'DELETE' not implemented"

cache:
# This is a fake cache. You should probably add a real one...
put: (key, value, time, done=()->)->
if typeof time is 'function'
done = time
time = null
done()
del: (key, done=()->)->
done()
clear: (done=()->)->
done()
get: (key, done=()->)->
done()
size: (done=()->)->
done 0
memsize: (done=()->)->
done 0
debug: ()->
true

module.exports["content-type"] = "application/vnd.collection+json"

module.exports.get = (href, options, done)->
defaults.cache.get href, (error, collection)->
# Give them the cached stuff if we have it
return done error, collection if error or collection

options.headers ||= {}
options.headers["accept"] = module.exports["content-type"]

module.exports.setOptions href, options

defaults._get href, options, (error, collection, headers)->
return done error if error
performCache collection, headers
done error, collection

module.exports.post = (href, options={}, done)->

options.headers ||= {}
options.headers["accept"] = module.exports["content-type"]
options.headers["content-type"] = module.exports["content-type"]

module.exports.setOptions href, options

defaults._post href, options, (error, collection, headers)->
# Do we cache this?
done error, collection

# Should be overridden by the client
module.exports.setOptions = (href, options)->

performCache = (collection, headers)->
# Expires
# expires = response.headers.expires
# # TODO convert to time-from-now
# # Cache-Control
# # TODO implement
# defaults.cache.put response.request.href, response.body, new Date(expires).getTime() if expires



Original index.coffee:




request = require "request"
http = require "./http"

http._get = (href, options, done)->
options.url = href
request options, (error, response, body)->
done error, body, response?.headers

http._post = (href, options, done)->
options.url = href
options.body = JSON.stringify options.body
options.method = "POST"
request options, (error, response, body)->
done error, body, response?.headers

http._put = (href, options, done)->
options.url = href
options.body = JSON.stringify options.body
request.put options, (error, response)->
done error, response

http._del = (href, options, done)->
options.url = href
request.del options, (error, response)->
done error, response

module.exports = require "./client"



Questions




  • Is this a good way to separate these two concerns?

  • Is there a much cleaner/simpler way to express all this?


I'm open to input on any part of the code, but especially this http aspect.










share|improve this question
























  • We've just taken it off hold.
    – Mast
    11 hours ago













up vote
2
down vote

favorite









up vote
2
down vote

favorite











I'm rewriting collection-json.js, a client for Collection+JSON. (Collection+JSON is a HATEAOS API media type built on top of JSON.)



My Goals:




  • Remove CoffeeScript

  • webpack

  • UMD so it can be used via node and via <script ...> (not there yet.)


I'm more comfortable in Ruby, though I do know JS. Still, I wouldn't say it's my native tongue.



The current challenge is the 'http' file. I have the actual HTTP calls in one module, and then the HTTP library in another. We use different abstraction patters but I believe the original author's intent was to allow the insertion of other network layers into the HTTP client, which would then allow for things like using jquery's methods or, perhaps more importantly, for inserting test-related stubs.



Here's my current code:



import fetch from 'node-fetch';

let content_type = "application/vnd.collection+json";

class HttpImplementation {

// http_callback is used by each of the verb-methods below.
// It should call `done` with (error_string_or_null, body_as_text,
// response_headers)
//
http_callback(response, done) {
if(response.status < 400) {
// strangely, response.text() returns a promise...
response.text().then((actual_response) => {
done(null, actual_response, response.headers)
})
}
else { done(response.status); }
}

get(href, options, done) {
fetch(href).then((response) => {this.http_callback(response, done)}).
catch(error => done(error));
};

post(href, options, done) {
http_options = Object.assign({}, options,
{
method: "POST", body: JSON.stringify(options.body),
headers: options.headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(this.http_callback, done));
};

put(href, options, done) {
http_options = Object.assign({}, options,
{
method: "PUT", body: JSON.stringify(options.body),
headers: headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(this.http_callback, done));
}

delete(href, options, done){
http_options = Object.assign({}, options,
{
method: "DELETE", body: JSON.stringify(options.body),
headers: headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(http_callback, done));
};
}

class InterfaceLayer {
constructor( network_layer )
{
if( network_layer == null || network_layer == undefined )
{ this.source = new HttpImplementation(); }
else
{ this.source = network_layer ; }
}

get(href, options, done) {
if(!options.headers) { options.headers = {} }
options.headers["accept"] = content_type;

this.source.get(href, options, function(error, collection, headers) {
// the previous implementation of this was async, so
// it might bear reworking once figured out
// seems like you could add a layer that would try each one
// and, on failure, try the next.

if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

post(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.post(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

put(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.put(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

delete(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.delete(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}
}

export default (new InterfaceLayer())


Sample usage in the form of a 'live test':



import Client from '../src/client.js'

describe("LIVE test from ACTUAL API", () => {
test("it gets an error when calling an invalid path", () => {
new Client('http://todo-collection-json.herokuapp.com/invalid',
(error, collection) => {
expect(error).toBe(404);
expect(collection).toBeUndefined();
});
})

test('it works with a real-life-ish example', () => {
expect.assertions();

new Client('http://todo-collection-json.herokuapp.com/collection/tasks/',
(error, collection) => {
expect(collection).not.toBeDefined();
console.log(collection);
expect(error).toBeNull();
});
})
})




Original http.coffee:




module.exports = defaults =
_get: (href, options, done=()->)->
done new Error "'GET' not implemented"

_post: (href, options, done=()->)->
done new Error "'POST' not implemented"

_put: (href, options, done=()->)->
done new Error "'PUT' not implemented"

_del: (href, options, done=()->)->
done new Error "'DELETE' not implemented"

cache:
# This is a fake cache. You should probably add a real one...
put: (key, value, time, done=()->)->
if typeof time is 'function'
done = time
time = null
done()
del: (key, done=()->)->
done()
clear: (done=()->)->
done()
get: (key, done=()->)->
done()
size: (done=()->)->
done 0
memsize: (done=()->)->
done 0
debug: ()->
true

module.exports["content-type"] = "application/vnd.collection+json"

module.exports.get = (href, options, done)->
defaults.cache.get href, (error, collection)->
# Give them the cached stuff if we have it
return done error, collection if error or collection

options.headers ||= {}
options.headers["accept"] = module.exports["content-type"]

module.exports.setOptions href, options

defaults._get href, options, (error, collection, headers)->
return done error if error
performCache collection, headers
done error, collection

module.exports.post = (href, options={}, done)->

options.headers ||= {}
options.headers["accept"] = module.exports["content-type"]
options.headers["content-type"] = module.exports["content-type"]

module.exports.setOptions href, options

defaults._post href, options, (error, collection, headers)->
# Do we cache this?
done error, collection

# Should be overridden by the client
module.exports.setOptions = (href, options)->

performCache = (collection, headers)->
# Expires
# expires = response.headers.expires
# # TODO convert to time-from-now
# # Cache-Control
# # TODO implement
# defaults.cache.put response.request.href, response.body, new Date(expires).getTime() if expires



Original index.coffee:




request = require "request"
http = require "./http"

http._get = (href, options, done)->
options.url = href
request options, (error, response, body)->
done error, body, response?.headers

http._post = (href, options, done)->
options.url = href
options.body = JSON.stringify options.body
options.method = "POST"
request options, (error, response, body)->
done error, body, response?.headers

http._put = (href, options, done)->
options.url = href
options.body = JSON.stringify options.body
request.put options, (error, response)->
done error, response

http._del = (href, options, done)->
options.url = href
request.del options, (error, response)->
done error, response

module.exports = require "./client"



Questions




  • Is this a good way to separate these two concerns?

  • Is there a much cleaner/simpler way to express all this?


I'm open to input on any part of the code, but especially this http aspect.










share|improve this question















I'm rewriting collection-json.js, a client for Collection+JSON. (Collection+JSON is a HATEAOS API media type built on top of JSON.)



My Goals:




  • Remove CoffeeScript

  • webpack

  • UMD so it can be used via node and via <script ...> (not there yet.)


I'm more comfortable in Ruby, though I do know JS. Still, I wouldn't say it's my native tongue.



The current challenge is the 'http' file. I have the actual HTTP calls in one module, and then the HTTP library in another. We use different abstraction patters but I believe the original author's intent was to allow the insertion of other network layers into the HTTP client, which would then allow for things like using jquery's methods or, perhaps more importantly, for inserting test-related stubs.



Here's my current code:



import fetch from 'node-fetch';

let content_type = "application/vnd.collection+json";

class HttpImplementation {

// http_callback is used by each of the verb-methods below.
// It should call `done` with (error_string_or_null, body_as_text,
// response_headers)
//
http_callback(response, done) {
if(response.status < 400) {
// strangely, response.text() returns a promise...
response.text().then((actual_response) => {
done(null, actual_response, response.headers)
})
}
else { done(response.status); }
}

get(href, options, done) {
fetch(href).then((response) => {this.http_callback(response, done)}).
catch(error => done(error));
};

post(href, options, done) {
http_options = Object.assign({}, options,
{
method: "POST", body: JSON.stringify(options.body),
headers: options.headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(this.http_callback, done));
};

put(href, options, done) {
http_options = Object.assign({}, options,
{
method: "PUT", body: JSON.stringify(options.body),
headers: headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(this.http_callback, done));
}

delete(href, options, done){
http_options = Object.assign({}, options,
{
method: "DELETE", body: JSON.stringify(options.body),
headers: headers,
redirect: "follow", mode: "CORS",
credentials: "same-origin", referrer: "client",
})

return(fetch(href, http_options).then(http_callback, done));
};
}

class InterfaceLayer {
constructor( network_layer )
{
if( network_layer == null || network_layer == undefined )
{ this.source = new HttpImplementation(); }
else
{ this.source = network_layer ; }
}

get(href, options, done) {
if(!options.headers) { options.headers = {} }
options.headers["accept"] = content_type;

this.source.get(href, options, function(error, collection, headers) {
// the previous implementation of this was async, so
// it might bear reworking once figured out
// seems like you could add a layer that would try each one
// and, on failure, try the next.

if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

post(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.post(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

put(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.put(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}

delete(href, options, done) {
if (options == null) { options = {}; }
if (!options.headers) { options.headers = {}; }

options.headers = Object.assign({}, options.headers, {
"accept": content_type,
"content-type": content_type,
"Content-Type": "application/collection+json; charset=utf-8"
})

this.source.delete(href, options, function(error, collection, headers)
{
if( error || collection )
{
// if a collection is returned, the source should handle caching
// though in the original code...
// https://github.com/collection-json/collection-json.js/blob/master/lib/http.coffee#L49
return(done(error, collection, headers));
}
else
{
return(false);
}
})
}
}

export default (new InterfaceLayer())


Sample usage in the form of a 'live test':



import Client from '../src/client.js'

describe("LIVE test from ACTUAL API", () => {
test("it gets an error when calling an invalid path", () => {
new Client('http://todo-collection-json.herokuapp.com/invalid',
(error, collection) => {
expect(error).toBe(404);
expect(collection).toBeUndefined();
});
})

test('it works with a real-life-ish example', () => {
expect.assertions();

new Client('http://todo-collection-json.herokuapp.com/collection/tasks/',
(error, collection) => {
expect(collection).not.toBeDefined();
console.log(collection);
expect(error).toBeNull();
});
})
})




Original http.coffee:




module.exports = defaults =
_get: (href, options, done=()->)->
done new Error "'GET' not implemented"

_post: (href, options, done=()->)->
done new Error "'POST' not implemented"

_put: (href, options, done=()->)->
done new Error "'PUT' not implemented"

_del: (href, options, done=()->)->
done new Error "'DELETE' not implemented"

cache:
# This is a fake cache. You should probably add a real one...
put: (key, value, time, done=()->)->
if typeof time is 'function'
done = time
time = null
done()
del: (key, done=()->)->
done()
clear: (done=()->)->
done()
get: (key, done=()->)->
done()
size: (done=()->)->
done 0
memsize: (done=()->)->
done 0
debug: ()->
true

module.exports["content-type"] = "application/vnd.collection+json"

module.exports.get = (href, options, done)->
defaults.cache.get href, (error, collection)->
# Give them the cached stuff if we have it
return done error, collection if error or collection

options.headers ||= {}
options.headers["accept"] = module.exports["content-type"]

module.exports.setOptions href, options

defaults._get href, options, (error, collection, headers)->
return done error if error
performCache collection, headers
done error, collection

module.exports.post = (href, options={}, done)->

options.headers ||= {}
options.headers["accept"] = module.exports["content-type"]
options.headers["content-type"] = module.exports["content-type"]

module.exports.setOptions href, options

defaults._post href, options, (error, collection, headers)->
# Do we cache this?
done error, collection

# Should be overridden by the client
module.exports.setOptions = (href, options)->

performCache = (collection, headers)->
# Expires
# expires = response.headers.expires
# # TODO convert to time-from-now
# # Cache-Control
# # TODO implement
# defaults.cache.put response.request.href, response.body, new Date(expires).getTime() if expires



Original index.coffee:




request = require "request"
http = require "./http"

http._get = (href, options, done)->
options.url = href
request options, (error, response, body)->
done error, body, response?.headers

http._post = (href, options, done)->
options.url = href
options.body = JSON.stringify options.body
options.method = "POST"
request options, (error, response, body)->
done error, body, response?.headers

http._put = (href, options, done)->
options.url = href
options.body = JSON.stringify options.body
request.put options, (error, response)->
done error, response

http._del = (href, options, done)->
options.url = href
request.del options, (error, response)->
done error, response

module.exports = require "./client"



Questions




  • Is this a good way to separate these two concerns?

  • Is there a much cleaner/simpler way to express all this?


I'm open to input on any part of the code, but especially this http aspect.







javascript json client






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited 12 hours ago









200_success

128k15149412




128k15149412










asked 2 days ago









MustModify

17616




17616












  • We've just taken it off hold.
    – Mast
    11 hours ago


















  • We've just taken it off hold.
    – Mast
    11 hours ago
















We've just taken it off hold.
– Mast
11 hours ago




We've just taken it off hold.
– Mast
11 hours ago















active

oldest

votes











Your Answer





StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");

StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});














draft saved

draft discarded


















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f209409%2fnetwork-abstraction-layer-for-a-javascript-json-api-client%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown






























active

oldest

votes













active

oldest

votes









active

oldest

votes






active

oldest

votes
















draft saved

draft discarded




















































Thanks for contributing an answer to Code Review Stack Exchange!


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


Use MathJax to format equations. MathJax reference.


To learn more, see our tips on writing great answers.





Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


Please pay close attention to the following guidance:


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f209409%2fnetwork-abstraction-layer-for-a-javascript-json-api-client%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

Morgemoulin

Scott Moir

Souastre