├── .coveralls.yml ├── browser.js ├── .travis.yml ├── index.js ├── .gitignore ├── lib ├── fields-to-keep.js └── url-assembler-factory.js ├── test ├── 000-module.js ├── 200-requests.js ├── 110-instance-with-baseurl.js └── 100-instance.js ├── package.json └── README.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: c7YrIDeLfpfZC7uS6LRAOTmzEyhaCCrOE 2 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/url-assembler-factory')(); 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - '4' 5 | - '6' 6 | - '8' 7 | - '10' 8 | after_script: 9 | - npm run coveralls 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var request; 2 | try { 3 | request = require('request'); 4 | } catch(e) {} 5 | module.exports = require('./lib/url-assembler-factory')(request) 6 | exports.default = module.exports 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw[a-z] 2 | .*.un~ 3 | .tern-port 4 | coverage 5 | 6 | node_modules 7 | 8 | lib-cov 9 | .nyc_output 10 | *.seed 11 | *.log 12 | *.csv 13 | *.dat 14 | *.out 15 | *.pid 16 | *.gz 17 | 18 | pids 19 | logs 20 | results 21 | 22 | npm-debug.log 23 | -------------------------------------------------------------------------------- /lib/fields-to-keep.js: -------------------------------------------------------------------------------- 1 | 2 | var urlFieldsToKeep = [ 3 | 'protocol', 4 | 'slashes', 5 | 'auth', 6 | 'host', 7 | 'port', 8 | 'hostname', 9 | 'hash', 10 | 'search', 11 | //'query', 12 | 'pathname', 13 | 'path', 14 | 'href' 15 | ]; 16 | 17 | module.exports = function selectUrlFields (assembler) { 18 | return urlFieldsToKeep.reduce(function(value, field) { 19 | value[field] = assembler[field] 20 | return value; 21 | }, {}) 22 | } 23 | -------------------------------------------------------------------------------- /test/000-module.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | 3 | describe('the module', function () { 4 | it('is requirable', function () { 5 | require('../'); 6 | }); 7 | it('is a function', function () { 8 | expect(require('../')).to.be.a('function'); 9 | }); 10 | 11 | it('is a constructor without new', function () { 12 | var UrlAssembler = require('../'); 13 | var myUrl = UrlAssembler('/hello'); 14 | expect(myUrl).to.be.an.instanceof(UrlAssembler); 15 | }); 16 | }) 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url-assembler", 3 | "version": "2.1.1", 4 | "description": "Assemble urls from route-like templates (/path/:param)", 5 | "main": "index.js", 6 | "browser": "browser.js", 7 | "scripts": { 8 | "test": "nyc -r text -r html -r lcov mocha -R spec test", 9 | "coveralls": "cat coverage/lcov.info | coveralls" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/Floby/node-url-assembler.git" 14 | }, 15 | "keywords": [ 16 | "url", 17 | "builder", 18 | "urlBuilder", 19 | "url-builder", 20 | "template", 21 | "assemble", 22 | "assembler", 23 | "route", 24 | "routing", 25 | "parameters", 26 | "query-builder", 27 | "query" 28 | ], 29 | "author": "Florent Jaby ", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/Floby/node-url-assembler/issues" 33 | }, 34 | "homepage": "https://github.com/Floby/node-url-assembler", 35 | "devDependencies": { 36 | "chai": "^1.10.0", 37 | "coveralls": "^3.0.2", 38 | "mocha": "^5.2.0", 39 | "nyc": "^14.1.1", 40 | "proxyquire": "^1.4.0", 41 | "sinon": "^1.14.1" 42 | }, 43 | "dependencies": { 44 | "extend": "^2.0.2", 45 | "qs": "^6.5.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/200-requests.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'); 2 | var proxyquire = require('proxyquire').noCallThru(); 3 | var expect = require('chai').expect 4 | var assert = require('chai').assert 5 | 6 | describe('if the request module is found', function () { 7 | var requestMock, UrlAssembler; 8 | var myUrl, defaulted; 9 | beforeEach(function () { 10 | defaulted = {}; 11 | requestMock = { 12 | defaults: sinon.stub().returns(defaulted) 13 | }; 14 | UrlAssembler = proxyquire('../', { request: requestMock }); 15 | myUrl = UrlAssembler('http://some.thing/hello'); 16 | }) 17 | 18 | describe('an instance', function () { 19 | it('has a "request" property', function () { 20 | expect(myUrl).to.have.property('request'); 21 | }) 22 | 23 | describe('the .request property', function () { 24 | it('returns a request object which defaults to the current URL', function () { 25 | expect(myUrl.request).to.equal(defaulted); 26 | assert(requestMock.defaults.calledWith({ uri: 'http://some.thing/hello' })); 27 | }); 28 | 29 | it('can be set to an already defaulted version of request', function () { 30 | var myRequest = {defaults: sinon.stub().returns(defaulted)}; 31 | myUrl.request = myRequest; 32 | expect(myUrl.request).to.equal(defaulted); 33 | assert(myRequest.defaults.calledWith({ uri: 'http://some.thing/hello' }), 'defaults are not reused'); 34 | }); 35 | 36 | describe('on an child instance', function () { 37 | it('uses the same defaults as its parent', function () { 38 | var myRequest = {defaults: sinon.stub().returns(defaulted)}; 39 | //var myRequest = {coucu: 8}; 40 | myUrl.request = myRequest; 41 | expect(myUrl.segment('/something').request).to.equal(defaulted); 42 | assert(myRequest.defaults.calledWith({ uri: 'http://some.thing/hello/something' }), 'defaults are not reused'); 43 | }); 44 | }); 45 | }); 46 | 47 | }); 48 | }); 49 | 50 | describe('if the request module is NOT found', function () { 51 | var UrlAssembler = proxyquire('../', { request: null }); 52 | 53 | describe('an instance', function () { 54 | var myUrl = UrlAssembler('http://some.thing/hello'); 55 | it('throws when trying to access the .request property', function () { 56 | expect(function () { 57 | myUrl.request 58 | }).to.throw(Error); 59 | }) 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/110-instance-with-baseurl.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | var UrlAssembler = require('../') 3 | var Url = require('url') 4 | 5 | describe('an instance with a baseUrl', function () { 6 | var myUrl; 7 | beforeEach(function () { 8 | myUrl = UrlAssembler('http://hello.com:8989/api'); 9 | }); 10 | 11 | describe('With no template', function () { 12 | describe('.query(key, value)', function () { 13 | it('add the query parameter directly after a /', function () { 14 | expect(myUrl.query('hello', 'world').toString()).to.equal('http://hello.com:8989/api?hello=world'); 15 | }); 16 | }); 17 | }); 18 | 19 | describe('given a template with no parameters', function () { 20 | beforeEach(function () { 21 | myUrl = myUrl.template('/hello/world'); 22 | }); 23 | 24 | it('adds it to the pathname of the url', function () { 25 | expect(myUrl.toString()).to.equal('http://hello.com:8989/api/hello/world') 26 | }); 27 | 28 | describe('.prefix(prefix)', function () { 29 | it('adds a prefix in addition to the exsting one', function () { 30 | expect(myUrl.prefix('/v2').toString()).to.equal('http://hello.com:8989/api/v2/hello/world'); 31 | }) 32 | }) 33 | }); 34 | 35 | describe('when the baseUrl stops at the port number', function () { 36 | beforeEach(function () { 37 | myUrl = UrlAssembler('http://domain.com:90'); 38 | }) 39 | 40 | it('does not double slash the result', function () { 41 | expect(myUrl.segment('/hello').toString()).to.equal('http://domain.com:90/hello'); 42 | }) 43 | }); 44 | 45 | describe('when passed to url.format', function () { 46 | beforeEach(function () { 47 | myUrl = myUrl.prefix('/v4').segment('/users/:user').segment('/rights'); 48 | }); 49 | it('should give the same output as toString()', function () { 50 | var url = require('url'); 51 | myUrl.param('user', 'floby'); 52 | var expected = myUrl.toString(); 53 | expect(url.format(myUrl)).to.equal(expected); 54 | }); 55 | }) 56 | 57 | describe('when baseUrl has a query param', function () { 58 | beforeEach(function () { 59 | myUrl = UrlAssembler('http://domain.com/coucou?hello=world'); 60 | }); 61 | 62 | it('should keep the query param', function () { 63 | expect(myUrl.query('a', 'b').toString()).to.equal('http://domain.com/coucou?hello=world&a=b'); 64 | }); 65 | 66 | }); 67 | 68 | describe("when baseUrl has a repeated query param with config", function() { 69 | beforeEach(function() { 70 | myUrl = UrlAssembler("http://domain.com/coucou?hello=world"); 71 | }); 72 | 73 | it("should keep accept qs config options", function() { 74 | expect( 75 | myUrl 76 | .qsConfig({ 77 | arrayFormat: "repeat" 78 | }) 79 | .query("a", ["b", "c"]) 80 | .toString() 81 | ).to.equal("http://domain.com/coucou?hello=world&a=b&a=c"); 82 | }); 83 | }); 84 | 85 | describe('when used with special characters', function() { 86 | 87 | it('should encode them in the final URL (with template)', function() { 88 | var expected = 'http://www.canal.com:8989' 89 | + '/pl%C3%BBs' 90 | + '/CARA%C3%8FBES/m%C3%A9dia/Bouquet%20p%C3%A8re' 91 | + '?now=2014-05-27T03%3A59%3A59%2B00%3A00&f%C3%B6%C3%B6=b%20a%20r' 92 | myUrl = UrlAssembler('http://www.canal.com:8989') 93 | .prefix('/plûs') 94 | .template('/:zone/média/:media') 95 | .param({'media': 'Bouquet père', 'zone': 'CARAÏBES'}) 96 | .query({now: '2014-05-27T03:59:59+00:00', föö: "b a r"}); 97 | expect(myUrl.toString()).to.equal(expected); 98 | expect(Url.format(myUrl)).to.equal(expected) 99 | }); 100 | 101 | it('should encode them in the final URL (with segment)', function() { 102 | var expected = 'http://www.canal.com:8989' 103 | + '/pl%C3%BBs' 104 | + '/CARA%C3%8FBES/m%C3%A9dia/Bouquet%20p%C3%A8re' 105 | + '?now=2014-05-27T03%3A59%3A59%2B00%3A00&f%C3%B6%C3%B6=b%20a%20r' 106 | myUrl = UrlAssembler('http://www.canal.com:8989') 107 | .prefix('/plûs') 108 | .segment('/:zone/média/:media') 109 | .param({'media': 'Bouquet père', 'zone': 'CARAÏBES'}) 110 | .query({now: '2014-05-27T03:59:59+00:00', föö: "b a r"}); 111 | expect(myUrl.toString()).to.equal(expected); 112 | expect(Url.format(myUrl)).to.equal(expected) 113 | }); 114 | 115 | it('should encode them in the final URL (with param)', function() { 116 | var expected = 'http://example.com' 117 | + "/search/-_.!~*'()%20%2F%3B%2C%3F%3A%40%26%3D%2B%24_abc_%E6%97%A5%E6%9C%AC%E8%AA%9E" 118 | myUrl = UrlAssembler('http://example.com') 119 | .segment('/search/:p') 120 | .param({ 121 | 'p': "-_.!~*'() /;,?:@&=+$_abc_日本語" 122 | }); 123 | expect(myUrl.toString()).to.equal(expected); 124 | expect(Url.format(myUrl)).to.equal(expected) 125 | }); 126 | }); 127 | 128 | }); 129 | -------------------------------------------------------------------------------- /lib/url-assembler-factory.js: -------------------------------------------------------------------------------- 1 | var extend = require('extend'); 2 | var url = require('url'); 3 | var qs = require('qs'); 4 | var selectUrlFields = require('./fields-to-keep'); 5 | 6 | module.exports = function (request) { 7 | 8 | function UrlAssembler (baseUrlOrUrlAssembler) { 9 | if(!(this instanceof UrlAssembler)) { 10 | return new UrlAssembler(baseUrlOrUrlAssembler); 11 | } 12 | 13 | var query = {}; 14 | //For future to keep other configs 15 | this._config = { 16 | qsConfig: {} 17 | }; 18 | this._query = addQueryParamTo(query); 19 | this._prefix = ''; 20 | this.pathname = ''; 21 | this.getParsedQuery = clone.bind(null, query); 22 | 23 | Object.defineProperty(this, '_requestModule', { value: request, writable: true }); 24 | 25 | if (baseUrlOrUrlAssembler instanceof UrlAssembler) { 26 | initWithInstance(this, baseUrlOrUrlAssembler); 27 | } else if (baseUrlOrUrlAssembler) { 28 | initWithBaseUrl(this, baseUrlOrUrlAssembler); 29 | } 30 | } 31 | 32 | function initWithBaseUrl (self, baseUrl) { 33 | extend(self, selectUrlFields(url.parse(baseUrl))); 34 | self._prefix = self.pathname; 35 | if(self._prefix === '/') { 36 | self._prefix = ''; 37 | self.pathname = ''; 38 | } 39 | if(self.search && self.search.length > 1) { 40 | var parsedQuery = qs.parse(self.search.substr(1)) 41 | self._query(parsedQuery) 42 | } 43 | } 44 | 45 | function initWithInstance (self, instance) { 46 | extend(self, selectUrlFields(instance)); 47 | self._prefix = instance._prefix; 48 | self._config = instance._config; 49 | self._query(instance.getParsedQuery()); 50 | self._requestModule = instance._requestModule; 51 | } 52 | 53 | var methods = UrlAssembler.prototype; 54 | 55 | methods._chain = function () { 56 | return new this.constructor(this); 57 | }; 58 | 59 | methods.template = function (fragment) { 60 | var chainable = this._chain(); 61 | chainable.pathname = this._prefix + encodeURI(fragment); 62 | return chainable; 63 | }; 64 | 65 | methods.qsConfig = function(config) { 66 | var chainable = this._chain(); 67 | extend(chainable._config.qsConfig, config); 68 | return chainable; 69 | }; 70 | 71 | methods.segment = function (segment) { 72 | var chainable = this._chain(); 73 | chainable.pathname = this.pathname + encodeURI(segment); 74 | return chainable; 75 | }; 76 | 77 | methods.toString = function toString () { 78 | return url.format(this); 79 | }; 80 | methods.valueOf = methods.toString; 81 | methods.toJSON = methods.toString; 82 | 83 | methods.query = function (param, value) { 84 | var chainable = this._chain(); 85 | chainable._query(param, value); 86 | return chainable; 87 | }; 88 | 89 | methods.prefix = function prefix (prefix) { 90 | var chainable = this._chain(); 91 | var pathToKeep = this.pathname.substr(this._prefix.length); 92 | chainable._prefix = this._prefix + encodeURI(prefix); 93 | chainable.pathname = chainable._prefix + pathToKeep; 94 | return chainable; 95 | }; 96 | 97 | methods.param = function param (key, value, strict) { 98 | if (typeof key === 'object') { 99 | return _multiParam(this, key, (value === true)); 100 | } 101 | var chainable = this._chain(); 102 | var previous = this.pathname; 103 | var symbol = ':' + key; 104 | chainable.pathname = this.pathname.replace(symbol, encodeURIComponent(value)); 105 | if (!strict && chainable.pathname === previous) { 106 | return chainable.query(key, value); 107 | } 108 | return chainable; 109 | }; 110 | 111 | function _multiParam (chainable, hash, strict) { 112 | for (var key in hash) { 113 | chainable = chainable.param(key, hash[key], strict); 114 | } 115 | return chainable; 116 | } 117 | 118 | function addQueryParamTo (query) { 119 | return function addQueryParam(key, value) { 120 | if(!value && typeof key === 'object') { 121 | addManyParameters(key); 122 | } else { 123 | addOneParameter(key, value) 124 | } 125 | this.search = qs.stringify(query, this._config.qsConfig); 126 | } 127 | 128 | function addManyParameters (hash) { 129 | for (var key in hash) { 130 | if (nullOrUndef(hash[key])) delete hash[key]; 131 | } 132 | extend(true, query, hash); 133 | } 134 | 135 | function addOneParameter (key, value) { 136 | if (!nullOrUndef(value)) { 137 | query[key] = value; 138 | } 139 | } 140 | } 141 | 142 | Object.defineProperty(UrlAssembler.prototype, 'request', { 143 | get: function () { 144 | var request = this._requestModule; 145 | if (request) { 146 | return request.defaults({ uri: this.toString() }); 147 | } else { 148 | throw Error('the "request" module was not found. You must have it installed to use this property'); 149 | } 150 | }, 151 | set: function (newRequest) { 152 | return this._requestModule = newRequest; 153 | } 154 | }); 155 | 156 | function nullOrUndef (value) { 157 | return value === null || typeof value === 'undefined'; 158 | } 159 | 160 | function clone (obj) { 161 | return extend(true, {}, obj); 162 | } 163 | return UrlAssembler; 164 | } 165 | -------------------------------------------------------------------------------- /test/100-instance.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | var UrlAssembler = require('../') 3 | 4 | describe('an instance with no baseUrl', function () { 5 | var myUrl; 6 | beforeEach(function () { 7 | myUrl = UrlAssembler(); 8 | }); 9 | 10 | describe('given no template', function () { 11 | describe('.prefix(prefix)', function () { 12 | it('should add the given prefix at the beginning of the URL', function () { 13 | expect(myUrl.prefix('/hello').toString()).to.equal('/hello'); 14 | }); 15 | }); 16 | }); 17 | 18 | describe('given a template with no parameters', function () { 19 | beforeEach(function () { 20 | myUrl = myUrl.template('/hello'); 21 | }); 22 | describe('.toString()', function () { 23 | it('returns the template', function () { 24 | expect(myUrl.toString()).to.equal('/hello'); 25 | }); 26 | }); 27 | 28 | describe('.valueOf()', function () { 29 | it('returns the template', function () { 30 | expect(myUrl.valueOf()).to.equal('/hello'); 31 | }) 32 | }); 33 | 34 | describe('.toJSON()', function () { 35 | it('returns the assembled string', function () { 36 | expect(myUrl.toJSON()).to.equal(myUrl.toString()); 37 | }); 38 | }); 39 | 40 | describe('.prefix(prefix)', function () { 41 | it('adds a prefix to the result of toString()', function () { 42 | expect(myUrl.prefix('/coucou').toString()).to.equal('/coucou/hello'); 43 | }); 44 | }); 45 | 46 | describe('.param(key, value)', function () { 47 | it('adds the parameter as a query parameter', function () { 48 | expect(myUrl.param('a', 'bc').toString()).to.equal('/hello?a=bc'); 49 | }); 50 | }); 51 | 52 | describe('.param(key, value, true)', function () { 53 | it('does not add the parameter as query parameter', function () { 54 | expect(myUrl.param('a', 'bc', true).toString()).to.equal('/hello'); 55 | }); 56 | }); 57 | 58 | describe('.query(key, value)', function () { 59 | it('add the parameter as a query parameter', function () { 60 | expect(myUrl.query('param', 12345).toString()).to.equal('/hello?param=12345'); 61 | }) 62 | 63 | it('does not add the query parameter if it has a null value', function () { 64 | expect(myUrl.query('param', null).toString()).to.equal('/hello'); 65 | }) 66 | 67 | it('keeps the query parameter if it has a falsy yet correct value', function () { 68 | expect(myUrl.query('param', 0).toString()).to.equal('/hello?param=0'); 69 | }); 70 | }); 71 | 72 | describe('.query({key: value})', function () { 73 | it('adds each of it to the query string', function () { 74 | myUrl = myUrl.query({ 75 | 'hello': 'goodbye', 76 | 'one': 1 77 | }) 78 | expect(myUrl.toString()).to.equal('/hello?hello=goodbye&one=1'); 79 | }) 80 | 81 | it('does not add the query parameters which are null', function () { 82 | myUrl = myUrl.query({ 83 | 'hello': 'goodbye', 84 | 'one': null, 85 | 'goodbye': 'hello' 86 | }) 87 | expect(myUrl.toString()).to.equal('/hello?hello=goodbye&goodbye=hello'); 88 | }); 89 | 90 | it('keeps falsy values if they are correct', function () { 91 | myUrl = myUrl.query({ 92 | 'hello': 'goodbye', 93 | 'two': '', 94 | 'three': 0, 95 | 'goodbye': 'hello' 96 | }) 97 | expect(myUrl.toString()).to.equal('/hello?hello=goodbye&two=&three=0&goodbye=hello'); 98 | }); 99 | 100 | describe('when some query param have already been set', function () { 101 | beforeEach(function () { 102 | myUrl = myUrl.query('yes', 'no'); 103 | }) 104 | it('keeps the previously set query params', function () { 105 | myUrl = myUrl.query({ 106 | 'hello': 'goodbye', 107 | 'one': 1 108 | }) 109 | expect(myUrl.toString()).to.equal('/hello?yes=no&hello=goodbye&one=1') 110 | }); 111 | }) 112 | }) 113 | }); 114 | 115 | describe('given a template with a simple parameter', function () { 116 | beforeEach(function () { 117 | myUrl = UrlAssembler('/path/:myparam'); 118 | }); 119 | describe('.param(key, value)', function () { 120 | it('replaces the parameter in the template', function () { 121 | expect(myUrl.param('myparam', 'hello').toString()).to.equal('/path/hello'); 122 | }) 123 | }); 124 | 125 | describe('.segment(subpath)', function () { 126 | beforeEach(function () { 127 | myUrl = myUrl 128 | .param('myparam', 'hello') 129 | .segment('/another/:parameter'); 130 | }) 131 | it('adds the given parametrized segment at the end of the path', function () { 132 | expect(myUrl.param('parameter', 8000).toString()).to.equal('/path/hello/another/8000'); 133 | }); 134 | }); 135 | }); 136 | 137 | describe('given segments with multiple parameters', function () { 138 | beforeEach(function () { 139 | myUrl = myUrl 140 | .segment('/groups/:group') 141 | .segment('/users/:user'); 142 | }) 143 | describe('.param({...})', function () { 144 | it('replace the correct value for each parameter', function () { 145 | var actual = myUrl.param({ 146 | group: 'A', 147 | user: 9 148 | }).toString(); 149 | expect(actual).to.equal('/groups/A/users/9'); 150 | }); 151 | 152 | it('puts parameters which are not substituted in the path to query params', function () { 153 | var actual = myUrl.param({ 154 | group: 'A', 155 | user: 9, 156 | something: 'else' 157 | }).toString(); 158 | expect(actual).to.equal('/groups/A/users/9?something=else'); 159 | }); 160 | }) 161 | 162 | describe('.param({...}, true)', function () { 163 | it('does not put unused parameters in query params', function () { 164 | var actual = myUrl.param({ 165 | group: 'A', 166 | user: 9, 167 | something: 'else' 168 | }, true).toString(); 169 | expect(actual).to.equal('/groups/A/users/9'); 170 | }); 171 | }); 172 | 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][travis-image]][travis-url] [![Coverage][coveralls-image]][coveralls-url] 2 | 3 | node-url-assembler 4 | ================== 5 | 6 | > Assemble urls from route-like templates (/path/:param) 7 | 8 | Chainable utility to assemble URLs from templates 9 | 10 | Installation 11 | ------------ 12 | 13 | npm install --save url-assembler 14 | 15 | Usage 16 | ----- 17 | 18 | #### Basic 19 | 20 | ```javascript 21 | UrlAssembler() 22 | .template('/users/:user') 23 | .param('user', 8) 24 | .param('include', 'address') 25 | .query({ 26 | some: 'thing', 27 | other: 1234 28 | }) 29 | .toString() // => "/users/8?include=address&some=thing&other=1234 30 | ``` 31 | 32 | #### With base URL 33 | 34 | Since you more often than not need a hostname and a protocol to go with this 35 | 36 | ```javascript 37 | UrlAssembler('http://my.domain.com:9000') 38 | .template('/groups/:group/users/:user') 39 | .param({ 40 | group: 'admin', 41 | user: 'floby' 42 | }) 43 | .toString() // => "http://my.domain.9000/groups/admin/users/floby" 44 | ``` 45 | 46 | #### Incremental assembling 47 | 48 | You can also incrementally build your URL. 49 | 50 | ```javascript 51 | UrlAssembler('https://api.site.com/') 52 | .prefix('/v2') 53 | .segment('/users/:user') 54 | .segment('/projects/:project') 55 | .segment('/summary') 56 | .param({ 57 | user: 'floby', 58 | project: 'node-url-assembler' 59 | }) 60 | .toString() // => 'https://api.site.com/v2/users/floby/projects/node-url-assembler/summary' 61 | ``` 62 | 63 | #### making requests 64 | 65 | If `url-assembler` finds the [`request`](npmjs.com/package/request) module. Then a `.request` property 66 | is available on every instance which can be used to make requests. 67 | 68 | ```javascript 69 | var google = UrlAssembler('https://google.com').query('q', 'url assembler'); 70 | 71 | google.request.get() // => makes a GET request to google 72 | 73 | // you can still pass any other option to request 74 | google.request.get({json: true}) 75 | ``` 76 | 77 | Design 78 | ------ 79 | 80 | Every method (except `toString()`) returns a new instance of `UrlAssembler`. You can 81 | consider that `UrlAssembler` instances are immutable. 82 | 83 | Because of this, you can use a single instance as a preconfigured url to reuse throughout your codebase. 84 | 85 | ```javascript 86 | var api = UrlAssembler('http://api.site.com'); 87 | 88 | var userResource = api.segment('/users/:user'); 89 | 90 | var userV1 = userResource.prefix('/v1'); 91 | var userV2 = userResource.prefix('/v2'); 92 | 93 | var userFeedResource = userV2.segment('/feed'); 94 | 95 | var authenticated = api.query('auth_token', '123457890'); 96 | 97 | var adminResource = authenticated.segment('/admin'); 98 | ``` 99 | 100 | In addition, an instance of `UrlAssembler` is a valid object to pass 101 | to `url.format()` or any function accepting this kind of object as 102 | parameter. 103 | 104 | 105 | API Reference 106 | ------------- 107 | 108 | ###### `new UrlAssembler([baseUrl])` 109 | - `baseUrl`: will be used for protocol, hostname, port and other base url kind of stuff. 110 | - **returns** an instance of a URL assembler. 111 | 112 | ###### `new UrlAssembler(urlAssembler)` 113 | - `urlAssembler`: an existing instance of `UrlAssembler` 114 | - this constructor is used for chaining internally. You should be aware of it if you extend `UrlAssembler` 115 | - **returns** a new instance of a URL assembler, copying the previous one 116 | 117 | ###### `.template(template)` 118 | - `template` a *string* with dynamic part noted as `:myparam` . For example `'/hello/:param/world'` 119 | - **returns** a new instance of `UrlAssembler` with this template configured 120 | 121 | ###### `.prefix(subPath)` 122 | - `subPath` : this *string* will be added at the beginning of the path part of the URL 123 | - if called several times, the `subPath` will be added after the previous prefix but before the rest of the path 124 | - **returns** a new instance of `UrlAssembler` 125 | 126 | ###### `.segment(subPathTemplate)` 127 | - `subPathTemplate` is a *string* of a segment to add to the path of the URL. It can have a templatized parameter eg. `'/user/:user'` 128 | - if called several times, the segment will be added at the end of the URL. 129 | - **returns** a new instance of `UrlAssembler` 130 | 131 | ###### `.param(key, value[, strict])` 132 | - `key`: a *string* of the dynamic part to replace 133 | - `value`: a *string* to replace the dynamic part with 134 | - **returns** a new instance of `UrlAssembler` with the parameter `key` replaced with `value`. 135 | If `strict` is falsy, the key will be added as query parameter. 136 | 137 | ###### `.param(params[, strict])` 138 | - `params`: a *hash* of key/value to give to the method above 139 | - `strict` a flag passed to the method above 140 | - **returns** a new instance of `UrlAssembler` with all the parameters from the `params` replaced 141 | 142 | ###### `.query(key, value)` 143 | - `key`: the name of the parameter to configure 144 | - `value`: the value of the parameter to configure 145 | - **returns** a new instance of `UrlAssembler` with the `key=value` pair added as 146 | query parameter with the [`qs`](https://www.npmjs.com/package/qs) module. 147 | 148 | ###### `.query(params)` 149 | - shortcut for the previous method with a hash of key/value. 150 | 151 | ###### `.qsConfig(config)` 152 | 153 | - add config supported by qs.stringify https://www.npmjs.com/package/qs version ^6.5.1 154 | 155 | 156 | ###### `.toString()`, `.valueOf()`, `toJSON()` 157 | - **returns** a *string* of the current state of the `UrlAssembler` instance. Path parameters not yet replaced will appear as `:param_name`. 158 | 159 | Test 160 | ---- 161 | 162 | Tests are written with [mocha][mocha-url] and covered with [istanbul][istanbul-url] 163 | You can run the tests with `npm test`. 164 | 165 | Contributing 166 | ------------ 167 | 168 | Anyone is welcome to submit issues and pull requests 169 | 170 | 171 | License 172 | ------- 173 | 174 | [MIT](http://opensource.org/licenses/MIT) 175 | 176 | Copyright (c) 2015 Florent Jaby 177 | 178 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 179 | 180 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 181 | 182 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 183 | 184 | 185 | [travis-image]: http://img.shields.io/travis/Floby/node-url-assembler/master.svg?style=flat 186 | [travis-url]: https://travis-ci.org/Floby/node-url-assembler 187 | [coveralls-image]: http://img.shields.io/coveralls/Floby/node-url-assembler/master.svg?style=flat 188 | [coveralls-url]: https://coveralls.io/r/Floby/node-url-assembler 189 | [mocha-url]: https://github.com/visionmedia/mocha 190 | [istanbul-url]: https://github.com/gotwarlost/istanbul 191 | --------------------------------------------------------------------------------