benhoskings-hammock 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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