resourcelogic 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+ *.log
3
+ *.sqlite3
4
+ pkg/*
5
+ coverage/*
6
+ doc/*
7
+ benchmarks/*
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ == 0.0.9
2
+
3
+ * Initial release, beta.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Ben Johnson of Binary Logic (binarylogic.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,167 @@
1
+ = Resourcelogic
2
+
3
+ <b>Beta warning: right now this plugin is in what I like to call a "semi-beta". The only thing left to do is write documentation and add in the tests. I have already thoroughly tested this in a separate application, and am in the process of moving over the tests without having to pull over the entire rails application.</b>
4
+
5
+ The purpose of Resourcelogic is to support a development style I created called <b>"Contextual Development"</b> (see contextual development below). This is an idea I've had for a while and finally decided to give it a try on one of my projects. It worked out great, and as a result, is probably the cleanest app I've built to date. So I decided to package this idea up and release it as Resourcelogic. This library spawned out of the {resource_controller plugin}[http://github.com/giraffesoft/resource_controller] by James Gollick, which is an excellent plugin. I eventually made so many changes to it that it made more sense to rewrite my own.
6
+
7
+ == Helpful links
8
+
9
+ * <b>Documentation:</b> http://rdoc.info/projects/binarylogic/resourcelogic
10
+ * <b>Bugs / feature suggestions:</b> http://binarylogic.lighthouseapp.com/projects/28581-resourcelogic
11
+
12
+ == Contextual Development
13
+
14
+ The idea behind contextual development is simple: <b>an API should be a be a byproduct of good design</b>. Meaning you should never have to explicitly build an API. It should, in essence, be an "accident".
15
+
16
+ === 1. No need to namespace controllers when you have context
17
+
18
+ Your interface should not dictate the structure of your controllers. Instead, your controllers should represent a collection of resources that your interface can use (AKA an API). The problem is that rails does so much magic, and makes so many assumptions, that sometimes it distorts the basics behind the MVC pattern.
19
+
20
+ A good example is using {Flex}[http://www.adobe.com/products/flex/] to build your interface. This allows you to go back to the basics behind RESTful development by forcing you to separate your interface from your application. A flex interface communicates with your application via XML. As a result, when you complete your application you also have a "production ready" API. You basically completed two projects in one.
21
+
22
+ That being said, why can't we accomplish the same thing with an HTML interface? For one, an HTML interface isn't as cleanly decoupled from the controllers as a Flex interface. For two, rails doesn't give us all of the tools to properly accomplish this. Instead, people create work-arounds by namespacing controllers to represent context and/or scope (Ex: Admin::CommentsController). Why not have a single controller that is <b>context aware</b>? Hence the name <b>"Contextual Development"</b>. So instead of an Admin namespace, you can use the context to modify the interface / scope. Example:
23
+
24
+ class CommentsController < ApplicationController
25
+ layout Proc.new { context == :admin ? "admin" : "application" }
26
+
27
+ private
28
+ # context is irrelevant when determining scope
29
+ def scope
30
+ current_user.admin? ? Comment : current_user.comments
31
+ end
32
+ end
33
+
34
+ === 2. Single point of access for resources
35
+
36
+ Having a <b>single</b> controller is great, because it represents a <b>single</b> resource. This means that <b>every</b> request for that resource must pass through that single controller. Think about it, what is an API essentially? Controlled access to your database. You don't go and create an entirely new model for every scope/context you use the model, do you? Why do the same for your API? Your API resources are the "models" for anyone using it, so it doesn't make sense to have /comments and /admin/comments.
37
+
38
+ To bring it all home, all requests for a resource should pass through a single controller, in the same manner that all database activity for a table must pass through a single model. You can feel confident adding controller specific logic to that resource without having to worry about duplicating it across multiple controllers. A very simple example: storing the user's IP address with a record.
39
+
40
+ === 3. Relative URL and path methods
41
+
42
+ Another big problem with using the same controller in various contexts is the need to specify relative paths instead of absolute paths. When you specify resources in your routes you get all kinds of useful <b>absolute</b> path methods, but where are the relative ones? Let's say you have the following paths:
43
+
44
+ 1. /articles
45
+ 2. /admin/articles
46
+
47
+ Articles have many comments. What if you want to link to that article's comments? Do you use <b>article_comments_path</b> or <b>admin_article_comments_path</b>? You could use the <b>context</b> method described above to determine this, but this will get messy and annoying very fast. Instead why not use <b>child_collection_path(:comments)</b>? The paths are relative based on a tree structure. So you also get <b>parent</b> and <b>sibling</b> paths. Now you can easily nest resources without having to worry about linking to them. See the documentation for more details on the various URL and path methods that are available to you.
48
+
49
+ == Install and use
50
+
51
+ === 1. Install the gem
52
+
53
+ Install the gem / plugin (recommended)
54
+
55
+ $ sudo gem install resourcelogic
56
+
57
+ Now add the gem dependency in your config:
58
+
59
+ # config/environment.rb
60
+ config.gem "resourcelogic"
61
+
62
+ Or you install this as a plugin (for older versions of rails)
63
+
64
+ script/plugin install git://github.com/binarylogic/resourcelogic.git
65
+
66
+ === 2. Create your ResourceController
67
+
68
+ script/generate controller resource
69
+
70
+ Your ResourceController should look something like:
71
+
72
+ class ResourceController < ApplicationController
73
+ acts_as_resource
74
+ end
75
+
76
+ Now all of your controllers that are "resources" can extend this controller. Why do this? So you can set your default behavior for resources in one spot. This idea what brought over from the resource_controller plugin. The DSL resource_controller came up with is pretty cool:
77
+
78
+ class ResourceController < ApplicationController
79
+ acts_as_resource
80
+
81
+ create.flash { # code to evaluate to create the flash message for a successful create }
82
+ create.before { # code you want to execute before the create action }
83
+ create.wants.js { # code you want to execute in the wants.js for a successful create }
84
+ create.failure.flash { # code to evaluate to create the flash message for an unsuccessful create }
85
+ create.failure.js { # code you want to execute in the wants.js for an unsuccessful create }
86
+ # etc...See Resourcelogic::ActionOptions and Resourcelogic::FailableActionOptions for more details
87
+ end
88
+
89
+ All of these are overrideable, meaning your subclasses can change behavior in a granular manner, and preserve the defaults where necessary.
90
+
91
+ === 3. Make sure you don't namespace your controllers
92
+
93
+ Instead of namespacing your controllers, give them context. For example, let's say you you have /commments and /admin/comments. This controller has 2 contexts: root and admin. So your routes should look something like:
94
+
95
+ map.with_options(:path_prefix => "admin", :name_prefix => "admin_") do |admin|
96
+ admin.resource :comments
97
+ end
98
+ map.resources :comments
99
+
100
+ Then in your controller use the context method to make adjustments:
101
+
102
+ class CommentsController < ResourceController
103
+ layout Proc.new { context == :admin ? "admin" : "application" }
104
+ end
105
+
106
+ You also have the same context method in your views. Lastly, if you feel your views are getting cluttered by trying to determine what context you are in, you can use the namespace_views_by_context option (See Resourcelogic::Context::Config for more info). This will change your default view path to a context subfolder. Ex:
107
+
108
+ /comments
109
+ /admin
110
+ /root
111
+ any other contexts..
112
+
113
+ See the Feature Highlights section below for more options, and the documentation for a complete list.
114
+
115
+ == Feature highlights
116
+
117
+ I don't want to repeat what is already in the documentation, but there are a lot of really nice configuration and utility methods. <b>Here are just a few</b>:
118
+
119
+ <b>Class level methods</b>
120
+
121
+ belongs_to :relationship_name # will check to see if the resource is being scoped by a parent and give you some nifty methods for this (see below). You can call this multiple times. Just like ActiveRecord.
122
+ contextual_views true # will split up your views into subfolders: comments/context1, comments/context2, and will change your default view path to the respective folder
123
+
124
+ <b>Instance level methods</b>
125
+
126
+ Lets pretend we are dealing with a products resource that belongs to a category.
127
+
128
+ context # the name of the context you are in
129
+
130
+ object # current product object, if any
131
+ collection # current collection of products
132
+ parent # current category object, if any
133
+
134
+ Now you have all of the "relative routes" to help you easily nest resources and use them in different contexts:
135
+
136
+ object_path # /products/:id
137
+ edit_object_path # /products/:id/edit
138
+ new_object_path # /products/new
139
+ collection_path # /products
140
+
141
+ parent_path # /categories/:parent_id
142
+ edit_parent_path # /categories/:parent_id/edit
143
+ parent_collection_path # /categories
144
+ new_parent_path # /categories/new
145
+
146
+ sibling_path(sibling) # /sibling_name/:id
147
+ edit_sibling_path(sibling) # /sibling_name/:id/edit
148
+ new_sibling_path(:sibling_name) # /sibling_name/new
149
+ sibling_collection_path(:sibling_name) # /sibling_name
150
+
151
+ child_path(child) # /products/:product_id/child_name/:id
152
+ edit_child_path(child) # /products/:product_id/child_name/:id/edit
153
+ new_child_path(:child_name) # /products/:product_id/child_name/new
154
+ child_collection_path(:child_name) # /products/:product_id/child_name
155
+
156
+ Notice you have the edit_* paths. The above paths are implemented in a very flexible manner. So you are not limited them, you can call your custom actions too:
157
+
158
+ map.resources :products, :member => [:approve], :collection => [:approve_all]
159
+ approve_object_path # /products/:id/approve
160
+ approve_all_collection_path # /products/approve
161
+
162
+ The above example is probably not the best one, but you get the point.
163
+
164
+ Lastly, all of the above can end with _url instead of _path. <b>See docs for a complete list of available methods.</b>
165
+
166
+
167
+ Copyright (c) 2008 {Ben Johnson of Binary Logic}[http://www.binarylogic.com], released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "resourcelogic"
8
+ gem.summary = "Removes the need to namespace controllers by adding context and relative url functions among other things."
9
+ gem.email = "bjohnson@binarylogic.com"
10
+ gem.homepage = "http://github.com/binarylogic/resourcelogic"
11
+ gem.authors = ["Ben Johnson of Binary Logic"]
12
+ gem.rubyforge_project = "resourcelogic"
13
+ gem.add_dependency "activesupport"
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/*_test.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+ rescue LoadError
34
+ task :rcov do
35
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
36
+ end
37
+ end
38
+
39
+ task :default => :test
40
+
41
+ begin
42
+ require 'rake/contrib/sshpublisher'
43
+ namespace :rubyforge do
44
+ desc "Release gem to RubyForge"
45
+ task :release => ["rubyforge:release:gem"]
46
+ end
47
+ rescue LoadError
48
+ puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
49
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 11
3
+ :major: 0
4
+ :minor: 0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "resourcelogic"
@@ -0,0 +1,16 @@
1
+ require File.dirname(__FILE__) + "/resourcelogic/accessors"
2
+ require File.dirname(__FILE__) + "/resourcelogic/action_options"
3
+ require File.dirname(__FILE__) + "/resourcelogic/actions"
4
+ require File.dirname(__FILE__) + "/resourcelogic/aliases"
5
+ require File.dirname(__FILE__) + "/resourcelogic/child"
6
+ require File.dirname(__FILE__) + "/resourcelogic/context"
7
+ require File.dirname(__FILE__) + "/resourcelogic/failable_action_options"
8
+ require File.dirname(__FILE__) + "/resourcelogic/parent"
9
+ require File.dirname(__FILE__) + "/resourcelogic/response_collector"
10
+ require File.dirname(__FILE__) + "/resourcelogic/scope"
11
+ require File.dirname(__FILE__) + "/resourcelogic/self"
12
+ require File.dirname(__FILE__) + "/resourcelogic/sibling"
13
+ require File.dirname(__FILE__) + "/resourcelogic/singleton"
14
+ require File.dirname(__FILE__) + "/resourcelogic/sub_views"
15
+ require File.dirname(__FILE__) + "/resourcelogic/urligence"
16
+ require File.dirname(__FILE__) + "/resourcelogic/base"
@@ -0,0 +1,53 @@
1
+ module Resourcelogic # :nodoc:
2
+ module Accessors # :nodoc:
3
+ private
4
+ def block_accessor(*accessors)
5
+ accessors.each do |block_accessor|
6
+ class_eval <<-"end_eval", __FILE__, __LINE__
7
+
8
+ def #{block_accessor}(*args, &block)
9
+ unless args.empty? && block.nil?
10
+ args.push block if block_given?
11
+ @#{block_accessor} = [args].flatten
12
+ end
13
+
14
+ @#{block_accessor}
15
+ end
16
+
17
+ end_eval
18
+ end
19
+ end
20
+
21
+ def scoping_reader(*accessor_names)
22
+ accessor_names.each do |accessor_name|
23
+ class_eval <<-"end_eval", __FILE__, __LINE__
24
+ def #{accessor_name}(&block)
25
+ @#{accessor_name}.instance_eval &block if block_given?
26
+ @#{accessor_name}
27
+ end
28
+ end_eval
29
+ end
30
+ end
31
+
32
+ def class_scoping_reader(accessor_name, start_value)
33
+ write_inheritable_attribute accessor_name, start_value
34
+
35
+ class_eval <<-"end_eval", __FILE__, __LINE__
36
+ def self.#{accessor_name}(context = :root, &block)
37
+ read_inheritable_attribute(:#{accessor_name}).instance_eval(&block) if block_given?
38
+ read_inheritable_attribute(:#{accessor_name})
39
+ end
40
+ end_eval
41
+ end
42
+
43
+ def reader_writer(accessor_name)
44
+ class_eval <<-"end_eval", __FILE__, __LINE__
45
+ def #{accessor_name}(*args, &block)
46
+ args << block unless block.nil?
47
+ @#{accessor_name} = args.first unless args.empty?
48
+ @#{accessor_name}
49
+ end
50
+ end_eval
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,40 @@
1
+ module Resourcelogic
2
+ class ActionOptions
3
+ extend Resourcelogic::Accessors
4
+
5
+ reader_writer :flash
6
+ reader_writer :flash_now
7
+
8
+ block_accessor :after, :before
9
+
10
+ def initialize
11
+ @collector = Resourcelogic::ResponseCollector.new
12
+ end
13
+
14
+ def response(*args, &block)
15
+ if !args.empty? || block_given?
16
+ @collector.clear
17
+ args.flatten.each { |symbol| @collector.send(symbol) }
18
+ block.call(@collector) if block_given?
19
+ end
20
+
21
+ @collector.responses
22
+ end
23
+ alias_method :respond_to, :response
24
+ alias_method :responds_to, :response
25
+
26
+ def wants
27
+ @collector
28
+ end
29
+
30
+ def dup
31
+ returning self.class.new do |duplicate|
32
+ duplicate.instance_variable_set(:@collector, wants.dup)
33
+ duplicate.instance_variable_set(:@before, before.dup) unless before.nil?
34
+ duplicate.instance_variable_set(:@after, after.dup) unless after.nil?
35
+ duplicate.instance_variable_set(:@flash, flash.dup) unless flash.nil?
36
+ duplicate.instance_variable_set(:@flash_now, flash_now.dup) unless flash_now.nil?
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,174 @@
1
+ module Resourcelogic
2
+ module Actions
3
+ ACTIONS = [:index, :show, :new_action, :create, :edit, :update, :destroy].freeze
4
+ FAILABLE_ACTIONS = ACTIONS - [:index, :new_action, :edit, :destroy].freeze
5
+
6
+ def self.included(klass)
7
+ klass.class_eval do
8
+ ACTIONS.each do |action|
9
+ class_scoping_reader action, FAILABLE_ACTIONS.include?(action) ? FailableActionOptions.new : ActionOptions.new
10
+ end
11
+ class_scoping_reader :context, ContextOptions.new
12
+ add_acts_as_resource_module(Methods)
13
+ end
14
+ end
15
+
16
+ module Methods
17
+ def new
18
+ load_object
19
+ before :new_action
20
+ response_for :new_action
21
+ end
22
+
23
+ def create
24
+ load_object
25
+ before :create
26
+ if object.save
27
+ after :create
28
+ set_flash :create
29
+ response_for :create
30
+ else
31
+ after :create_fails
32
+ set_flash :create_fails
33
+ response_for :create_fails
34
+ end
35
+ end
36
+
37
+ def edit
38
+ load_object
39
+ before :edit
40
+ response_for :edit
41
+ end
42
+
43
+ def update
44
+ load_object
45
+ object.attributes = attributes
46
+ before :update
47
+ if object.save
48
+ after :update
49
+ set_flash :update
50
+ response_for :update
51
+ else
52
+ after :update_fails
53
+ set_flash :update_fails
54
+ response_for :update_fails
55
+ end
56
+ end
57
+
58
+ def destroy
59
+ load_object
60
+ before :destroy
61
+ object.destroy
62
+ after :destroy
63
+ set_flash :destroy
64
+ response_for :destroy
65
+ end
66
+
67
+ def show
68
+ load_object
69
+ before :show
70
+ response_for :show
71
+ rescue ActiveRecord::RecordNotFound
72
+ response_for :show_fails
73
+ end
74
+
75
+ def index
76
+ load_collection
77
+ before :index
78
+ response_for :index
79
+ end
80
+
81
+ private
82
+ # Used to actually pass the responses along to the controller's respond_to method.
83
+ #
84
+ def response_for(action)
85
+ begin
86
+ respond_to do |wants|
87
+ options_for(action).response.each do |method, block|
88
+ if block.nil?
89
+ wants.send(method)
90
+ else
91
+ wants.send(method) { instance_eval(&block) }
92
+ end
93
+ end
94
+ end
95
+ rescue ActionController::DoubleRenderError
96
+ end
97
+ end
98
+
99
+ # Calls the after callbacks for the action, if one is present.
100
+ #
101
+ def after(action)
102
+ invoke_callbacks *options_for(action).after
103
+ end
104
+
105
+ # Calls the before block for the action, if one is present.
106
+ #
107
+ def before(action)
108
+ invoke_callbacks *self.class.send(action).before
109
+ end
110
+
111
+ # Sets the flash and flash_now for the action, if it is present.
112
+ #
113
+ def set_flash(action)
114
+ set_normal_flash(action)
115
+ set_flash_now(action)
116
+ end
117
+
118
+ # Sets the regular flash (i.e. flash[:notice] = '...')
119
+ #
120
+ def set_normal_flash(action)
121
+ if f = options_for(action).flash
122
+ flash[:notice] = f.is_a?(Proc) ? instance_eval(&f) : options_for(action).flash
123
+ end
124
+ end
125
+
126
+ # Sets the flash.now (i.e. flash.now[:notice] = '...')
127
+ #
128
+ def set_flash_now(action)
129
+ if f = options_for(action).flash_now
130
+ flash.now[:notice] = f.is_a?(Proc) ? instance_eval(&f) : options_for(action).flash_now
131
+ end
132
+ end
133
+
134
+ # Returns the options for an action, which is a symbol.
135
+ #
136
+ # Manages splitting things like :create_fails.
137
+ #
138
+ def options_for(action)
139
+ action = action == :new_action ? [action] : "#{action}".split('_').map(&:to_sym)
140
+ options = self.class.send(action.first)
141
+ options = options.send(action.last == :fails ? :fails : :success) if Resourcelogic::Actions::FAILABLE_ACTIONS.include? action.first
142
+
143
+ options
144
+ end
145
+
146
+ def invoke_callbacks(*callbacks)
147
+ unless callbacks.empty?
148
+ callbacks.select { |callback| callback.is_a? Symbol }.each { |symbol| send(symbol) }
149
+
150
+ block = callbacks.detect { |callback| callback.is_a? Proc }
151
+ instance_eval &block unless block.nil?
152
+ end
153
+ end
154
+
155
+ def load_parent
156
+ instance_variable_set "@#{parent_model_name}", parent_object if parent?
157
+ end
158
+
159
+ # Used internally to load the member object in to an instance variable @#{model_name} (i.e. @post)
160
+ #
161
+ def load_object
162
+ load_parent
163
+ instance_variable_set "@#{object_name}", object
164
+ end
165
+
166
+ # Used internally to load the collection in to an instance variable @#{model_name.pluralize} (i.e. @posts)
167
+ #
168
+ def load_collection
169
+ load_parent
170
+ instance_variable_set "@#{object_name.to_s.pluralize}", collection
171
+ end
172
+ end
173
+ end
174
+ end