Skip to content

Instantly share code, notes, and snippets.

@aseemk
Created April 27, 2012 05:51
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aseemk/2506270 to your computer and use it in GitHub Desktop.
Save aseemk/2506270 to your computer and use it in GitHub Desktop.
Simplified wrapper around Node's native 'url' module
# url.coffee
# by Aseem Kishore ( https://github.com/aseemk ), under MIT license
#
# A simplified wrapper around the native 'url' module that returns "live"
# objects: updates to properties are reflected in related properties. E.g.
# updates to the `port` property will be reflected on the `host` property.
#
# The public API and behavior are pretty close to the native 'url' module's:
# http://nodejs.org/docs/latest/api/url.html Currently lacking / known issues:
#
# - No support for `parseQueryString`; `query` can only be a string.
# Similarly, no support for `slashesDenoteHost`.
#
# - Parsing of URLs beyond the initial parse (which uses the native 'url'
# module) isn't thorough or strict right now. It assumes plenty.
#
# This is mainly just a simple and easy way to mutate/tweak URLs (e.g. to
# change a URL's port). Feedback and patches welcome! Thanks. =)
NativeURL = require 'url'
class URL
# Private constructor:
constructor: (href) ->
@_reparse href
# Language helpers:
get = (props) =>
@::__defineGetter__ name, getter for name, getter of props
set = (props) =>
@::__defineSetter__ name, setter for name, setter of props
# Public properties:
get href: ->
# Postfix http/https/ftp/gopher/file protocols (trailing colon) with
# colon-slash-slash; all others just colon:
protocol = @protocol
protocol += '//' if protocol.replace(/:$/, '') in ['http', 'https', 'ftp', 'gopher', 'file']
# Add auth via @ if needed:
auth = @auth
auth += '@' if auth
# Finally:
"#{protocol}#{auth}#{@host}#{@pathname}#{@search}#{@hash}"
set href: (val='') ->
@_reparse val
get protocol: -> @_protocol
set protocol: (val='') ->
# Lowercase, and ensure trailing colon:
@_protocol = val.toLowerCase().replace(/:$/, '') + ':'
# WARNING: The latest Node docs are inaccurate on host. They say host
# includes auth, but it doesn't in practice:
# https://github.com/joyent/node/issues/1626#issuecomment-5373917
get host: ->
host = @hostname
host = [host, @port].join ':' if @port
host
set host: (val='') ->
[@hostname, @port] = val.split ':'
get auth: -> @_auth
set auth: (@_auth='') ->
get hostname: -> @_hostname
set hostname: (@_hostname='') ->
get port: -> @_port
set port: (@_port='') ->
get pathname: -> @_pathname
set pathname: (val='') ->
# Ensure trailing slash:
@_pathname = '/' + val.replace(/^\//, '')
get search: ->
# Leading question mark if there's a query string:
@query and "?#{@query}"
set search: (val='') ->
# Ensure leading question mark / strip it when assigning to @query:
@_query = val.replace(/^\?/, '')
get path: ->
"#{@pathname}#{@search}"
set path: (val='') ->
[@pathname, @query] = val.split '?'
get query: -> @_query
set query: (@_query='') ->
get hash: -> @_hash
set hash: (val='') ->
# Ensure leading anchor:
@_hash = '#' + val.replace(/^#/, '')
# Private methods:
# Reparses the given URL and updates this instance in-place:
_reparse: (href) ->
# Save all properties as private(-ish) properties so that we can
# validate/adjust updates to them, e.g. add trailing colons.
# NOTE: All properties will be present -- missing ones will be the
# empty string. This is consistent with browsers, but not w/ Node's
# native 'url' module. That's fine hopefully?
for prop, value of NativeURL.parse href
@["_#{prop}"] = value or ''
# Public methods:
equals: (other) ->
@href is other.href
toString: ->
@href
@parse: (str) ->
new URL str
@format: (url) ->
# Don't assume the given param will be an instance of this class:
NativeURL.format url
@resolve: (from, to) ->
NativeURL.resolve from, to
# Export static functions only:
for name, func of URL when typeof func is 'function'
exports[name] = func
assert = require 'assert'
NativeURL = require 'url'
URL = require './url'
# Helper func for testing all getters:
test = (url, props) ->
# Ignore 'slashes' property; we don't implement that yet.
for prop, expected of props when prop isnt 'slashes'
assert.equal url[prop], expected, """
url.#{prop}:
expected: #{JSON.stringify expected}
actual: #{JSON.stringify url[prop]}
"""
# Test case: a full URL, straight from the Node docs!
# http://nodejs.org/docs/latest/api/url.html
STR = 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'
exp = NativeURL.parse STR
# Test initial parse:
url = URL.parse STR
test url, exp
# Test protocol set w/out trailing colon:
url.protocol = 'https'
exp.protocol = 'https:'
exp.href = exp.href.replace 'http:', 'https:'
test url, exp
# Test auth set:
url.auth = 'usr:pwd'
exp.auth = 'usr:pwd'
exp.href = exp.href.replace 'user:pass', 'usr:pwd'
test url, exp
# Test host set, clearing port:
url.host = 'example.com'
exp.host = 'example.com'
exp.port = ''
exp.hostname = 'example.com'
exp.href = exp.href.replace 'host.com:8080', 'example.com'
test url, exp
# Test hostname set:
url.hostname = 'foobar.net'
exp.hostname = 'foobar.net'
exp.host = 'foobar.net'
exp.href = exp.href.replace 'example.com', 'foobar.net'
test url, exp
# Test port set:
url.port = '1234'
exp.port = '1234'
exp.host = 'foobar.net:1234'
exp.href = exp.href.replace 'foobar.net', 'foobar.net:1234'
test url, exp
# Test path set w/out leading slash, clearing query string:
url.path = 'hello/world'
exp.path = '/hello/world'
exp.pathname = '/hello/world'
exp.search = ''
exp.query = ''
exp.href = exp.href.replace '/p/a/t/h?query=string', '/hello/world'
test url, exp
# Test pathname set w/out leading slash:
url.pathname = 'foo/bar'
exp.pathname = '/foo/bar'
exp.path = exp.path.replace '/hello/world', '/foo/bar'
exp.href = exp.href.replace '/hello/world', '/foo/bar'
test url, exp
# Test search set w/out leading question mark:
url.search = 'a=b'
exp.search = '?a=b'
exp.query = 'a=b'
exp.path = exp.path + '?a=b'
exp.href = exp.href.replace '/foo/bar', exp.path
test url, exp
# Test query set -- w/ (double) question mark:
# TODO Is this actually legitimate/correct?? It works...
url.query = '?foo=bar'
exp.query = '?foo=bar'
exp.search = '??foo=bar'
exp.path = exp.path.replace '?a=b', '??foo=bar'
exp.href = exp.href.replace '?a=b', '??foo=bar'
test url, exp
# Test hash set w/out leading anchor:
url.hash = 'baz'
exp.hash = '#baz'
exp.href = exp.href.replace '#hash', '#baz'
test url, exp
# Test hash clear:
url.hash = ''
exp.hash = '#'
exp.href = exp.href.replace '#baz', '#'
test url, exp
# Test search clear:
# TODO Should we be testing query clear too? Should question mark remain then?
url.search = ''
exp.search = ''
exp.query = ''
exp.path = exp.path.replace '??foo=bar', ''
exp.href = exp.href.replace '??foo=bar', ''
test url, exp
# Test path clear:
url.path = ''
exp.path = '/'
exp.pathname = '/'
exp.href = exp.href.replace '/foo/bar', '/'
test url, exp
# Test port clear:
url.port = ''
exp.port = ''
exp.host = exp.host.replace ':1234', ''
exp.href = exp.href.replace ':1234', ''
test url, exp
# Test auth clear:
url.auth = ''
exp.auth = ''
exp.href = exp.href.replace 'usr:pwd@', ''
test url, exp
# Finally, test set href:
url.href = STR
exp = NativeURL.parse STR
test url, exp
console.log 'All tests passed!'
@44203
Copy link

44203 commented Jan 10, 2014

this is a really clever hack for native looking getters/setters!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment