Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit 64a07f8d3e23fed49220b99fc619107812b8ec9d Sven Fuchs committed May 29, 2008
Showing with 450 additions and 0 deletions.
  1. +20 −0 MIT-LICENSE
  2. +116 −0 README.markdown
  3. +1 −0 init.rb
  4. +82 −0 lib/routing_filter.rb
  5. +9 −0 lib/routing_filter/base.rb
  6. +23 −0 lib/routing_filter/locale.rb
  7. +41 −0 spec/root_section.rb
  8. +136 −0 spec/routing_filter_spec.rb
  9. +4 −0 spec/spec.opts
  10. +18 −0 spec/spec_helper.rb
@@ -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.
@@ -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
@@ -0,0 +1 @@
+require 'routing_filter'
@@ -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
@@ -0,0 +1,9 @@
+module RoutingFilter
+ class Base
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+ end
+end
@@ -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
@@ -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
Oops, something went wrong.

0 comments on commit 64a07f8

Please sign in to comment.