utopia 0.12.5 → 0.12.6
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.
- checksums.yaml +4 -4
- data/lib/utopia/extensions/array.rb +4 -16
- data/lib/utopia/extensions/date.rb +6 -41
- data/lib/utopia/extensions/hash.rb +1 -2
- data/lib/utopia/extensions/rack.rb +4 -8
- data/lib/utopia/extensions/string.rb +6 -7
- data/lib/utopia/middleware/all.rb +2 -0
- data/lib/utopia/middleware/controller/action.rb +121 -0
- data/lib/utopia/middleware/controller/base.rb +184 -0
- data/lib/utopia/middleware/controller/variables.rb +72 -0
- data/lib/utopia/middleware/controller.rb +53 -246
- data/lib/utopia/middleware/directory_index.rb +0 -2
- data/lib/utopia/middleware/exception_handler.rb +80 -0
- data/lib/utopia/middleware/redirector.rb +0 -57
- data/lib/utopia/path.rb +27 -10
- data/lib/utopia/setup/config.ru +1 -2
- data/lib/utopia/tags/all.rb +2 -0
- data/lib/utopia/version.rb +1 -1
- data/lib/utopia.rb +9 -2
- data/spec/utopia/extensions_spec.rb +84 -0
- data/spec/utopia/middleware/controller_spec.rb +108 -14
- data/spec/utopia/path_spec.rb +14 -0
- data/spec/utopia/rack_spec.rb +71 -0
- metadata +10 -6
- data/lib/utopia/extensions/maybe.rb +0 -31
- data/lib/utopia/middleware/logger.rb +0 -78
- data/lib/utopia/setup/access_log/readme.txt +0 -1
- data/lib/utopia/time_store.rb +0 -107
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 296a41faf645d12aa55672af9445a4f94f939da1
|
4
|
+
data.tar.gz: 53019aaae569b4191d3361d3d2a1094a9e52bcfe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34aaf114f9917265dbe27b65379bb77cb66fda5310b27384d2edcae8dc21fc01d7d814d6ea79f98fca46be81cbf2c1b9dc0c1da9f40b6af20dffa7cfb5dd2557
|
7
|
+
data.tar.gz: 09e47aa4fd61d9d015c7aa836c734dda5f20570efd02bacdd745756b2e02c6fc4e32e413bd47dfd1d140d740adf340dd4f0682b08ad63b579352af2798b0fae7
|
@@ -19,23 +19,11 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
class Array
|
22
|
-
def find_index(&block)
|
23
|
-
each_with_index do |item, index|
|
24
|
-
if yield(item)
|
25
|
-
return index
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
return nil
|
30
|
-
end
|
31
|
-
|
32
22
|
def split_at(&block)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
23
|
+
if middle = index(&block)
|
24
|
+
[self[0...middle], self[middle], self[middle+1..-1]]
|
25
|
+
else
|
26
|
+
[[], nil, []]
|
37
27
|
end
|
38
|
-
|
39
|
-
return [[], nil, []]
|
40
28
|
end
|
41
29
|
end
|
@@ -23,61 +23,26 @@
|
|
23
23
|
|
24
24
|
require 'date'
|
25
25
|
|
26
|
-
class Date
|
27
|
-
alias_method :old_compare, :<=>
|
28
|
-
|
29
|
-
def <=>(other)
|
30
|
-
if other.class == Date
|
31
|
-
old_compare(other)
|
32
|
-
else
|
33
|
-
if Time === other
|
34
|
-
other = other.to_datetime
|
35
|
-
end
|
36
|
-
|
37
|
-
if DateTime === other
|
38
|
-
result = old_compare(other.to_date)
|
39
|
-
if result == 0 && other.day_fraction > 0
|
40
|
-
-1
|
41
|
-
else
|
42
|
-
result
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
26
|
class Time
|
50
27
|
alias_method :old_compare, :<=>
|
51
28
|
|
52
29
|
def <=>(other)
|
53
|
-
if other
|
54
|
-
|
55
|
-
elsif Time === other
|
56
|
-
old_compare(other)
|
30
|
+
if Date === other or DateTime === other
|
31
|
+
self.to_datetime <=> other
|
57
32
|
else
|
58
|
-
if DateTime === other
|
59
|
-
other = other.to_time
|
60
|
-
end
|
61
|
-
|
62
33
|
old_compare(other)
|
63
34
|
end
|
64
35
|
end
|
65
36
|
end
|
66
37
|
|
67
|
-
class
|
38
|
+
class Date
|
68
39
|
alias_method :old_compare, :<=>
|
69
40
|
|
70
41
|
def <=>(other)
|
71
|
-
if
|
72
|
-
|
73
|
-
elsif DateTime === other
|
74
|
-
old_compare(other)
|
42
|
+
if Time === other
|
43
|
+
self.to_datetime <=> other.to_datetime
|
75
44
|
else
|
76
|
-
if Time === other
|
77
|
-
other = other.to_datetime
|
78
|
-
end
|
79
|
-
|
80
45
|
old_compare(other)
|
81
46
|
end
|
82
47
|
end
|
83
|
-
end
|
48
|
+
end
|
@@ -22,23 +22,18 @@ require 'rack'
|
|
22
22
|
|
23
23
|
class Rack::Request
|
24
24
|
def url_with_path(path = "")
|
25
|
-
|
26
|
-
url << host
|
27
|
-
|
28
|
-
if scheme == "https" && port != 443 || scheme == "http" && port != 80
|
29
|
-
url << ":#{port}"
|
30
|
-
end
|
31
|
-
|
32
|
-
url << path
|
25
|
+
base_url << path
|
33
26
|
end
|
34
27
|
end
|
35
28
|
|
36
29
|
class Rack::Response
|
30
|
+
# Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
|
37
31
|
def do_not_cache!
|
38
32
|
self["Cache-Control"] = "no-cache, must-revalidate"
|
39
33
|
self["Expires"] = Time.now.httpdate
|
40
34
|
end
|
41
35
|
|
36
|
+
# Specify that the content should be cached.
|
42
37
|
def cache!(duration = 3600)
|
43
38
|
unless (self["Cache-Control"] || "").match(/no-cache/)
|
44
39
|
self["Cache-Control"] = "public, max-age=#{duration}"
|
@@ -46,6 +41,7 @@ class Rack::Response
|
|
46
41
|
end
|
47
42
|
end
|
48
43
|
|
44
|
+
# Specify the content type of the response data.
|
49
45
|
def content_type!(value)
|
50
46
|
self["Content-Type"] = value.to_s
|
51
47
|
end
|
@@ -18,23 +18,22 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
|
22
|
-
HTML_ESCAPE = {"&" => "&", "<" => "<", ">" => ">", "\"" => """}
|
23
|
-
HTML_ESCAPE_PATTERN = Regexp.new("[" + Regexp.quote(HTML_ESCAPE.keys.join) + "]")
|
21
|
+
require 'trenni/strings'
|
24
22
|
|
23
|
+
class String
|
25
24
|
def to_html
|
26
|
-
|
25
|
+
Trenni::Strings::to_html(self)
|
27
26
|
end
|
28
27
|
|
29
28
|
def to_quoted_string
|
30
|
-
|
29
|
+
Trenni::Strings::to_quoted_string(self)
|
31
30
|
end
|
32
31
|
|
33
32
|
def to_title
|
34
|
-
self
|
33
|
+
Trenni::Strings::to_title(self)
|
35
34
|
end
|
36
35
|
|
37
36
|
def to_snake
|
38
|
-
|
37
|
+
Trenni::Strings::to_snake(self)
|
39
38
|
end
|
40
39
|
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Utopia
|
22
|
+
module Middleware
|
23
|
+
class Controller
|
24
|
+
class Action < Hash
|
25
|
+
attr_accessor :callback
|
26
|
+
attr_accessor :options
|
27
|
+
|
28
|
+
def callback?
|
29
|
+
@callback != nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def indirect?
|
33
|
+
@options[:indirect]
|
34
|
+
end
|
35
|
+
|
36
|
+
def eql? other
|
37
|
+
super and self.callback.eql? other.callback and self.options.eql? other.options
|
38
|
+
end
|
39
|
+
|
40
|
+
def hash
|
41
|
+
[super, callback, options].hash
|
42
|
+
end
|
43
|
+
|
44
|
+
def == other
|
45
|
+
super and (self.callback == other.callback) and (self.options == other.options)
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def append(path, index, actions = [])
|
51
|
+
# ** is greedy, it always matches if possible:
|
52
|
+
if match_all = self[:**]
|
53
|
+
# Match all remaining input:
|
54
|
+
actions << match_all if match_all.callback?
|
55
|
+
end
|
56
|
+
|
57
|
+
if index < path.size
|
58
|
+
name = path[index].to_sym
|
59
|
+
|
60
|
+
if match_name = self[name]
|
61
|
+
# Match the exact name:
|
62
|
+
match_name.append(path, index+1, actions)
|
63
|
+
end
|
64
|
+
|
65
|
+
if match_one = self[:*]
|
66
|
+
# Match one input:
|
67
|
+
match_one.append(path, index+1, actions)
|
68
|
+
end
|
69
|
+
else
|
70
|
+
# Got to end, matched completely:
|
71
|
+
actions << self if self.callback?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
public
|
76
|
+
|
77
|
+
# relative_path = 2014/mr-potato
|
78
|
+
# actions => {:** => A}
|
79
|
+
def select(relative_path)
|
80
|
+
actions = []
|
81
|
+
|
82
|
+
append(relative_path.reverse, 0, actions)
|
83
|
+
|
84
|
+
# puts "select(#{relative_path}, #{self.inspect}) => #{actions.inspect}"
|
85
|
+
|
86
|
+
return actions
|
87
|
+
end
|
88
|
+
|
89
|
+
def define(path, options = {}, &callback)
|
90
|
+
current = self
|
91
|
+
|
92
|
+
path.reverse.each do |name|
|
93
|
+
current = (current[name.to_sym] ||= Action.new)
|
94
|
+
end
|
95
|
+
|
96
|
+
current.options = options
|
97
|
+
current.callback = callback
|
98
|
+
|
99
|
+
return current
|
100
|
+
end
|
101
|
+
|
102
|
+
def invoke!(controller, *arguments)
|
103
|
+
if @options[:unbound]
|
104
|
+
# This is the old code path for explicit methods.
|
105
|
+
controller.instance_exec(controller, *arguments, &@callback)
|
106
|
+
else
|
107
|
+
controller.instance_exec(*arguments, &@callback)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def inspect
|
112
|
+
if callback?
|
113
|
+
"<action " + super + ":#{callback.source_location}(#{options})>"
|
114
|
+
else
|
115
|
+
"<action " + super + ">"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'utopia/path'
|
22
|
+
require 'utopia/http'
|
23
|
+
|
24
|
+
module Utopia
|
25
|
+
module Middleware
|
26
|
+
class Controller
|
27
|
+
class Base
|
28
|
+
def self.base_path
|
29
|
+
self.const_get(:BASE_PATH)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.uri_path
|
33
|
+
self.const_get(:URI_PATH)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.controller
|
37
|
+
self.const_get(:CONTROLLER)
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
def require_local(path)
|
42
|
+
require File.join(base_path, path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def direct?(path)
|
46
|
+
path.dirname == uri_path
|
47
|
+
end
|
48
|
+
|
49
|
+
def actions
|
50
|
+
@actions ||= Action.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def on(path, options = {}, &block)
|
54
|
+
if Symbol === path
|
55
|
+
path = ['**', path]
|
56
|
+
end
|
57
|
+
|
58
|
+
actions.define(Path[path].components, options, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def lookup(path)
|
62
|
+
possible_actions = actions.select((path - uri_path).components)
|
63
|
+
end
|
64
|
+
|
65
|
+
def method_added(name)
|
66
|
+
if name.match(/^on_(.*)$/)
|
67
|
+
warn "Method #{name} using legacy definition mechanism."
|
68
|
+
on($1.split("_").join('/'), unbound: true, &name)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Given a path, look up all matched actions.
|
74
|
+
def lookup(path)
|
75
|
+
self.class.lookup(path)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Given a request, call associated actions if at least one exists.
|
79
|
+
def passthrough(request, path)
|
80
|
+
actions = lookup(path)
|
81
|
+
|
82
|
+
if actions.size > 0
|
83
|
+
variables = request.controller
|
84
|
+
controller_clone = self.clone
|
85
|
+
|
86
|
+
variables << controller_clone
|
87
|
+
|
88
|
+
response = catch(:response) do
|
89
|
+
# By default give nothing - i.e. keep on processing:
|
90
|
+
actions.each do |action|
|
91
|
+
action.invoke!(controller_clone, request, path)
|
92
|
+
end and nil
|
93
|
+
end
|
94
|
+
|
95
|
+
if response
|
96
|
+
return controller_clone.respond_with(*response)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
return nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# Copy the instance variables from the previous controller to the next controller (usually only a few). This allows controllers to share effectively the same instance variables while still being separate classes/instances.
|
104
|
+
def copy_instance_variables(from)
|
105
|
+
from.instance_variables.each do |name|
|
106
|
+
instance_variable_set(name, from.instance_variable_get(name))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def call(env)
|
111
|
+
self.class.controller.app.call(env)
|
112
|
+
end
|
113
|
+
|
114
|
+
def respond!(*args)
|
115
|
+
throw :response, args
|
116
|
+
end
|
117
|
+
|
118
|
+
def ignore!
|
119
|
+
throw :response, nil
|
120
|
+
end
|
121
|
+
|
122
|
+
def redirect! (target, status = 302)
|
123
|
+
respond! :redirect => target.to_str, :status => status
|
124
|
+
end
|
125
|
+
|
126
|
+
def rewrite! location
|
127
|
+
throw :rewrite, location.to_str
|
128
|
+
end
|
129
|
+
|
130
|
+
def fail!(error = :bad_request)
|
131
|
+
respond! error
|
132
|
+
end
|
133
|
+
|
134
|
+
def success!(*args)
|
135
|
+
respond! :success, *args
|
136
|
+
end
|
137
|
+
|
138
|
+
def respond_with(*args)
|
139
|
+
return args[0] if args[0] == nil || Array === args[0]
|
140
|
+
|
141
|
+
status = 200
|
142
|
+
options = nil
|
143
|
+
|
144
|
+
if Numeric === args[0] || Symbol === args[0]
|
145
|
+
status = args[0]
|
146
|
+
options = args[1] || {}
|
147
|
+
else
|
148
|
+
options = args[0]
|
149
|
+
status = options[:status] || status
|
150
|
+
end
|
151
|
+
|
152
|
+
status = Utopia::HTTP::STATUS_CODES[status] || status
|
153
|
+
headers = options[:headers] || {}
|
154
|
+
|
155
|
+
if options[:type]
|
156
|
+
headers['Content-Type'] ||= options[:type]
|
157
|
+
end
|
158
|
+
|
159
|
+
if options[:redirect]
|
160
|
+
headers["Location"] = options[:redirect]
|
161
|
+
status = 302 if status < 300 || status >= 400
|
162
|
+
end
|
163
|
+
|
164
|
+
body = []
|
165
|
+
if options[:body]
|
166
|
+
body = options[:body]
|
167
|
+
elsif options[:content]
|
168
|
+
body = [options[:content]]
|
169
|
+
elsif status >= 300
|
170
|
+
body = [Utopia::HTTP::STATUS_DESCRIPTIONS[status] || "Status #{status}"]
|
171
|
+
end
|
172
|
+
|
173
|
+
return [status, headers, body]
|
174
|
+
end
|
175
|
+
|
176
|
+
# Return nil if this controller didn't do anything. Request will keep on processing. Return a valid rack response if the controller can do so.
|
177
|
+
def process!(request, path)
|
178
|
+
# puts "process! #{request} #{path}"
|
179
|
+
passthrough(request, path)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Utopia
|
22
|
+
module Middleware
|
23
|
+
class Controller
|
24
|
+
class Variables
|
25
|
+
def initialize
|
26
|
+
@controllers = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def << controller
|
30
|
+
top = @controllers.last
|
31
|
+
|
32
|
+
@controllers << controller
|
33
|
+
|
34
|
+
# This ensures that most variables will be at the top and controllers can naturally interactive with instance variables.
|
35
|
+
controller.copy_instance_variables(top) if top
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch(key)
|
39
|
+
@controllers.reverse_each do |controller|
|
40
|
+
if controller.instance_variables.include?(key)
|
41
|
+
return controller.instance_variable_get(key)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
if block_given?
|
46
|
+
yield key
|
47
|
+
else
|
48
|
+
raise KeyError.new(key)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_hash
|
53
|
+
attributes = {}
|
54
|
+
|
55
|
+
@controllers.each do |controller|
|
56
|
+
controller.instance_variables.each do |name|
|
57
|
+
key = name[1..-1]
|
58
|
+
|
59
|
+
attributes[key] = controller.instance_variable_get(name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
return attributes
|
64
|
+
end
|
65
|
+
|
66
|
+
def [] key
|
67
|
+
fetch("@#{key}".to_sym) { nil }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|