2019-09-11 20:39 — By Erik van Eykelen
In this post I discuss the Addressable gem and the ways in which it improves dealing with URIs in Ruby code. If you find the term URI confusing then I recommend reading this article first.
Before diving into the strengths of Addressable, let’s look at a typical code example:
class Invoice
BASE_URI = "https://api.example.com"
INVOICE_URI = "#{BASE_URI}/customers/%<customer_id>i/invoices/%<invoice_id>i"
def self.uri(customer_id:, invoice_id:, format: nil)
uri = INVOICE_URI % { customer_id: customer_id, invoice_id: invoice_id }
uri += ".#{format}" if format
uri
end
end
Using Addressable it could look like this:
class Invoice
BASE_URI = "https://api.example.com"
INVOICE_URI = "#{BASE_URI}/customers/{customer_id}/invoices/{invoice_id}{.format}"
def self.uri(customer_id:, invoice_id:, format: nil)
Addressable::Template.new(INVOICE_URI).expand({
customer_id: customer_id,
invoice_id: invoice_id,
format: format
})
end
end
If we run both examples the results will be the same:
https://api.example.com/customers/1/invoices/2
https://api.example.com/customers/1/invoices/2.json
While you could argue that the example using Addressable consists of a few more lines, the benefit is that you no longer need a condition to deal with format
. Additionally, we don’t have to do any string concatenation which is a bit of a code smell.
Before we continue, let’s mention the two foundations on which Addressable rests:
IRI deals with URIs like iñtërnâtiônàlizætiøn.com
i.e. URIs containing non-ASCII characters.
A URI template allows you to define a URI based on variables, which are expanded based on certain rules. RFC 6570 describes four so-called “template levels”, all of which are supported by Addressable.
Template Level 1
This is the simplest level, which the RFC describes as:
[...] most template processors implemented prior to this specification have only implemented the default expression type, we refer to these as Level 1 templates.
Example:
template = Addressable::Template.new("https://www.example.com/{user}/{resource}")
template.expand({ user: 'erik', resource: 'archived documents' })
#=> https://www.example.com/erik/archived%20documents
Notice that Addressable properly escapes the space between archived
and documents
.
Template Level 2
RFC 6570 states about level 2:
Level 2 templates add the plus (“+”) operator, for expansion of values that are allowed to include reserved URI characters (Section 1.5), and the crosshatch (“#”) operator for expansion of fragment identifiers.
template = Addressable::Template.new("https://www.example.com/{+department}/people")
template.expand({ department: 'technology/r&d' })
#=> https://www.example.com/technology/r&d/people
The +
in {+department}
instructs the template to retain reserved URI characters instead of encoding them. Without the +
the result would be https://www.example.com/technology%2Fr%26d/people
.
Level 2 has one more trick up its sleeve namely fragments:
template = Addressable::Template.new("https://www.example.com/{+department}/people{#person}")
template.expand({ department: 'technology/r&d', person: 'erik' })
#=> https://www.example.com/technology/r&d/people#erik
Template Level 3
Level 3 turns it up a notch:
Level 3 templates allow multiple variables per expression, each separated by a comma, and add more complex operators for dot-prefixed labels, slash-prefixed path segments, semicolon-prefixed path parameters, and the form-style construction of a query syntax consisting of name=value pairs that are separated by an ampersand character.
String expansion with multiple variables:
template = Addressable::Template.new("https://www.example.com/map?{lat,long}")
template.expand({ lat: 37.384, long: -122.264 })
#=> https://www.example.com/map?37.384,-122.264
Reserved expansion with multiple variables:
template = Addressable::Template.new("https://www.example.com/{+department,floor}")
template.expand({ department: 'technology/r&d', floor: 1 })
#=> https://www.example.com/technology/r&d,1
Fragment expansion with multiple variables:
template = Addressable::Template.new("https://www.example.com/people{#id,person}")
template.expand({ id: 1001, person: 'erik' })
#=> https://www.example.com/people#1001,erik
Label expansion, dot-prefixed:
template = Addressable::Template.new("https://www.example.com/versions/v{.major,minor,build}")
template.expand({ major: 1, minor: 2, build: 1103 })
#=> https://www.example.com/versions/v.1.2.1103
Path segments, slash-prefixed:
template = Addressable::Template.new("https://www.example.com{/path,subpath}")
template.expand({ path: 'legal', subpath: 'terms-of-service' })
#=> https://www.example.com/legal/terms-of-service
Path-style parameters, semicolon-prefixed:
template = Addressable::Template.new("https://www.example.com/action?op=crop{;x,y,w,h}")
template.expand({ x: 0, y: 20, w: 256, h: 256 })
#=> https://www.example.com/action?op=crop;x=0;y=20;w=256;h=256
Form-style query, ampersand-separated:
template = Addressable::Template.new("https://www.example.com/action{?op,x,y,w,h}")
template.expand({ op: 'crop', x: 0, y: 20, w: 256, h: 256 })
#=> https://www.example.com/action?op=crop&x=0&y=20&w=256&h=256
Form-style query continuation:
template = Addressable::Template.new("https://www.example.com/action?op=crop{&x,y,w,h}")
template.expand({ x: 0, y: 20, w: 256, h: 256 })
#=> https://www.example.com/action?op=crop&x=0&y=20&w=256&h=256
Template Level 4
Finally, Level 4 templates add value modifiers as an optional suffix to each variable name. A prefix modifier (“:”) indicates that only a limited number of characters from the beginning of the value are used by the expansion (Section 2.4.1). An explode (“*”) modifier indicates that the variable is to be treated as a composite value, consisting of either a list of names or an associative array of (name, value) pairs, that is expanded as if each member were a separate variable (Section 2.4.2).
String expansion with value modifiers:
template = Addressable::Template.new("https://www.example.com/{user:1}/{user}/{resource}")
template.expand({ user: 'erik', resource: 'archived documents' })
#=> https://www.example.com/e/erik/archived%20documents
Notice the /e/
path segment.
Explode (*
) modifier examples:
template = Addressable::Template.new("https://www.example.com/map?{coords*}")
template.expand({ coords: [37.384, -122.264] })
#=> https://www.example.com/map?37.384,-122.264
template = Addressable::Template.new("https://www.example.com/map?{coords*}")
template.expand({ coords: { lat: 37.384, long: -122.264 } })
#=> https://www.example.com/map?lat=37.384,long=-122.264
Besides creating URIs, Addressable can also be used to parse URIs.
Suppose you have to deal with UTM parameters:
template = Addressable::Template.new("http://{host}{/segments*}/{?utm_source,utm_medium}{#fragment}")
uri = Addressable::URI.parse("http://example.com/a/b/c/?utm_source=1&utm_medium=2#preface")
template.extract(uri)
#=> {"host"=>"example.com", "segments"=>["a", "b", "c"], "utm_source"=>"1", "utm_medium"=>"2", "fragment"=>"preface"}
For other examples see Addressable’s readme. Also check out its tests, they’re easy to read.
I am now using Addressable whenever I have to craft or parse URIs. Let me know if you spot any errors or omissions.