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
@@ -0,0 +1,28 @@
1
+ module Hammock
2
+ module BufferedLoggerPatches
3
+ MixInto = ActiveSupport::BufferedLogger
4
+
5
+ def self.included base
6
+ base.send :include, InstanceMethods
7
+ base.send :extend, ClassMethods
8
+
9
+ # base.class_eval {
10
+ # alias_method_chain :fatal, :color
11
+ # }
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ end
17
+
18
+ module InstanceMethods
19
+
20
+ def fatal_with_color message = nil, progname = nil, &block
21
+ first_line, other_lines = message.strip.split("\n", 2)
22
+ fatal_without_color "\n" + first_line.colorize('on red'), progname, &block
23
+ fatal_without_color other_lines.colorize('red') + "\n\n", progname, &block
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ module Hammock
2
+ module ModulePatches
3
+ MixInto = Module
4
+ LoadFirst = true
5
+
6
+ def self.included base # :nodoc:
7
+ base.send :include, InstanceMethods
8
+ base.send :extend, ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ end
13
+
14
+ module InstanceMethods
15
+
16
+ def alias_method_chain_once target, feature
17
+ aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
18
+ without_method = "#{aliased_target}_without_#{feature}#{punctuation}"
19
+
20
+ unless [public_instance_methods, protected_instance_methods, private_instance_methods].flatten.include? without_method
21
+ alias_method_chain target, feature
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ module Hammock
2
+ module NumericPatches
3
+ MixInto = Numeric
4
+
5
+ def self.included base # :nodoc:
6
+ base.send :include, InstanceMethods
7
+ base.send :extend, ClassMethods # TODO maybe include in the metaclass instead of extending the class?
8
+
9
+ base.class_eval {
10
+ alias kb kilobytes
11
+ alias mb megabytes
12
+ alias gb gigabytes
13
+ alias tb terabytes
14
+ }
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ end
20
+
21
+ module InstanceMethods
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,61 @@
1
+ module Hammock
2
+ module ObjectPatches
3
+ MixInto = Object
4
+
5
+ def self.included base # :nodoc:
6
+ base.send :include, InstanceMethods
7
+ base.send :extend, ClassMethods
8
+
9
+ base.class_eval {
10
+ alias is_an? is_a?
11
+ }
12
+ end
13
+
14
+ module ClassMethods
15
+ end
16
+
17
+ module InstanceMethods
18
+
19
+ # Return +self+ after yielding to the given block.
20
+ #
21
+ # Useful for inline logging and diagnostics. Consider the following:
22
+ # @items.map {|i| process(i) }.join(", ")
23
+ # With +tap+, adding intermediate logging is simple:
24
+ # @items.map {|i| process(i) }.tap {|obj| log obj.inspect }.join(", ")
25
+ #--
26
+ # TODO Remove for Ruby 1.9
27
+ def tap
28
+ yield self
29
+ self
30
+ end
31
+
32
+ # The reverse of <tt>Enumerable#include?</tt> - returns +true+ if +self+ is
33
+ # equal to one of the elements of +args+.
34
+ def in? *args
35
+ args.include? self
36
+ end
37
+
38
+ # A symbolized, underscored (i.e. reverse-camelized) representation of +self+.
39
+ #
40
+ # Examples:
41
+ #
42
+ # Hash.symbolize #=> :hash
43
+ # ActiveRecord::Base.symbolize #=> :"active_record/base"
44
+ # "GetThisCamelOffMyCase".symbolize #=> :get_this_camel_off_my_case
45
+ def symbolize
46
+ self.to_s.underscore.to_sym
47
+ end
48
+
49
+ # If +condition+ evaluates to true, return the result of sending +method_name+ to +self+; <tt>*args</tt> to +self+, otherwise, return +self+ as-is.
50
+ def send_if condition, method_name, *args
51
+ condition ? send(method_name, *args) : self
52
+ end
53
+
54
+ # If +condition+ evaluates to true, return the result of sending +method_name+ to +self+; <tt>*args</tt> to +self+, otherwise, return +self+ as-is.
55
+ def send_if_respond_to method_name, *args
56
+ send_if respond_to?(method_name), method_name, *args
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,200 @@
1
+ module Hammock
2
+ module RouteSetPatches
3
+ MixInto = ActionController::Routing::RouteSet
4
+
5
+ def self.included base # :nodoc:
6
+ base.send :include, InstanceMethods
7
+ base.send :extend, ClassMethods
8
+
9
+ base.class_eval {
10
+ attr_accessor :route_map
11
+ }
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ end
17
+
18
+ module InstanceMethods
19
+
20
+ private
21
+
22
+ def initialize_hammock_route_map
23
+ self.route_map = HammockResource.new
24
+ end
25
+
26
+ class HammockResource < ActionController::Resources::Resource
27
+ class HammockRoutePiece
28
+ attr_reader :resource, :routeable_as, :verb, :entity, :parent
29
+
30
+ def initialize resource
31
+ @resource = resource
32
+ end
33
+
34
+ def for verb, entity
35
+ routeable_as = resource.routeable_as(verb, entity)
36
+
37
+ if !routeable_as
38
+ raise "The verb '#{verb}' can't be applied to " + (entity.record? ? "#{entity.resource} records" : "the #{entity.resource} resource") + "."
39
+ elsif (:record == routeable_as) && entity.new_record?
40
+ raise "The verb '#{verb}' requires a #{entity.resource} with an ID (i.e. not a new record)."
41
+ elsif (:build == routeable_as) && entity.record? && !entity.new_record?
42
+ raise "The verb '#{verb}' requires either the #{entity.resource} resource, or a #{entity.resource} without an ID (i.e. a new record)."
43
+ else
44
+ @verb, @entity, @routeable_as = verb, entity, routeable_as
45
+ end
46
+
47
+ self
48
+ end
49
+
50
+ def within parent
51
+ @parent = parent
52
+ self
53
+ end
54
+
55
+ def setup?
56
+ !@entity.nil?
57
+ end
58
+
59
+ def path params = nil
60
+ raise_unless_setup_while_trying_to 'render a path'
61
+
62
+ buf = '/'
63
+ buf << entity.resource_name
64
+ buf << '/' + entity.to_param if entity.record? && !entity.new_record?
65
+ buf << '/' + verb.to_s unless verb.nil? or implied_verb?(verb)
66
+
67
+ buf = parent.path + buf unless parent.nil?
68
+ buf << param_str(params)
69
+
70
+ buf
71
+ end
72
+
73
+ def http_method
74
+ raise_unless_setup_while_trying_to 'extract the HTTP method'
75
+ resource.send("#{routeable_as}_routes")[verb]
76
+ end
77
+
78
+ def fake_http_method
79
+ http_method.in?(:get, :post) ? http_method : :post
80
+ end
81
+
82
+ def get?; :get == http_method end
83
+ def post?; :post == http_method end
84
+ def put?; :put == http_method end
85
+ def delete?; :delete == http_method end
86
+
87
+ def safe?
88
+ get? && !verb.in?(Hammock::Constants::ImpliedUnsafeActions)
89
+ end
90
+
91
+ private
92
+
93
+ def implied_verb? verb
94
+ verb.in? :index, :create, :show, :update, :destroy
95
+ end
96
+
97
+ def raise_unless_setup_while_trying_to task
98
+ raise "You have to call for(verb, entity) (and optionally within(parent)) on this HammockRoutePiece before you can #{task}." unless setup?
99
+ end
100
+
101
+ def param_str params
102
+ link_params = entity.record? ? entity.unsaved_attributes.merge(params || {}) : params
103
+
104
+ if link_params.blank?
105
+ ''
106
+ else
107
+ '?' + {entity.base_model => link_params}.to_query
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ DefaultRecordVerbs = {
114
+ :show => :get,
115
+ :edit => :get,
116
+ :update => :put,
117
+ :destroy => :delete
118
+ }.freeze
119
+ DefaultResourceVerbs = {
120
+ :index => :get
121
+ }.freeze
122
+ DefaultBuildVerbs = {
123
+ :new => :get,
124
+ :create => :post
125
+ }.freeze
126
+
127
+ attr_reader :mdl, :parent, :children, :record_routes, :resource_routes, :build_routes
128
+
129
+ def initialize entity = nil, options = {}
130
+ @mdl = entity if entity.is_a?(Symbol)
131
+ @parent = options[:parent]
132
+ @children = {}
133
+ define_routes options
134
+ end
135
+
136
+ def ancestry
137
+ parent.nil? ? [] : parent.ancestry.push(self)
138
+ end
139
+
140
+ def for verb, entities, options
141
+ raise "HammockResource#for requires an explicitly specified verb as its first argument." unless verb.is_a?(Symbol)
142
+ raise "You have to supply at least one record or resource." if entities.empty?
143
+
144
+ entity = entities.shift
145
+
146
+ if entities.empty?
147
+ piece_for verb, entity
148
+ else
149
+ children[entity.resource_sym].for(verb, entities, options).within piece_for(nil, entity)
150
+ end
151
+ end
152
+
153
+ def add entity, options, steps = nil
154
+ if steps.nil?
155
+ add entity, options, (options[:name_prefix] || '').chomp('_').split('_').map {|i| i.pluralize.underscore.to_sym }
156
+ elsif steps.empty?
157
+ add_child entity, options
158
+ else
159
+ children[steps.shift].add entity, options, steps
160
+ end
161
+ end
162
+
163
+ def routeable_as verb, entity
164
+ if entity.record? && record_routes[verb || :show]
165
+ :record
166
+ elsif entity.resource? && resource_routes[verb || :index]
167
+ :resource
168
+ elsif !verb.nil? && build_routes[verb]
169
+ :build
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def define_routes options
176
+ @record_routes = DefaultRecordVerbs.dup.update(options[:member] || {})
177
+ @resource_routes = DefaultResourceVerbs.dup.update(options[:collection] || {})
178
+ @build_routes = DefaultBuildVerbs.dup.update(options[:build] || {})
179
+ end
180
+
181
+ def add_child entity, options
182
+ child = HammockResource.new entity, options.merge(:parent => self)
183
+ children[child.mdl] = child
184
+ end
185
+
186
+ def piece_for verb, entity
187
+ child = children[entity.resource_sym]
188
+
189
+ if child.nil?
190
+ raise "No routes are defined for #{entity.resource}#{' within ' + ancestry.map {|r| r.mdl.to_s }.join(', ') unless ancestry.empty?}."
191
+ else
192
+ HammockRoutePiece.new(child).for(verb, entity)
193
+ end
194
+ end
195
+
196
+ end
197
+
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,197 @@
1
+ module Hammock
2
+ module StringPatches
3
+ MixInto = String
4
+
5
+ def self.included base # :nodoc:
6
+ base.send :include, InstanceMethods
7
+ base.send :extend, ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ # Generates a random string consisting of +length+ hexadecimal characters (i.e. matching [0-9a-f]{length}).
13
+ def af09 length = 1
14
+ (1..length).inject('') {|a, t|
15
+ a << rand(16).to_s(16)
16
+ }
17
+ end
18
+
19
+ # Generates a random string consisting of +length+ alphamuneric characters (i.e. matching [0-9a-zA-Z]{length}).
20
+ def azAZ09 length = 1
21
+ (1..length).inject('') {|a, t|
22
+ a << ((r = rand(62)) < 36 ? r.to_s(36) : (r - 26).to_s(36).upcase)
23
+ }
24
+ end
25
+
26
+ end
27
+
28
+ module InstanceMethods
29
+
30
+ # Returns true iff +str+ appears exactly at the start of +self+.
31
+ def starts_with? str
32
+ self[0, str.length] == str
33
+ end
34
+
35
+ # Returns true iff +str+ appears exactly at the end of +self+.
36
+ def ends_with? str
37
+ self[-str.length, str.length] == str
38
+ end
39
+
40
+ # Return a duplicate of +self+, with +str+ prepended to it if it doesn't already start with +str+.
41
+ def start_with str
42
+ starts_with?(str) ? self : str + self
43
+ end
44
+
45
+ # Return a duplicate of +self+, with +str+ appended to it if it doesn't already end with +str+.
46
+ def end_with str
47
+ ends_with?(str) ? self : self + str
48
+ end
49
+
50
+ def possessive
51
+ "#{self}'#{'s' unless ends_with?('s')}"
52
+ end
53
+
54
+ # TODO any more to add?
55
+ NamePrefixes = %w[de den la von].freeze
56
+
57
+ def capitalize_name
58
+ split(' ').map {|term|
59
+ term.split('-').map {|term|
60
+ if NamePrefixes.include?(term)
61
+ term.downcase
62
+ elsif (term != term.downcase)
63
+ term
64
+ else # only capitalize words that are entirely lower case
65
+ term.capitalize
66
+ end
67
+ }.join('-')
68
+ }.join(' ')
69
+ end
70
+
71
+ def capitalize_name!
72
+ self.replace self.capitalize_name
73
+ end
74
+
75
+ # Returns whether this IP should be considered a valid one for a client to be using.
76
+ def valid_ip?
77
+ if production?
78
+ describe_as_ip == :public
79
+ else
80
+ describe_as_ip.in? :public, :private, :loopback
81
+ end
82
+ end
83
+
84
+ # Returns a symbol describing the class of IP address +self+ represents, if any.
85
+ #
86
+ # Examples:
87
+ #
88
+ # "Hello world!".valid_ip? #=> false
89
+ # "192.168.".valid_ip? #=> false
90
+ # "127.0.0.1".valid_ip? #=> :loopback
91
+ # "172.24.137.6".valid_ip? #=> :private
92
+ # "169.254.1.142".valid_ip? #=> :self_assigned
93
+ # "72.9.108.122".valid_ip? #=> :public
94
+ def describe_as_ip
95
+ parts = strip.split('.')
96
+ bytes = parts.zip(parts.map(&:to_i)).map {|(str,val)|
97
+ val if ((1..255) === val) || (val == 0 && str == '0')
98
+ }.squash
99
+
100
+ if bytes.length != 4
101
+ false
102
+ elsif bytes.starts_with? 0 # Source hosts on "this" network
103
+ :reserved
104
+ elsif bytes.starts_with? 127 # Loopback network; RFC1700
105
+ :loopback
106
+ elsif bytes.starts_with? 10 # Class-A private; RFC1918
107
+ :private
108
+ elsif bytes.starts_with?(172) && ((16..31) === bytes[1]) # Class-B private; RFC1918
109
+ :private
110
+ elsif bytes.starts_with? 169, 254 # Link-local range; RFC3330/3927
111
+ bytes[2].in?(0, 255) ? :reserved : :self_assigned
112
+ elsif bytes.starts_with? 192, 0, 2 # TEST-NET - used as example.com IP
113
+ :reserved
114
+ elsif bytes.starts_with? 192, 88, 99 # 6-to-4 relay anycast; RFC3068
115
+ :reserved
116
+ elsif bytes.starts_with? 192, 168 # Class-C private; RFC1918
117
+ :private
118
+ elsif bytes.starts_with? 198, 18 # Benchmarking; RFC2544
119
+ :reserved
120
+ else
121
+ :public
122
+ end
123
+ end
124
+
125
+ # Returns true if the string represents a valid email address.
126
+ def valid_email?
127
+ /^([a-z0-9\-\+\_\.]{2,})\@([a-z0-9\-]+\.)*([a-z0-9\-]{2,}\.)([a-z0-9\-]{2,})$/ =~ self
128
+ end
129
+
130
+ def colorize description = '', start_at = nil
131
+ if start_at.nil? || (cut_point = index(start_at)).nil?
132
+ Colorizer.colorize self, description
133
+ else
134
+ self[0...cut_point] + Colorizer.colorize(self[cut_point..-1], description)
135
+ end
136
+ end
137
+
138
+ def colorize! description = '', start_at = nil
139
+ replace colorize(description, start_at)
140
+ end
141
+
142
+ private
143
+
144
+ class Colorizer
145
+ HomeOffset = 29
146
+ LightOffset = 60
147
+ BGOffset = 10
148
+ LightRegex = /^light_/
149
+ ColorRegex = /^(light_)?none|gr[ae]y|red|green|yellow|blue|pink|cyan|white$/
150
+ CtrlRegex = /^bold|underlined?|blink(ing)?|reversed?$/
151
+ ColorOffsets = {
152
+ 'none' => 0,
153
+ 'gray' => 1, 'grey' => 1,
154
+ 'red' => 2,
155
+ 'green' => 3,
156
+ 'yellow' => 4,
157
+ 'blue' => 5,
158
+ 'pink' => 6,
159
+ 'cyan' => 7,
160
+ 'white' => 8
161
+ }
162
+ CtrlOffsets = {
163
+ 'bold' => 1,
164
+ 'underline' => 4, 'underlined' => 4,
165
+ 'blink' => 5, 'blinking' => 5,
166
+ 'reverse' => 7, 'reversed' => 7
167
+ }
168
+ class << self
169
+ def colorize text, description
170
+ terms = " #{description} ".gsub(' light ', ' light_').gsub(' on ', ' on_').strip.split(/\s+/)
171
+ bg = terms.detect {|i| /on_#{ColorRegex}/ =~ i }
172
+ fg = terms.detect {|i| ColorRegex =~ i }
173
+ ctrl = terms.detect {|i| CtrlRegex =~ i }
174
+
175
+ "\e[#{"0;#{fg_for(fg)};#{bg_for(bg) || ctrl_for(ctrl)}"}m#{text}\e[0m"
176
+ end
177
+
178
+ def fg_for name
179
+ light = name.gsub!(LightRegex, '') unless name.nil?
180
+ (ColorOffsets[name] || 0) + HomeOffset + (light ? LightOffset : 0)
181
+ end
182
+
183
+ def bg_for name
184
+ # There's a hole in the table on bg=none, so we use BGOffset to the left
185
+ offset = fg_for((name || '').sub(/^on_/, ''))
186
+ offset + BGOffset unless offset == HomeOffset
187
+ end
188
+
189
+ def ctrl_for name
190
+ CtrlOffsets[name] || HomeOffset
191
+ end
192
+ end
193
+ end
194
+
195
+ end
196
+ end
197
+ end