benhoskings-hammock 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/LICENSE +24 -0
  2. data/Manifest.txt +42 -0
  3. data/README.rdoc +105 -0
  4. data/Rakefile +27 -0
  5. data/lib/hammock.rb +25 -0
  6. data/lib/hammock/ajaxinate.rb +152 -0
  7. data/lib/hammock/callbacks.rb +107 -0
  8. data/lib/hammock/canned_scopes.rb +121 -0
  9. data/lib/hammock/constants.rb +7 -0
  10. data/lib/hammock/controller_attributes.rb +66 -0
  11. data/lib/hammock/export_scope.rb +74 -0
  12. data/lib/hammock/hamlink_to.rb +47 -0
  13. data/lib/hammock/javascript_buffer.rb +63 -0
  14. data/lib/hammock/logging.rb +98 -0
  15. data/lib/hammock/model_attributes.rb +38 -0
  16. data/lib/hammock/model_logging.rb +30 -0
  17. data/lib/hammock/monkey_patches/action_pack.rb +32 -0
  18. data/lib/hammock/monkey_patches/active_record.rb +227 -0
  19. data/lib/hammock/monkey_patches/array.rb +73 -0
  20. data/lib/hammock/monkey_patches/hash.rb +49 -0
  21. data/lib/hammock/monkey_patches/logger.rb +28 -0
  22. data/lib/hammock/monkey_patches/module.rb +27 -0
  23. data/lib/hammock/monkey_patches/numeric.rb +25 -0
  24. data/lib/hammock/monkey_patches/object.rb +61 -0
  25. data/lib/hammock/monkey_patches/route_set.rb +200 -0
  26. data/lib/hammock/monkey_patches/string.rb +197 -0
  27. data/lib/hammock/overrides.rb +32 -0
  28. data/lib/hammock/resource_mapping_hooks.rb +28 -0
  29. data/lib/hammock/resource_retrieval.rb +115 -0
  30. data/lib/hammock/restful_actions.rb +170 -0
  31. data/lib/hammock/restful_rendering.rb +114 -0
  32. data/lib/hammock/restful_support.rb +167 -0
  33. data/lib/hammock/route_drawing_hooks.rb +22 -0
  34. data/lib/hammock/route_for.rb +58 -0
  35. data/lib/hammock/scope.rb +120 -0
  36. data/lib/hammock/suggest.rb +36 -0
  37. data/lib/hammock/utils.rb +42 -0
  38. data/misc/scaffold.txt +83 -0
  39. data/misc/template.rb +17 -0
  40. data/tasks/hammock_tasks.rake +5 -0
  41. data/test/hammock_test.rb +8 -0
  42. metadata +129 -0
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2008 Ben Hoskings <ben@hoskings.net>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of Ben Hoskings nor the names of any of the software's
12
+ contributors may be used to endorse or promote products derived from
13
+ this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
16
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/Manifest.txt ADDED
@@ -0,0 +1,42 @@
1
+ History.txt
2
+ LICENSE
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ lib/hammock.rb
7
+ lib/hammock/ajaxinate.rb
8
+ lib/hammock/callbacks.rb
9
+ lib/hammock/canned_scopes.rb
10
+ lib/hammock/constants.rb
11
+ lib/hammock/controller_attributes.rb
12
+ lib/hammock/export_scope.rb
13
+ lib/hammock/hamlink_to.rb
14
+ lib/hammock/javascript_buffer.rb
15
+ lib/hammock/logging.rb
16
+ lib/hammock/model_attributes.rb
17
+ lib/hammock/model_logging.rb
18
+ lib/hammock/monkey_patches/action_pack.rb
19
+ lib/hammock/monkey_patches/active_record.rb
20
+ lib/hammock/monkey_patches/array.rb
21
+ lib/hammock/monkey_patches/hash.rb
22
+ lib/hammock/monkey_patches/logger.rb
23
+ lib/hammock/monkey_patches/module.rb
24
+ lib/hammock/monkey_patches/numeric.rb
25
+ lib/hammock/monkey_patches/object.rb
26
+ lib/hammock/monkey_patches/route_set.rb
27
+ lib/hammock/monkey_patches/string.rb
28
+ lib/hammock/overrides.rb
29
+ lib/hammock/resource_mapping_hooks.rb
30
+ lib/hammock/resource_retrieval.rb
31
+ lib/hammock/restful_actions.rb
32
+ lib/hammock/restful_rendering.rb
33
+ lib/hammock/restful_support.rb
34
+ lib/hammock/route_drawing_hooks.rb
35
+ lib/hammock/route_for.rb
36
+ lib/hammock/scope.rb
37
+ lib/hammock/suggest.rb
38
+ lib/hammock/utils.rb
39
+ misc/scaffold.txt
40
+ misc/template.rb
41
+ tasks/hammock_tasks.rake
42
+ test/hammock_test.rb
data/README.rdoc ADDED
@@ -0,0 +1,105 @@
1
+ = hammock
2
+
3
+ http://github.com/benhoskings/hammock
4
+
5
+
6
+ == DESCRIPTION:
7
+
8
+ Hammock is a Rails plugin that eliminates redundant code in a very RESTful manner. It does this in lots in lots of different places, but in one manner: it encourages specification in place of implementation.
9
+
10
+
11
+ Hammock enforces RESTful resource access by abstracting actions away from the controller in favour of a clean, model-like callback system.
12
+
13
+ Hammock tackles the hard and soft sides of security at once with a scoping security system on your models. Specify who can verb what resources under what conditions once, and everything else - the actual security, link generation, index filtering - just happens.
14
+
15
+ Hammock inspects your routes and resources to generate a routing tree for each resource. Parent resources in a nested route are handled transparently at every point - record retrieval, creation, and linking.
16
+
17
+ It makes more sense when you see how it works though, so check out the screencast!
18
+
19
+
20
+ == REQUIREMENTS:
21
+
22
+ benhoskings-ambition
23
+ benhoskings-ambitious-activerecord
24
+
25
+
26
+ == INSTALL:
27
+
28
+ sudo gem install benhoskings-hammock --source http://gems.github.com
29
+
30
+ class ApplicationController
31
+ include Hammock
32
+ ...
33
+
34
+
35
+ == LICENSE:
36
+
37
+ Hammock is licensed under the BSD license, which can be found in full in the LICENSE file.
38
+
39
+
40
+ == SYNOPSIS
41
+
42
+ At the moment, you can do this with Hammock:
43
+
44
+ class ApplicationController < ActionController::Base
45
+ include Hammock
46
+ end
47
+
48
+ class BeersController < ApplicationController
49
+ end
50
+
51
+ class Person < ActiveRecord::Base
52
+ end
53
+
54
+ class Beer < ActiveRecord::Base
55
+ belongs_to :creator, :class_name => 'Person'
56
+ belongs_to :recipient, :class_name => 'Person'
57
+
58
+ def read_scope_for account
59
+ proc {|beer| beer.creator_id == account.id || beer.recipient_id == account.id }
60
+ end
61
+ export_scope :read
62
+ export_scope :read, :as => :index
63
+
64
+ def write_scope_for account
65
+ proc {|beer| record.creator_id == account.id }
66
+ end
67
+ export_scope :write
68
+ end
69
+
70
+ <% @beers.each do |beer| %>
71
+ From <%= beer.creator.name %> to <%= beer.recipient.name %>, <%= beer.reason %>, rated <%= beer.rating %>
72
+ <%= hamlink_to :edit, beer %>
73
+ <% end %>
74
+
75
+ The scope methods above require just one thing -- a context-free proc object that takes an ActiveRecord record as its argument, and returns true iff that record is within the scope for the specified account. Hammock uses the method (e.g. Beer.read_scope_for) to define resource and record scopes for the model:
76
+
77
+ Beer.readable_by(account): the set of Beer records whose existence can be known by account
78
+ Beer#readable_by?(account): returns true if the existence of this Beer instance can be known by account
79
+
80
+ You define the logic for read, index and write scopes in Beer.[read,index,write]_scope_for, and the rest just works.
81
+
82
+ These scope definitions are exploited extensively, to provide index selection, scoping for record selection, and post-selection object checks.
83
+
84
+ - They provide the conditions that should be applied to retrieve the index of each resource. The scope is used transperently by Hammock on /beers -> BeersController#index, and is available for use through Beer.indexable_by(account).
85
+
86
+ - They provide a scope within which records are searched for on single-record actions. For example, given the request /beers/5 -> BeersController#show{:id => 5}, Rails would generate the following SQL:
87
+
88
+ SELECT * FROM "beers" WHERE (beers."id" = 5) LIMIT 1
89
+
90
+ Hammock uses the conditions specified in Beer.read_scope_for to generate (assuming an account_id of 3):
91
+
92
+ SELECT * FROM "beers" WHERE ((beers.creator_id = 3 OR beers.recipient_id = 3) AND beers."id" = 5) LIMIT 1
93
+
94
+ Hammock uses Beer.read_scope_for on #show, and write_scope_for on #edit, #update and #destroy. These scopes can be accessed as above through Beer.readable_by(account) and Beer.writeable_by(account). This eliminates authorization checks from the action, because if the ID of a Beer is provided that the user doesn't have access to it will fall outside the scope and will not be found in the DB at all.
95
+
96
+ - They are used to discover credentials for already-queried ActiveRecord objects, without touching the database again. Just as Beer.readable_by(account) returns the set of Beer records whose existence can be known by account, @beer.readable_by?(account) returns true iff @beer's existence can be known by account. This is employed by hamlink_to.
97
+
98
+ These three uses of the scope, plus another as-yet unimplemented bit, provide the entire security model of the application.
99
+
100
+ == THE MASTER PLAN
101
+
102
+ Lots of functionality is planned that will take this much further.
103
+
104
+
105
+
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/hammock'
3
+
4
+ # Generate all the Rake tasks
5
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
+ $hoe = Hoe.new('hammock', Hammock::VERSION) do |p|
7
+ p.developer('Ben Hoskings', 'ben@hoskings.net')
8
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
+ p.rubyforge_name = p.name
10
+ p.extra_deps = [
11
+ ['benhoskings-ambitious-activerecord','>= 0.1.3.4'],
12
+ ]
13
+ p.extra_dev_deps = [
14
+ ['newgem', ">= #{::Newgem::VERSION}"]
15
+ ]
16
+
17
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
18
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
19
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
20
+ p.rsync_args = '-av --delete --ignore-errors'
21
+ end
22
+
23
+ require 'newgem/tasks' # load /tasks/*.rake
24
+ Dir['tasks/**/*.rake'].each { |t| load t }
25
+
26
+ # TODO - want other tests/tasks run by default? Add them to the list
27
+ # task :default => [:spec, :features]
data/lib/hammock.rb ADDED
@@ -0,0 +1,25 @@
1
+ gem 'benhoskings-ambition'
2
+ gem 'benhoskings-ambitious-activerecord'
3
+ require 'ambition'
4
+ require 'ambition/adapters/active_record'
5
+
6
+ Dir.glob("#{File.dirname __FILE__}/hammock/**/*.rb").each {|dep|
7
+ require dep
8
+ } if defined?(RAILS_ROOT) # Loading Hammock components under 'rake package' fails.
9
+
10
+ module Hammock
11
+ VERSION = '0.2.4'
12
+
13
+ def self.included base # :nodoc:
14
+ Hammock.constants.map {|constant_name|
15
+ Hammock.const_get constant_name
16
+ }.select {|constant|
17
+ constant.is_a? Module
18
+ }.partition {|mod|
19
+ mod.constants.include?('LoadFirst') && mod::LoadFirst
20
+ }.flatten.each {|mod|
21
+ target = mod.constants.include?('MixInto') ? mod::MixInto : base
22
+ target.send :include, mod
23
+ }
24
+ end
25
+ end
@@ -0,0 +1,152 @@
1
+ module Hammock
2
+ module Ajaxinate
3
+ MixInto = ActionView::Base
4
+
5
+ def self.included base # :nodoc:
6
+ base.send :include, InstanceMethods
7
+ base.send :extend, ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ end
12
+
13
+ module InstanceMethods
14
+
15
+ def ajax_button verb, record, opts = {}
16
+ ajax_link verb, record, opts.merge(:class => [opts[:class], 'button'].squash.join(' '))
17
+ end
18
+
19
+ def ajax_link verb, record, opts = {}
20
+ if can_verb_entity?(verb, record)
21
+ route = ajaxinate verb, record, opts
22
+
23
+ content_tag :a,
24
+ opts[:text] || route.verb.to_s.capitalize,
25
+ :class => [opts[:class], link_class_for(route.verb, record)].squash.join(' '),
26
+ :href => route.path,
27
+ :onclick => 'return false;',
28
+ :style => opts[:style]
29
+ end
30
+ end
31
+
32
+ def ajaxinate verb, record, opts = {}
33
+ record_attributes = {record.base_model => record.unsaved_attributes}
34
+ link_params = {record.base_model => (opts.delete(:record) || {}) }.merge(opts[:params] || {})
35
+ route = route_for verb, record
36
+ attribute = link_params[:attribute]
37
+ link_class = link_class_for route.verb, record, attribute
38
+
39
+ link_params[:_method] = route.http_method
40
+ link_params[:format] = opts[:format].to_s
41
+
42
+ form_elements_hash = if route.get?
43
+ '{ }'
44
+ elsif attribute.blank?
45
+ "jQuery('form').serializeHash()"
46
+ else
47
+ "{ '#{record.base_model}[#{attribute}]': $('.#{link_class}').val() }"
48
+ end
49
+
50
+ response_action = case link_params[:format].to_s
51
+ when 'js'
52
+ "eval(response)"
53
+ else
54
+ "jQuery('.#{opts[:target] || link_class + '_target'}').before(response).remove()"
55
+ end
56
+
57
+ # TODO check the response code in the callback, and replace :after with :success and :failure.
58
+ js = %Q{
59
+ jQuery('.#{link_class}').#{opts[:on] || 'click'}(function() {
60
+ /*if (#{attribute.blank? ? 'false' : 'true'} && (jQuery('.#{link_class}_target .original_value').html() == jQuery('.#{link_class}_target .modify input').val())) {
61
+ eval("#{clean_snippet opts[:skipped]}");
62
+ } else*/ if (false == eval("#{clean_snippet opts[:before]}")) {
63
+ // before callback failed
64
+ } else { // fire the request
65
+ jQuery.#{route.fake_http_method}(
66
+ '#{route.path}',
67
+ jQuery.extend(
68
+ #{record_attributes.to_flattened_json},
69
+ #{form_elements_hash},
70
+ #{link_params.to_flattened_json},
71
+ #{forgery_key_json(route.http_method)}
72
+ ),
73
+ function(response) {
74
+ #{response_action};
75
+ eval("#{clean_snippet opts[:after]}");
76
+ }
77
+ );
78
+ }
79
+ });
80
+ }
81
+
82
+ append_javascript js
83
+ route
84
+ end
85
+
86
+ def status_callback
87
+ %Q{
88
+ if ('success' == textStatus) {
89
+ jQuery('.success', jQuery('#' + jQuery(data).attr("id"))).show().fadeOut(4000);
90
+ } else {
91
+ jQuery('.statuses .failure', obj).hide();
92
+ jQuery('.statuses .failure', obj).show().parents('obj').BlindUp();
93
+ }
94
+ }
95
+ end
96
+
97
+ def jquery_xhr verb, record, opts = {}
98
+ route = route_for verb, record
99
+ params = if opts[:params].is_a?(String)
100
+ opts[:params].chomp(',').start_with('{').end_with('}')
101
+ else
102
+ (opts[:params] || {}).merge(record.base_model => (opts[:record] || {})).to_flattened_json
103
+ end
104
+
105
+ response_action = case opts[:format].to_s
106
+ when 'js'
107
+ "eval(data);"
108
+ else
109
+ "obj.replaceWith(data);"
110
+ end
111
+
112
+ %Q{
113
+ jQuery('.spinner', obj).show();
114
+
115
+ jQuery.#{route.fake_http_method}(
116
+ '#{route.path}',
117
+ jQuery.extend(
118
+ #{params},
119
+ {format: '#{opts[:format] || 'html'}', _method: '#{route.http_method}'},
120
+ #{forgery_key_json(route.http_method)}
121
+ ),
122
+ function(data, textStatus) {
123
+ #{response_action}
124
+ #{status_callback}
125
+ #{(opts[:callback] || '').end_with(';')}
126
+ }
127
+ );
128
+ }
129
+ end
130
+
131
+ private
132
+
133
+ def link_class_for verb, record, attribute = nil
134
+ [verb, record.base_model, record.id, attribute].compact.join('_')
135
+ end
136
+
137
+ def clean_snippet snippet
138
+ report "Double quote detected in snippet '#{snippet}'" if snippet['"'] unless snippet.nil?
139
+ (snippet || '').gsub("\n", '\n').end_with(';')
140
+ end
141
+
142
+ def forgery_key_json request_method = nil
143
+ if !protect_against_forgery? || (:get == (request_method || request.method))
144
+ '{ }'
145
+ else
146
+ "{ '#{request_forgery_protection_token}': encodeURIComponent('#{escape_javascript(form_authenticity_token)}') }"
147
+ end
148
+ end
149
+
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,107 @@
1
+ module Hammock
2
+ module Callbacks
3
+ LoadFirst = true
4
+
5
+ def self.included base # :nodoc:
6
+ base.send :include, InstanceMethods
7
+ base.send :extend, ClassMethods
8
+
9
+ base.class_eval {
10
+ include ActiveSupport::Callbacks
11
+
12
+ define_hammock_callbacks *%w[
13
+ before_find during_find after_failed_find
14
+
15
+ before_index before_show
16
+ before_modify before_new before_edit
17
+
18
+ before_save after_save after_failed_save
19
+ before_create after_create after_failed_create
20
+ before_update after_update after_failed_update
21
+ before_destroy after_destroy
22
+ before_undestroy after_undestroy
23
+
24
+ before_suggest after_suggest
25
+ ]
26
+ }
27
+ end
28
+
29
+ module ClassMethods
30
+
31
+ class HammockCallback < ActiveSupport::Callbacks::Callback
32
+ private
33
+
34
+ def evaluate_method method, *args, &block
35
+ if method.is_a? Proc
36
+ # puts "was a HammockCallback proc within #{args.first.class}."
37
+ method.bind(args.shift).call(*args, &block)
38
+ else
39
+ super
40
+ end
41
+ end
42
+ end
43
+
44
+ def define_hammock_callbacks *callbacks
45
+ callbacks.each do |callback|
46
+ class_eval <<-"end_eval"
47
+ def self.#{callback}(*methods, &block)
48
+ callbacks = if !block_given? || methods.length > 1
49
+ CallbackChain.build(:#{callback}, *methods, &block)
50
+ else # hammock-style callback
51
+ if methods.empty?
52
+ log "<-- you should give this callback a description", :skip => 1
53
+ elsif !methods.first.is_a?(String)
54
+ raise ArgumentError, "Inline callback definitions require a description as their sole argument."
55
+ else
56
+ # logger.info "defining \#{methods.first} on \#{name} with method \#{block.inspect}."
57
+ [HammockCallback.new(:#{callback}, block, :identifier => methods.first)]
58
+ end || []
59
+ end
60
+ # log callbacks
61
+ (@#{callback}_callbacks ||= CallbackChain.new).concat callbacks
62
+ end
63
+
64
+ def self.#{callback}_callback_chain
65
+ @#{callback}_callbacks ||= CallbackChain.new
66
+
67
+ if superclass.respond_to?(:#{callback}_callback_chain)
68
+ CallbackChain.new(superclass.#{callback}_callback_chain + @#{callback}_callbacks)
69
+ else
70
+ @#{callback}_callbacks
71
+ end
72
+ end
73
+ end_eval
74
+ end
75
+ end
76
+
77
+ end
78
+
79
+ module InstanceMethods
80
+ private
81
+
82
+ CallbackFail = false
83
+
84
+ def callback kind, *args
85
+ callback_chain_for(kind).all? {|cb|
86
+ # dlog "Calling #{kind} callback #{cb.method}"
87
+ result = cb.call(self, *args) != CallbackFail
88
+ log "#{self.class}.#{cb.kind} callback '#{cb.method}' failed." unless result
89
+ result
90
+ }
91
+ end
92
+
93
+ def required_callback kind, *args
94
+ callback(kind, *args) if has_callbacks_for?(kind)
95
+ end
96
+
97
+ def has_callbacks_for? kind
98
+ !callback_chain_for(kind).empty?
99
+ end
100
+
101
+ def callback_chain_for kind
102
+ self.class.send "#{kind}_callback_chain"
103
+ end
104
+
105
+ end
106
+ end
107
+ end