Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Sven Fuchs committed May 29, 2008
0 parents commit 64a07f8
Show file tree
Hide file tree
Showing 10 changed files with 450 additions and 0 deletions.
20 changes: 20 additions & 0 deletions MIT-LICENSE
@@ -0,0 +1,20 @@
Copyright (c) 2008 Sven Fuchs

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:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

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.
116 changes: 116 additions & 0 deletions README.markdown
@@ -0,0 +1,116 @@
## Routing Filter

This plugin is a wild hack that wraps around the complex beast that the Rails
routing system is to allow for unseen flexibility and power in Rails URL
recognition and generation.

As powerful and awesome the Rails' routes are, when you need to design your
URLs in a manner that only slightly leaves the paved cowpaths of Rails
conventions, you're usually unable to use all the goodness of helpers and
convenience that Rails ships with.

## Usage

This plugin comes with a locale routing filter that demonstrates the
implementation of a custom filter.

The following would be a sceleton of an empty filter:

module RoutingFilter
class Awesomeness < Base
def around_recognition(route, path, env)
# Alter the path here before it gets recognized.
# Next make sure to yield (calls the next around filter if present
# and eventually `recognize_path` on the routeset):
returning yield(path, env) do |params|
# You can additionally modify the params here before they get passed
# to the controller.
end
end

def after_generate(base, url_or_path, *args)
# You can change the generated url_or_path here. Make sure to use
# one of the "in-place" modifying String methods though (like sub! and
# friends).
end
end
end

You then have to specify the filter explicitely in your routes.rb:

ActionController::Routing::Routes.draw do |map|
map.filter 'awesomeness'
end

(I am not sure if it makes sense to provide more technical information than
this because the usage of this plugin definitely requires some advanced
knowledge about Rails internals and especially its routing system. So, I
figure, anyone who could use this should also be able to read the code and
figure out what it's doing much better then from any lengthy documentation.

If I'm mistaken on this please drop me an email with your suggestions.)


## Rationale: Two example usecases

An early usecase from which this originated was the need to define a locale
at the beginning of an URL in a way so that

* the locale can be omitted when it is the default locale
* all the url\_helpers that are generated by named routes continue to work in
a concise manner (i.e. without specifying all parameters again and again)
* ideally also plays nicely with default route helpers in tests/specs

You can read about this struggle and to possible, yet unsatisfying solutions
[here](). The conclusion so far is that Rails itself does not provide the
tools to solve this problem in a clean and dry way.

Another usecase that eventually spawned the manifestation of this plugin was
the need to map an arbitrary count of path segments to a certain model
instance. In an application that I've been working on recently I needed to
map URL paths to a nested tree of models like so:

root
+ docs
+ api
+ wiki

E.g. the docs section should map to the path /docs, the api section to
the path /docs/api and so on. Furthermore, after these paths need to be
more things to be specified. E.g. the wiki needs define a usual Rails resource
like /docs/wiki/pages/1/edit.

The only way to solve this problem with Rails' routing toolkit is to map
a bold /*whatever catch-all ("glob") and process the whole path in a custom
dispatcher.

This, of course, is a really unsatisfying solution because one has to
reimplement everything that Rails routes are here to help with, both with
regards to URL recognition (like parameter mappings, resources, ...) and
generation (url\_helpers don't work).

## Solution

This plugin offers a solution that takes exactly the opposite route. Instead
of trying to change things *between* the URL recognition and generation stages
to achieve the desired result it *wraps around* the whole routing system and
allows to filter both what goes into it (URL recognition) and what comes out
of it (URL generation).

This way we can leave *everything* else completely untouched.

* We can tinker with the URLs that we receive from the server and feed URLs to
Rails that perfectly match the best breed of Rails' conventions.
* Inside of the application we can use all the nice helper goodness and
conveniences that rely on these conventions being followed.
* Finally we can accept URLs that have been generated by the url\_helpers and,
again, mutate them in the way that matches our requirements.

So, even though the plugin itself is a blatant monkey-patch to one of the
most complex area of Rails internals, this solution seems to be effectively
less intrusive and pricey than others are.

## Etc

Authors: [Sven Fuchs](http://www.artweb-design.de) <svenfuchs at artweb-design dot de>
License: MIT
1 change: 1 addition & 0 deletions init.rb
@@ -0,0 +1 @@
require 'routing_filter'
82 changes: 82 additions & 0 deletions lib/routing_filter.rb
@@ -0,0 +1,82 @@
module RoutingFilter; end # make dependencies happy

# allows to install a filter to the route set by calling: map.filter 'locale'
ActionController::Routing::RouteSet::Mapper.class_eval do
def filter(name, options = {})
klass = "RoutingFilter::#{name.to_s.camelize}".constantize
@set.filters ||= []
@set.filters.push klass.new(options)
end
end

# hook into url_for and call before and after filters
ActionController::Base.class_eval do
def url_for_with_filtering(options = nil)
ActionController::Routing::Routes.filter_generate self, :before, options
returning url_for_without_filtering(options) do |result|
ActionController::Routing::Routes.filter_generate self, :after, result, options
end
end
alias_method_chain :url_for, :filtering
end

# same here for the optimized url generation in named routes
ActionController::Routing::RouteSet::NamedRouteCollection.class_eval do
# gosh. monkey engineering optimization code
def generate_optimisation_block_with_filtering(*args)
code = generate_optimisation_block_without_filtering *args
if match = code.match(%r(^return (.*) if (.*)))
<<-code
if #{match[2]}
ActionController::Routing::Routes.filter_generate self, :before, *args
result = #{match[1]}
ActionController::Routing::Routes.filter_generate self, :after, result, *args
return result
end
code
end
end
alias_method_chain :generate_optimisation_block, :filtering
end

ActionController::Routing::RouteSet.class_eval do
# allow to register filters to the route set
def filters
@filters ||= []
end

# call filter stage (:before or :after) with the passed args
def filter_generate(base, stage, *args)
filters.each do |filter|
filter.send :"#{stage}_generate", base, *args if filter.respond_to? :"#{stage}_generate"
end
end

# wrap recognition filters around recognize_path
def recognize_path_with_filtering(path, env)
path = path.dup
chain = [lambda{|path, env| recognize_path_without_filtering(path, env) }]
filters.each do |filter|
chain.unshift lambda{|path, env|
filter.around_recognition(self, path, env, &chain.shift)
}
end
chain.shift.call path, env
end
alias_method_chain :recognize_path, :filtering

# add some useful information to the request environment
# right, this is from jamis buck's excellent article about routes internals
# http://weblog.jamisbuck.org/2006/10/26/monkey-patching-rails-extending-routes-2
# TODO move this ... where?
alias_method :extract_request_environment_without_host, :extract_request_environment unless method_defined? :extract_request_environment_without_host
def extract_request_environment(request)
returning extract_request_environment_without_host(request) do |env|
env.merge! :host => request.host,
:port => request.port,
:host_with_port => request.host_with_port,
:domain => request.domain,
:subdomain => request.subdomains.first
end
end
end
9 changes: 9 additions & 0 deletions lib/routing_filter/base.rb
@@ -0,0 +1,9 @@
module RoutingFilter
class Base
attr_reader :options

def initialize(options)
@options = options
end
end
end
23 changes: 23 additions & 0 deletions lib/routing_filter/locale.rb
@@ -0,0 +1,23 @@
module RoutingFilter
class Locale < Base
@@default_locale = 'en'
cattr_reader :default_locale

# remove the locale from the beginning of the path, pass the path
# to the given block and set it to the resulting params hash
def around_recognition(route, path, env, &block)
locale = nil
path.sub! %r(^/([a-zA-Z]{2})(?=/|$)) do locale = $1; '' end
returning yield(path, env) do |params|
params[:locale] = locale if locale
end
end

# prepend the current locale to the path if it's not the default locale
def after_generate(base, result, *args)
locale = base.instance_variable_get(:@locale)
result.replace "/#{locale}#{result}" if locale and locale != @@default_locale
# TODO won't work with full urls, stupid
end
end
end
41 changes: 41 additions & 0 deletions spec/root_section.rb
@@ -0,0 +1,41 @@
# only here for test purposes ...

module RoutingFilter
class RootSection < Base

# this pattern matches a path that starts (aside from an optional locale)
# with a single slash or one of articles|pages|categories|tags, 4 digits
# or a dot followed by anything.
#
# So all of the following paths will match:
# / and /de
# /articles and /de/articles (same with pages, categories, tags)
# /2008 and /de/2008
# /.rss and /de.rss

# TODO ... should be defined through the dsl in routes.rb
@@pattern = %r(^/?(/[\w]{2})?(/articles|/pages|/categories|/tags|/\d{4}|\.|/?$))

# prepends the root section path to the path if the given pattern matches
def around_recognition(route, path, env, &block)
unless path =~ %r(^/admin) # TODO ... should be defined through the dsl in routes.rb
if match = path.match(@@pattern)
section = Site.find_by_host(env[:host_with_port]).sections.root
path.sub! /^#{match[0]}/, "#{match[1]}/#{section.type.pluralize.downcase}/#{section.id}#{match[2]}"
end
end
yield path, env
end

def after_generate(base, result, *args)
if site = base.instance_variable_get(:@site)
root = site.sections.root
pattern = %r(^(/[\w]{2})?(/#{root.type.pluralize.downcase}/#{root.id}(\.|/|$)))
if match = result.match(pattern)
result.sub! match[2], match[3] unless match[3] == '.'
result.replace '/' if result.empty?
end
end
end
end
end

0 comments on commit 64a07f8

Please sign in to comment.