Adds callbacks and allows for setting options with access to the rack env hash

This commit is contained in:
Daniel Neighman 2010-06-07 11:01:05 +10:00
parent 4685a622d2
commit f827ab06c0
10 changed files with 213 additions and 102 deletions

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ pkg
## PROJECT::SPECIFIC
*.gem
.bundle

11
Gemfile Normal file
View File

@ -0,0 +1,11 @@
source :rubygems
gem 'rack'
group :development do
gem 'rake'
end
group :test do
gem 'shoulda'
end

25
Gemfile.lock Normal file
View File

@ -0,0 +1,25 @@
---
dependencies:
rake:
group:
- :development
version: ">= 0"
shoulda:
group:
- :test
version: ">= 0"
rack:
group:
- :default
version: ">= 0"
specs:
- rake:
version: 0.8.7
- rack:
version: 1.1.0
- shoulda:
version: 2.10.3
hash: 20813f28addb840e72440df2793e52ace4717691
sources:
- Rubygems:
uri: http://gemcutter.org

View File

@ -4,7 +4,7 @@ UrlMount is a universal mount point designed for use in rack applications.
It provides a simple way to pass a url mounting point to the mounted application.
This means that when you mount an application in the url space, it's a simple to_s to get the mount point of where the application is.
This means that when you mount an application in the url space, it's a simple call to url to get the mount point of where the application is.
h2. Example
@ -23,23 +23,23 @@ Say you mount an application at "/foo/bar"
This means that if an application can handle a url_mount, you give it one.
This then allows the +my_app+ applciation, to know where it's mounted. Generating a url is now as simple as
<pre><code>File.join(url_mount.to_s, "/local/url/path")</code></pre>
<pre><code>File.join(url_mount.url, "/local/url/path")</code></pre>
The benefit of this is that all routers or applciations can make use of this. If used, it can act as a universal glue between rack applications where parent apps can let the child applications know where they're mounted so the child applciations can have a chance of generating full routes, even when they're not in the request path.
h3. Show me the code
urlmount is made to be used with many routers that are currently available.
url_mount is made to be used with many routers that are currently available.
<pre><code>
# simple string mounting point
mount = urlmount.new("/foo/bar")
mount.to_s == "/foo/bar"
mount = UrlMount.new("/foo/bar")
mount.url == "/foo/bar"
# Mount Point including variables
mount = UrlMount.new("/foo/:bar", :bar => "bar")
mount.to_s(:bar => "something") == "/foo/something"
mount.to_s #=> Raises UrlMount::Ungeneratable because a required variable was not found
mount.url(:bar => "something") == "/foo/something"
mount.url #=> Raises UrlMount::Ungeneratable because a required variable was not found
mount.required_variables == [:bar]
# UrlMount::Ungeneratable raised when a route cannot be generated without options
@ -47,20 +47,20 @@ no_mount = UrlMount.new("/foo/:bar") # fails because the mount point cannot be g
# Mount Point including optional variables
mount = UrlMount.new("/foo/:bar(/:baz)", :bar => "bar")
mount.to_s(:bar => "doh") == "/foo/doh"
mount.to_s(:bar => "doh", :baz => "hah") == "/foo/doh/hah"
mount.url(:bar => "doh") == "/foo/doh"
mount.url(:bar => "doh", :baz => "hah") == "/foo/doh/hah"
mount.required_variables == [:bar]
mount.optional_variables == [:baz]
# Mount Point with defaults
mount = UrlMount.new("/foo/:bar(/:baz)", :bar => "default_bar")
mount.to_s == "/foo/default_bar"
mount.to_s(:baz => "baz_value") == "/foo/default_bar/baz_value"
mount.to_s(:bar => "other_bar") == "/foo/other_bar"
mount.url == "/foo/default_bar"
mount.url(:baz => "baz_value") == "/foo/default_bar/baz_value"
mount.url(:bar => "other_bar") == "/foo/other_bar"
# Using procs for mount point defaults
mount = UrlMount.new("/foo/:bar", :bar => proc{"some_bar"})
mount.to_s == "/foo/some_bar"
mount.url == "/foo/some_bar"
# Nested mounting point
mount_parent = UrlMount.new("/foo/bar")
@ -68,8 +68,8 @@ mount_child = UrlMount.new("/baz/:barry)
mount_child.url_mount = mount_parent
mount_parent.to_s == "/foo/bar"
mount_child.to_s(:barry =>"barry_value") == "/foo/bar/baz/barry_value"
mount_parent.url == "/foo/bar"
mount_child.url(:barry =>"barry_value") == "/foo/bar/baz/barry_value"
</code></pre>
Considering that UrlMounts can be nested, when you mount an application you should do it something like this.
@ -94,7 +94,7 @@ When you generate routes, you can see which variables will be used by the mount
url = UrlMount.new(/"foo/:bar(/:baz)", :bar => "some_bar")
generation_options = {:baz => "baz", :bar => "bar"}
mount_point = url.to_s(generation_options)
mount_point = url.url(generation_options)
# We can now at this point ask the mount point what variables it will use if present.
url.variables.each{|v| generation_options.delete(v)}
@ -102,6 +102,20 @@ When you generate routes, you can see which variables will be used by the mount
# use the generation_options now without the mount_point variables to interfere with a query string
</code></pre>
h2. Callbacks
When generating the routes, you can provide one or more callbacks to the mount point. Inside the callbacks you can set options for the routes based on the Rack request.
<pre><code>mount = UrlMount.new("/foo/:bar") do |env, opts|
opts[:bar] ||= "my_bar"
end
mount.url(env) # Rack::Request === env
#=> /foo/my_bar
</code></pre>
NOTE: callbacks are only run when a rack environment is given to the url method
h2. Note on Patches/Pull Requests
* Fork the project.

View File

@ -1,23 +1,6 @@
require 'rubygems'
require 'rake'
begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "url_mount"
gem.summary = %Q{Universal mounting points for rack applications}
gem.description = %Q{Glue to allow mounted rack applications to know where they're mounted}
gem.email = "has.sox@gmail.com"
gem.homepage = "http://github.com/hassox/url_mount"
gem.authors = ["Daniel Neighman"]
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
end
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
@ -38,8 +21,6 @@ rescue LoadError
end
end
task :test => :check_dependencies
task :default => :test
require 'rake/rdoctask'

View File

@ -1 +0,0 @@
0.1.1

View File

@ -3,13 +3,20 @@ class UrlMount
# Inspiration for this is taken straight from Usher. http://github.com/joshbuddy/usher
DELIMETERS = ['/', '(', ')']
attr_accessor :raw_path, :options, :url_mount
attr_accessor :raw_path, :options, :url_mount, :host, :scheme
alias_method :defaults, :options
def initialize(path, opts = {})
def initialize(path, opts = {}, &blk)
@raw_path, @options = path, opts
@url_split_regex = Regexp.new("[^#{DELIMETERS.collect{|d| Regexp.quote(d)}.join}]+|[#{DELIMETERS.collect{|d| Regexp.quote(d)}.join}]")
to_s
@host, @scheme = opts[:host], opts[:scheme]
@callbacks = []
@callbacks << blk if blk
end
def callback(&blk)
@callbacks << blk if blk
@callbacks
end
def local_segments
@ -53,7 +60,14 @@ class UrlMount
end
end
def to_s(opts = {})
def url(env = {}, opts = {})
unless env.key?('rack.version')
opts = env
env = nil
end
@callbacks.each{|blk| blk.call(env,opts)} if env
requirements_met = (local_required_variables - (opts.keys + options.keys)).empty?
if !required_to_generate? && !requirements_met
@ -62,9 +76,21 @@ class UrlMount
raise Ungeneratable, "Missing required variables" if !requirements_met
File.join(local_segments.inject([]){|url, segment| str = segment.to_s(opts); url << str if str; url}) =~ /(.*?)\/?$/
result = $1
url_mount.nil? ? result : File.join(url_mount.to_s(opts), result)
path = url_mount.nil? ? result : File.join(url_mount.to_s(opts), result)
if opts[:host] || host || opts[:scheme] || scheme
_host = opts[:host] || host
_scheme = opts[:scheme] || scheme || "http"
raise Ungeneratable, "Missing host when generating absolute url" if _scheme && !_host
uri = URI.parse(path)
uri.host = _host
uri.scheme = _scheme || "http"
uri.to_s
else
path
end
end
end
alias_method :to_s, :url
private
def local_required_variables

View File

@ -1,6 +1,7 @@
require 'rubygems'
require 'test/unit'
require 'shoulda'
require 'rack'
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.dirname(__FILE__))

View File

@ -27,31 +27,37 @@ class TestUrlMount < Test::Unit::TestCase
should "calculate required variables from procs" do
u = UrlMount.new("/foo/:bar/:baz", :bar => "a_bar", :baz => proc{"baz_in_proc"})
assert_equal "/foo/a_bar/baz_in_proc", u.to_s
assert_equal "/foo/a_bar/baz_in_proc", u.url
end
should "generate a static url mount" do
u = UrlMount.new("/foo/bar")
assert_equal "/foo/bar", u.to_s
assert_equal "/foo/bar", u.url
end
should "generate a dynamic url with static and variable segments" do
u = UrlMount.new("/foo/:bar/baz/:barry", :bar => "bar", :barry => "sue")
assert_equal "/foo/bar/baz/sue", u.to_s
assert_equal "/foo/bar/baz/sue", u.url
end
should "raise an exception when a required variable is missing" do
assert_raises UrlMount::Ungeneratable do
UrlMount.new("/foo/:bar/:baz")
UrlMount.new("/foo/:bar/:baz").url
end
end
should "consume the options so the router does not use them" do
opts = {:bar => "bar", :other => "other"}
u = UrlMount.new("/foo/:bar", :bar => "some_default_bar")
u.to_s(opts)
u.url(opts)
assert_equal( {:bar => "bar", :other => "other"}, opts )
end
should "alias to_s to url" do
u = UrlMount.new "/foo/bar"
assert_equal u.to_s, u.url
end
end
context "optional variables" do
@ -67,57 +73,57 @@ class TestUrlMount < Test::Unit::TestCase
should "calculate optional variables when there are some" do
u = UrlMount.new("/foo(/:bar(/:baz))")
assert_equal "/foo/gary", u.to_s(:bar => "gary")
assert_equal "/foo/gary", u.url(:bar => "gary")
end
should "skip nested optional variables when the optional parent is not present" do
u = UrlMount.new("/foo(/:bar(/:baz))")
assert_equal "/foo", u.to_s(:baz => "sue")
assert_equal "/foo", u.url(:baz => "sue")
end
end
context "default variables" do
should "generate a simple url with a variable with a default" do
u = UrlMount.new("/foo/:bar", :bar => "default")
assert_equal "/foo/default", u.to_s
assert_equal "/foo/default", u.url
end
should "generate urls with multiple varilables using defaults" do
u = UrlMount.new("/foo/:bar/:baz", :bar => "bar", :baz => "baz")
assert_equal "/foo/bar/baz", u.to_s
assert_equal "/foo/bar/baz", u.url
end
should "generate urls with optional variables" do
u = UrlMount.new("/foo(/:bar)", :bar => "bar")
assert_equal "/foo/bar", u.to_s
assert_equal "/foo/bar", u.url
end
should "generate urls with mixed variables" do
u = UrlMount.new("/foo/:bar(/:baz(/:barry))", :barry => "bazz", :bar => "clue")
assert_equal "/foo/clue", u.to_s
assert_equal "/foo/clue/sue/bazz", u.to_s(:baz => "sue")
assert_equal "/foo/clue", u.url
assert_equal "/foo/clue/sue/bazz", u.url(:baz => "sue")
end
should "generate urls with overwritten defaults" do
u = UrlMount.new("/foo/:bar(/:baz)", :bar => "barr", :baz => "bazz")
assert_equal "/foo/sue/larry", u.to_s(:bar => "sue", :baz => "larry")
assert_equal "/foo/barr/gary", u.to_s(:baz => "gary")
assert_equal "/foo/harry/bazz", u.to_s(:bar => "harry")
assert_equal "/foo/sue/larry", u.url(:bar => "sue", :baz => "larry")
assert_equal "/foo/barr/gary", u.url(:baz => "gary")
assert_equal "/foo/harry/bazz", u.url(:bar => "harry")
end
should "generate optional and fixed paths with procs" do
u = UrlMount.new("/foo/:bar(/:baz)", :bar => proc{"the_bar"}, :baz => proc{"the_baz"})
assert_equal "/foo/the_bar/the_baz", u.to_s
assert_equal "/foo/bar/other_baz", u.to_s(:bar => "bar", :baz => proc{"other_baz"})
assert_equal "/foo/the_bar/the_baz", u.url
assert_equal "/foo/bar/other_baz", u.url(:bar => "bar", :baz => proc{"other_baz"})
end
end
context "complex compound urls" do
should "generate complex urls containing multiple nested conditionals and multiple required variables" do
u = UrlMount.new("/foo(/:bar(/:baz))/:gary", :gary => "gary")
assert_equal "/foo/gary", u.to_s
assert_equal "/foo/bar/gary", u.to_s(:bar => "bar")
assert_equal "/foo/bar/baz/gary", u.to_s(:bar => "bar", :baz => "baz")
assert_equal "/foo/gary", u.url
assert_equal "/foo/bar/gary", u.url(:bar => "bar")
assert_equal "/foo/bar/baz/gary", u.url(:bar => "bar", :baz => "baz")
end
end
@ -132,15 +138,15 @@ class TestUrlMount < Test::Unit::TestCase
u1 = UrlMount.new("/root/bar")
u2 = UrlMount.new("/baz/barry")
u2.url_mount = u1
assert_equal "/root/bar", u1.to_s
assert_equal "/root/bar/baz/barry", u2.to_s
assert_equal "/root/bar", u1.url
assert_equal "/root/bar/baz/barry", u2.url
end
should "overwrite a parents options" do
u1 = UrlMount.new("/root/:bar", :bar => "bar")
u2 = UrlMount.new("/baz/barry")
u2.url_mount = u1
assert_equal "/root/different/baz/barry", u2.to_s(:bar => "different")
assert_equal "/root/different/baz/barry", u2.url(:bar => "different")
end
should "not consume params to nested routes" do
@ -148,8 +154,87 @@ class TestUrlMount < Test::Unit::TestCase
u2 = UrlMount.new("/baz/:barry", :barry => "barry")
u2.url_mount = u1
opts = {:bar => "sue", :barry => "wendy"}
assert_equal "/root/sue/baz/wendy", u2.to_s(opts)
assert_equal "/root/sue/baz/wendy", u2.url(opts)
assert_equal({:bar => "sue", :barry => "wendy"}, opts)
end
end
context "host options" do
should "generate an absolute url when given the host option" do
u = UrlMount.new("/foo/bar")
assert_equal "http://example.com/foo/bar", u.url(:host => "example.com")
end
should "generate an absolute url when the host option is set in the route" do
u = UrlMount.new("/foo/bar", :host => "example.com")
assert_equal "http://example.com/foo/bar", u.url
end
should "overwrite the host" do
u = UrlMount.new("/foo/bar", :host => "example.com")
assert_equal "http://foo.com/foo/bar", u.url(:host => "foo.com")
end
should "allow me to set the scheme" do
u = UrlMount.new("/foo/bar", :host => "example.com", :scheme => "https")
assert_equal "https://example.com/foo/bar", u.url
end
should "raise an exception if the scheme is set and not the host" do
u = UrlMount.new("/foo/bar")
assert_raises(UrlMount::Ungeneratable) do
u.url(:scheme => "https")
end
end
end
context "callbacks" do
should "let me add a callback" do
captures = []
u = UrlMount.new("/foo/bar") do |env, opts|
captures << :block
req = Rack::Request.new(env)
assert_equal "example.com", req.host
opts[:host] = req.host
opts[:scheme] = req.scheme
end
env = Rack::MockRequest.env_for("/", 'HTTP_HOST' => "example.com")
assert_equal "http://example.com/foo/bar", u.url(env)
assert_equal :block, captures.first
end
should "let me add many callbacks" do
captures = []
u = UrlMount.new("/foo/bar")
u.callback do |env, opts|
captures << :one
opts[:one] = :one
end
u.callback do |env, opts|
captures << :two
captures << opts[:one]
end
env = Rack::MockRequest.env_for("/", :host => "example.com")
assert_equal "/foo/bar", u.url(env)
assert_equal [:one, :two, :one], captures
end
should "not run the callbacks if the env is not available" do
captures = []
u = UrlMount.new("/foo/bar")
u.callback do |env, opts|
captures << :here
end
assert_equal "/foo/bar", u.url
assert_equal [], captures
end
should "let me generate a required option url via callbacks" do
env = Rack::MockRequest.env_for "/"
u = UrlMount.new("/foo/:bar") do |env, opts|
opts[:bar] ||= "here_is_my_bar"
end
assert_equal "/foo/here_is_my_bar", u.url(env)
end
end
end

View File

@ -1,54 +1,22 @@
# Generated by jeweler
# DO NOT EDIT THIS FILE DIRECTLY
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
# -*- encoding: utf-8 -*-
require 'bundler'
Gem::Specification.new do |s|
s.name = %q{url_mount}
s.version = "0.1.0"
s.version = "0.2.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Daniel Neighman"]
s.date = %q{2010-02-18}
s.date = %q{2010-06-07}
s.description = %q{Glue to allow mounted rack applications to know where they're mounted}
s.email = %q{has.sox@gmail.com}
s.extra_rdoc_files = [
"LICENSE",
"README.rdoc"
]
s.files = [
".document",
".gitignore",
"LICENSE",
"README.rdoc",
"Rakefile",
"VERSION",
"lib/url_mount.rb",
"test/helper.rb",
"test/test_url_mount.rb",
"url_mount.gemspec"
]
s.files = Dir["**/*"]
s.homepage = %q{http://github.com/hassox/url_mount}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.5}
s.summary = %q{Universal mounting points for rack applications}
s.test_files = [
"test/helper.rb",
"test/test_url_mount.rb"
]
if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 3
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
else
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
end
else
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
end
s.add_bundler_dependencies
end