hammock 0.2.11.2
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.
- data/History.txt +67 -0
- data/LICENSE +24 -0
- data/Manifest.txt +46 -0
- data/README.rdoc +105 -0
- data/Rakefile +29 -0
- data/hammock.gemspec +43 -0
- data/lib/hammock/ajaxinate.rb +154 -0
- data/lib/hammock/callback.rb +16 -0
- data/lib/hammock/callbacks.rb +94 -0
- data/lib/hammock/canned_scopes.rb +125 -0
- data/lib/hammock/constants.rb +9 -0
- data/lib/hammock/controller_attributes.rb +52 -0
- data/lib/hammock/export_scope.rb +74 -0
- data/lib/hammock/hamlink_to.rb +47 -0
- data/lib/hammock/javascript_buffer.rb +63 -0
- data/lib/hammock/logging.rb +98 -0
- data/lib/hammock/model_attributes.rb +53 -0
- data/lib/hammock/model_logging.rb +30 -0
- data/lib/hammock/monkey_patches/action_pack.rb +32 -0
- data/lib/hammock/monkey_patches/active_record.rb +231 -0
- data/lib/hammock/monkey_patches/array.rb +73 -0
- data/lib/hammock/monkey_patches/hash.rb +57 -0
- data/lib/hammock/monkey_patches/logger.rb +28 -0
- data/lib/hammock/monkey_patches/module.rb +27 -0
- data/lib/hammock/monkey_patches/numeric.rb +25 -0
- data/lib/hammock/monkey_patches/object.rb +61 -0
- data/lib/hammock/monkey_patches/route_set.rb +28 -0
- data/lib/hammock/monkey_patches/string.rb +197 -0
- data/lib/hammock/overrides.rb +36 -0
- data/lib/hammock/resource_mapping_hooks.rb +28 -0
- data/lib/hammock/resource_retrieval.rb +119 -0
- data/lib/hammock/restful_actions.rb +172 -0
- data/lib/hammock/restful_rendering.rb +114 -0
- data/lib/hammock/restful_support.rb +186 -0
- data/lib/hammock/route_drawing_hooks.rb +22 -0
- data/lib/hammock/route_for.rb +59 -0
- data/lib/hammock/route_node.rb +159 -0
- data/lib/hammock/route_step.rb +87 -0
- data/lib/hammock/scope.rb +127 -0
- data/lib/hammock/suggest.rb +36 -0
- data/lib/hammock/utils.rb +42 -0
- data/lib/hammock.rb +29 -0
- data/misc/scaffold.txt +83 -0
- data/misc/template.rb +17 -0
- data/tasks/hammock_tasks.rake +5 -0
- data/test/hammock_test.rb +8 -0
- metadata +142 -0
@@ -0,0 +1,231 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ActiveRecordPatches
|
3
|
+
MixInto = ActiveRecord::Base
|
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
|
+
%w[before_undestroy after_undestroy].each {|callback_name|
|
10
|
+
MixInto.define_callbacks callback_name
|
11
|
+
# base.send :define_method, callback_name, lambda { }
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
def new_with attributes
|
18
|
+
default_attributes.merge(attributes).inject(new) {|record,(k,v)|
|
19
|
+
record.send "#{k}=", v
|
20
|
+
record
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def sorter
|
25
|
+
# TODO updated_at DESC
|
26
|
+
proc {|record| record.id }
|
27
|
+
end
|
28
|
+
|
29
|
+
def resource
|
30
|
+
base_class
|
31
|
+
end
|
32
|
+
|
33
|
+
def resource_sym
|
34
|
+
resource_name.to_sym
|
35
|
+
end
|
36
|
+
|
37
|
+
def resource_name
|
38
|
+
# TODO almost certainly a better way to do this
|
39
|
+
base_class.to_s.pluralize.underscore
|
40
|
+
end
|
41
|
+
|
42
|
+
def param_key
|
43
|
+
"#{base_model}_id"
|
44
|
+
end
|
45
|
+
|
46
|
+
def base_model
|
47
|
+
base_class.to_s.underscore
|
48
|
+
end
|
49
|
+
|
50
|
+
def description
|
51
|
+
base_model
|
52
|
+
end
|
53
|
+
|
54
|
+
def record?; false end
|
55
|
+
def resource?; true end
|
56
|
+
|
57
|
+
def update_statement set_clause, where_clause
|
58
|
+
statement = "UPDATE #{table_name} SET #{set_clause} WHERE #{send :sanitize_sql_array, where_clause}"
|
59
|
+
connection.update statement
|
60
|
+
end
|
61
|
+
|
62
|
+
def reset_cached_column_info
|
63
|
+
reset_column_information
|
64
|
+
subclasses.each &:reset_cached_column_info
|
65
|
+
end
|
66
|
+
|
67
|
+
def find_or_new_with(find_attributes, create_attributes = {})
|
68
|
+
finder = respond_to?(:find_with_deleted) ? :find_with_deleted : :find
|
69
|
+
|
70
|
+
if record = send(finder, :first, :conditions => find_attributes.discard(:deleted_at))
|
71
|
+
# Found the record, so we can return it, if:
|
72
|
+
# (a) the record can't have a stored deletion state,
|
73
|
+
# (b) it can, but it's not actually deleted,
|
74
|
+
# (c) it is deleted, but we want to find one that's deleted, or
|
75
|
+
# (d) we don't want a deleted record, and undestruction succeeds.
|
76
|
+
if (finder != :find_with_deleted) || !record.deleted? || create_attributes[:deleted_at] || record.undestroy
|
77
|
+
record
|
78
|
+
end
|
79
|
+
else
|
80
|
+
creating_class = if create_attributes[:type].is_a?(ActiveRecord::Base)
|
81
|
+
create_attributes.delete(:type)
|
82
|
+
else
|
83
|
+
self
|
84
|
+
end
|
85
|
+
creating_class.new_with create_attributes.merge(find_attributes)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def find_or_create_with(find_attributes, create_attributes = {}, adjust_attributes = false)
|
90
|
+
if record = find_or_new_with(find_attributes, create_attributes)
|
91
|
+
log "Create failed. #{record.errors.inspect}", :skip => 1 if record.new_record? && !record.save
|
92
|
+
log "Adjust failed. #{record.errors.inspect}", :skip => 1 if adjust_attributes && !record.adjust(create_attributes)
|
93
|
+
record
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# def find_or_create_with! find_attributes, create_attributes = {}, adjust_attributes = false)
|
98
|
+
# record = find_or_new_with find_attributes, create_attributes, adjust_attributes
|
99
|
+
# record.valid? ? record : raise("Save failed. #{record.errors.inspect}")
|
100
|
+
# end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
module InstanceMethods
|
105
|
+
|
106
|
+
def concise_inspect
|
107
|
+
"#{self.class}<#{self.to_param || 'new'}>"
|
108
|
+
end
|
109
|
+
|
110
|
+
def resource
|
111
|
+
self.class.resource
|
112
|
+
end
|
113
|
+
|
114
|
+
def resource_sym
|
115
|
+
self.class.resource_sym
|
116
|
+
end
|
117
|
+
|
118
|
+
def resource_name
|
119
|
+
self.class.resource_name
|
120
|
+
end
|
121
|
+
|
122
|
+
def record?; true end
|
123
|
+
def resource?; false end
|
124
|
+
|
125
|
+
def id_str
|
126
|
+
if new_record?
|
127
|
+
"new_#{base_model}"
|
128
|
+
else
|
129
|
+
"#{base_model}_#{id}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def description
|
134
|
+
new_record? ? "new_#{base_model}" : "#{base_model}_#{id}"
|
135
|
+
end
|
136
|
+
|
137
|
+
def base_model
|
138
|
+
self.class.base_model
|
139
|
+
end
|
140
|
+
|
141
|
+
def new_or_deleted_before_save?
|
142
|
+
@new_or_deleted_before_save
|
143
|
+
end
|
144
|
+
def set_new_or_deleted_before_save
|
145
|
+
@new_or_deleted_before_save = new_record? || send_if_respond_to(:deleted?)
|
146
|
+
end
|
147
|
+
|
148
|
+
def undestroy
|
149
|
+
unless new_record?
|
150
|
+
if frozen?
|
151
|
+
self.class.find_with_deleted(self.id).undestroy # Re-fetch ourselves and undestroy the thawed copy
|
152
|
+
else
|
153
|
+
# We can undestroy
|
154
|
+
return false if callback(:before_undestroy) == false
|
155
|
+
result = self.class.update_all ['deleted_at = ?', (self.deleted_at = nil)], ['id = ?', self.id]
|
156
|
+
callback(:after_undestroy)
|
157
|
+
self if result != false
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Updates each given attribute to the current time.
|
163
|
+
#
|
164
|
+
# Assumes that each column can accept a +Time+ instance, i.e. that they're all +datetime+ columns or similar.
|
165
|
+
#
|
166
|
+
# The updates are done with update_attribute, and as such they are done with callbacks but
|
167
|
+
# without validation.
|
168
|
+
def touch *attrs
|
169
|
+
now = Time.now
|
170
|
+
attrs.each {|attribute|
|
171
|
+
update_attribute attribute, now
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
# Updates each given attribute to the current time, skipping attributes that are already set.
|
176
|
+
#
|
177
|
+
# Assumes that each column can accept a +Time+ instance, i.e. that they're all +datetime+ columns or similar.
|
178
|
+
#
|
179
|
+
# The updates are done with update_attribute, and as such they are done with callbacks but
|
180
|
+
# without validation.
|
181
|
+
def touch_once *attrs
|
182
|
+
touch *attrs.select {|attribute| attributes[attribute.to_s].nil? }
|
183
|
+
end
|
184
|
+
|
185
|
+
def adjust attrs
|
186
|
+
attrs.each {|k,v| send "#{k}=", v }
|
187
|
+
save false
|
188
|
+
end
|
189
|
+
|
190
|
+
def unsaved_attributes
|
191
|
+
self.changed.inject({}) {|hsh,k|
|
192
|
+
hsh[k] = attributes[k]
|
193
|
+
hsh
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
# Offset +attribute+ by +offset+ atomically in SQL.
|
198
|
+
def offset! attribute, offset
|
199
|
+
if new_record?
|
200
|
+
log "Can't offset! a new record."
|
201
|
+
else
|
202
|
+
# Update the in-memory model
|
203
|
+
send "#{attribute}=", send(attribute) + offset
|
204
|
+
# Update the DB
|
205
|
+
run_updater_sql 'Offset', "#{connection.quote_column_name(attribute)} = #{connection.quote_column_name(attribute)} + #{quote_value(offset)}"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def run_updater_sql logger_prefix, set_clause
|
213
|
+
connection.update(
|
214
|
+
"UPDATE #{self.class.table_name} " +
|
215
|
+
"SET #{set_clause} " +
|
216
|
+
"WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}",
|
217
|
+
|
218
|
+
"#{self.class.name} #{logger_prefix}"
|
219
|
+
) != false
|
220
|
+
end
|
221
|
+
|
222
|
+
# TODO Use ambition for association queries.
|
223
|
+
# def self.collection_reader_method reflection, association_proxy_class
|
224
|
+
# define_method(reflection.name) do |*params|
|
225
|
+
# reflection.klass.ambition_context.select { |entity| entity.__send__(reflection.primary_key_name) == quoted_id }
|
226
|
+
# end
|
227
|
+
# end
|
228
|
+
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ArrayPatches
|
3
|
+
MixInto = Array
|
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
|
+
# Returns true iff +other+ appears exactly at the start of +self+.
|
16
|
+
def starts_with? *other
|
17
|
+
self[0, other.length] == other
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns true iff +other+ appears exactly at the end of +self+.
|
21
|
+
def ends_with? *other
|
22
|
+
self[-other.length, other.length] == other
|
23
|
+
end
|
24
|
+
|
25
|
+
def squash
|
26
|
+
self.dup.squash!
|
27
|
+
end
|
28
|
+
def squash!
|
29
|
+
self.delete_if &:blank?
|
30
|
+
end
|
31
|
+
|
32
|
+
def discard *args
|
33
|
+
self.dup.discard! *args
|
34
|
+
end
|
35
|
+
def discard! *args
|
36
|
+
args.each {|arg| self.delete arg }
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def as_index_for &value_function
|
41
|
+
inject({}) do |accum, elem|
|
42
|
+
accum[elem] = value_function.call(elem)
|
43
|
+
accum
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def remove_framework_backtrace
|
48
|
+
reverse.drop_while {|step|
|
49
|
+
!step.starts_with?(RAILS_ROOT)
|
50
|
+
}.reverse
|
51
|
+
end
|
52
|
+
|
53
|
+
def hash_by *methods, &block
|
54
|
+
hsh = Hash.new {|h,k| h[k] = [] }
|
55
|
+
this_method = methods.shift
|
56
|
+
|
57
|
+
# First, hash this array into +hsh+.
|
58
|
+
self.each {|i| hsh[i.send(this_method)] << i }
|
59
|
+
|
60
|
+
if methods.empty?
|
61
|
+
# If there are no methods remaining, yield this group to the block if required.
|
62
|
+
hsh.each_pair {|k,v| hsh[k] = yield(hsh[k]) } if block_given?
|
63
|
+
else
|
64
|
+
# Recursively hash remaining methods.
|
65
|
+
hsh.each_pair {|k,v| hsh[k] = v.hash_by(*methods, &block) }
|
66
|
+
end
|
67
|
+
|
68
|
+
hsh
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Hammock
|
2
|
+
module HashPatches
|
3
|
+
MixInto = Hash
|
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 discard! *keys
|
16
|
+
keys.each {|k| delete k }
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def discard *keys
|
21
|
+
dup.discard! *keys
|
22
|
+
end
|
23
|
+
|
24
|
+
def selekt &block
|
25
|
+
hsh = {}
|
26
|
+
each_pair {|k,v|
|
27
|
+
hsh[k] = v if yield(k,v)
|
28
|
+
}
|
29
|
+
hsh
|
30
|
+
end
|
31
|
+
|
32
|
+
def dragnet *keys
|
33
|
+
dup.dragnet! *keys
|
34
|
+
end
|
35
|
+
|
36
|
+
def dragnet! *keys
|
37
|
+
keys.inject({}) {|acc,key|
|
38
|
+
acc[key] = self.delete(key) if self.has_key?(key)
|
39
|
+
acc
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_param_hash prefix = ''
|
44
|
+
hsh = self.dup
|
45
|
+
# TODO these two blocks can probably be combined
|
46
|
+
hsh.keys.each {|k| hsh.merge!(hsh.delete(k).to_param_hash(k)) if hsh[k].is_a?(Hash) }
|
47
|
+
hsh.keys.each {|k| hsh["#{prefix}[#{k}]"] = hsh.delete(k) } unless prefix.blank?
|
48
|
+
hsh
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_flattened_json
|
52
|
+
to_param_hash.to_json
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -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,28 @@
|
|
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 = Hammock::RouteNode.new
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|