utopia 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -1
  4. data/Gemfile +3 -0
  5. data/README.md +11 -0
  6. data/benchmarks/hash_vs_openstruct.rb +52 -0
  7. data/benchmarks/struct_vs_class.rb +89 -0
  8. data/bin/utopia +4 -5
  9. data/lib/utopia.rb +1 -0
  10. data/lib/utopia/content.rb +24 -15
  11. data/lib/utopia/content/node.rb +69 -3
  12. data/lib/utopia/content/processor.rb +32 -5
  13. data/lib/utopia/content/transaction.rb +138 -147
  14. data/lib/utopia/content_length.rb +50 -0
  15. data/lib/utopia/controller.rb +4 -0
  16. data/lib/utopia/controller/variables.rb +1 -1
  17. data/lib/utopia/http.rb +2 -0
  18. data/lib/utopia/localization.rb +4 -8
  19. data/lib/utopia/path.rb +13 -13
  20. data/lib/utopia/static.rb +25 -14
  21. data/lib/utopia/tags/environment.rb +1 -1
  22. data/lib/utopia/tags/override.rb +1 -1
  23. data/lib/utopia/version.rb +1 -1
  24. data/setup/server/git/hooks/post-receive +32 -24
  25. data/setup/site/Gemfile +8 -0
  26. data/setup/site/Rakefile +29 -6
  27. data/setup/site/config.ru +8 -8
  28. data/setup/site/pages/_heading.xnode +1 -1
  29. data/setup/site/pages/_page.xnode +2 -2
  30. data/spec/utopia/content_spec.rb +3 -3
  31. data/spec/utopia/controller/sequence_spec.rb +1 -1
  32. data/spec/utopia/controller/variables_spec.rb +1 -1
  33. data/spec/utopia/pages/node/index.xnode +1 -1
  34. data/spec/utopia/performance_spec.rb +90 -0
  35. data/spec/utopia/performance_spec/cache/head/readme.txt +1 -0
  36. data/spec/utopia/performance_spec/cache/meta/readme.txt +1 -0
  37. data/spec/utopia/performance_spec/config.ru +39 -0
  38. data/spec/utopia/performance_spec/lib/readme.txt +1 -0
  39. data/spec/utopia/performance_spec/pages/_heading.xnode +2 -0
  40. data/spec/utopia/performance_spec/pages/_page.xnode +26 -0
  41. data/spec/utopia/performance_spec/pages/api/controller.rb +7 -0
  42. data/spec/utopia/performance_spec/pages/errors/exception.xnode +5 -0
  43. data/spec/utopia/performance_spec/pages/errors/file-not-found.xnode +5 -0
  44. data/spec/utopia/performance_spec/pages/links.yaml +2 -0
  45. data/spec/utopia/performance_spec/pages/welcome/index.xnode +17 -0
  46. data/spec/utopia/rack_helper.rb +5 -2
  47. data/spec/utopia/setup_spec.rb +93 -0
  48. data/utopia.gemspec +1 -1
  49. metadata +34 -5
  50. data/lib/utopia/mail_exceptions.rb +0 -136
@@ -22,6 +22,7 @@ require_relative 'links'
22
22
 
23
23
  module Utopia
24
24
  class Content
25
+ # This error is thrown if a tag doesn't match up when parsing the
25
26
  class UnbalancedTagError < StandardError
26
27
  def initialize(tag)
27
28
  @tag = tag
@@ -32,114 +33,47 @@ module Utopia
32
33
  attr :tag
33
34
  end
34
35
 
35
- # A single request through content middleware.
36
+ DEFERRED_TAG_NAME = "deferred".freeze
37
+ CONTENT_TAG_NAME = "content".freeze
38
+
39
+ # A single request through content middleware. We use a struct to hide instance varibles since we instance_exec within this context.
36
40
  class Transaction
37
- # The state of a single tag being rendered.
38
- class State
39
- def initialize(tag, node)
40
- @node = node
41
-
42
- @buffer = StringIO.new
43
- @overrides = {}
44
-
45
- @tags = []
46
- @attributes = tag.to_hash
47
-
48
- @content = nil
49
- @deferred = []
50
- end
51
-
52
- attr :attributes
53
- attr :overrides
54
- attr :content
55
- attr :node
56
- attr :tags
57
-
58
- attr :deferred
59
-
60
- def defer(value = nil, &block)
61
- @deferred << block
41
+ # extend Gem::Deprecate
42
+
43
+ def initialize(request, response, attributes = {})
44
+ @request = request
45
+ @response = response
46
+
47
+ @attributes = attributes
62
48
 
63
- Tag.closed("deferred", :id => @deferred.size - 1).to_html
64
- end
65
-
66
- def [](key)
67
- @attributes[key.to_s]
68
- end
69
-
70
- def call(transaction)
71
- @content = @buffer.string
72
- @buffer = StringIO.new
73
-
74
- if node.respond_to? :call
75
- node.call(transaction, self)
76
- else
77
- transaction.parse_xml(@content)
78
- end
79
-
80
- return @buffer.string
81
- end
82
-
83
- def lookup(tag)
84
- if override = @overrides[tag.name]
85
- if override.respond_to? :call
86
- return override.call(tag)
87
- elsif String === override
88
- return Tag.new(override, tag.attributes)
89
- else
90
- return override
91
- end
92
- else
93
- return tag
94
- end
95
- end
96
-
97
- def cdata(text)
98
- @buffer.write(text)
99
- end
100
-
101
- def markup(text)
102
- cdata(text)
103
- end
104
-
105
- def tag_complete(tag)
106
- tag.write_full_html(@buffer)
107
- end
108
-
109
- def tag_begin(tag)
110
- @tags << tag
111
- tag.write_open_html(@buffer)
112
- end
113
-
114
- def tag_end(tag)
115
- raise UnbalancedTagError(tag) unless @tags.pop.name == tag.name
116
-
117
- tag.write_close_html(@buffer)
118
- end
119
- end
120
-
121
- def initialize(request, response)
122
49
  @begin_tags = []
123
50
  @end_tags = []
124
-
125
- @request = request
126
- @response = response
127
51
  end
128
-
129
- attr :request
130
- attr :response
131
52
 
132
53
  # A helper method for accessing controller variables from view:
133
54
  def controller
134
- @request.env[VARIABLES_KEY]
55
+ @controller ||= Utopia::Controller[request]
56
+ end
57
+
58
+ def localization
59
+ @localization ||= Utopia::Localization[request]
135
60
  end
136
61
 
137
- def parse_xml(xml_data)
138
- Processor.parse_xml(xml_data, self)
62
+ def parse_markup(markup)
63
+ Processor.parse_markup(markup, self)
139
64
  end
140
65
 
66
+ # The Rack::Request for this transaction.
67
+ attr :request
68
+
69
+ # The mutable Rack::Response for this transaction.
70
+ attr :response
71
+
72
+ # Per-transaction global attributes.
73
+ attr :attributes
74
+
141
75
  # Begin tags represents a list from outer to inner most tag.
142
- # At any point in parsing xml, begin_tags is a list of the inner most tag,
76
+ # At any point in parsing markup, begin_tags is a list of the inner most tag,
143
77
  # then the next outer tag, etc. This list is used for doing dependent lookups.
144
78
  attr :begin_tags
145
79
 
@@ -147,30 +81,6 @@ module Utopia
147
81
  # have appeared when evaluating nodes.
148
82
  attr :end_tags
149
83
 
150
- def attributes
151
- return current.attributes
152
- end
153
-
154
- def localization
155
- @localization ||= Utopia::Localization[request]
156
- end
157
-
158
- def current
159
- @begin_tags[-1]
160
- end
161
-
162
- def content
163
- @end_tags[-1].content
164
- end
165
-
166
- def parent
167
- end_tags[-2]
168
- end
169
-
170
- def first
171
- @begin_tags[0]
172
- end
173
-
174
84
  def tag(name, attributes = {}, &block)
175
85
  tag = Tag.new(name, attributes)
176
86
 
@@ -182,7 +92,7 @@ module Utopia
182
92
  end
183
93
 
184
94
  def tag_complete(tag, node = nil)
185
- if tag.name == "content"
95
+ if tag.name == CONTENT_TAG_NAME
186
96
  current.markup(content)
187
97
  else
188
98
  node ||= lookup(tag)
@@ -201,7 +111,7 @@ module Utopia
201
111
 
202
112
  if node
203
113
  state = State.new(tag, node)
204
- @begin_tags << state
114
+ self.begin_tags << state
205
115
 
206
116
  if node.respond_to? :tag_begin
207
117
  node.tag_begin(self, state)
@@ -219,31 +129,21 @@ module Utopia
219
129
  current.cdata(text)
220
130
  end
221
131
 
222
- def partial(*args, &block)
223
- if block_given?
224
- current.defer(&block)
225
- else
226
- current.defer do
227
- tag(*args)
228
- end
229
- end
230
- end
231
-
232
- alias deferred_tag partial
233
-
234
132
  def tag_end(tag = nil)
133
+ # Get the current tag which we are completing/ending:
235
134
  top = current
236
-
135
+
136
+
237
137
  if top.tags.empty?
238
138
  if top.node.respond_to? :tag_end
239
139
  top.node.tag_end(self, top)
240
140
  end
241
141
 
242
- @end_tags << top
142
+ self.end_tags << top
243
143
  buffer = top.call(self)
244
144
 
245
- @begin_tags.pop
246
- @end_tags.pop
145
+ self.begin_tags.pop
146
+ self.end_tags.pop
247
147
 
248
148
  if current
249
149
  current.markup(buffer)
@@ -258,8 +158,7 @@ module Utopia
258
158
  end
259
159
 
260
160
  def render_node(node, attributes = {})
261
- state = State.new(attributes, node)
262
- @begin_tags << state
161
+ self.begin_tags << State.new(attributes, node)
263
162
 
264
163
  return tag_end
265
164
  end
@@ -269,29 +168,121 @@ module Utopia
269
168
  result = tag
270
169
  node = nil
271
170
 
272
- @begin_tags.reverse_each do |state|
171
+ self.begin_tags.reverse_each do |state|
273
172
  result = state.lookup(result)
274
173
 
275
174
  node ||= state.node if state.node.respond_to? :lookup
276
175
 
277
- return result if Node === result
176
+ return result if result.is_a?(Node)
278
177
  end
279
178
 
280
- @end_tags.reverse_each do |state|
179
+ self.end_tags.reverse_each do |state|
281
180
  return state.node.lookup(result) if state.node.respond_to? :lookup
282
181
  end
283
182
 
284
183
  return nil
285
184
  end
185
+
186
+ # The current tag being processed/rendered. Prefer to access state directly.
187
+ def current
188
+ @begin_tags.last
189
+ end
190
+
191
+ # The content of the node
192
+ def content
193
+ @end_tags.last.content
194
+ end
195
+
196
+ def parent
197
+ @end_tags[-2]
198
+ end
199
+
200
+ def first
201
+ @begin_tags.first
202
+ end
203
+ end
204
+
205
+ # The state of a single tag being rendered within a Transaction instance.
206
+ class Transaction::State
207
+ def initialize(tag, node, attributes = tag.to_hash)
208
+ @node = node
209
+
210
+ @buffer = StringIO.new
211
+ @overrides = {}
212
+
213
+ @tags = []
214
+ @attributes = attributes
215
+
216
+ @content = nil
217
+ @deferred = []
218
+ end
219
+
220
+ attr :attributes
221
+ attr :overrides
222
+ attr :content
223
+ attr :node
224
+ attr :tags
225
+
226
+ attr :deferred
227
+
228
+ def defer(value = nil, &block)
229
+ @deferred << block
230
+
231
+ Tag.closed(DEFERRED_TAG_NAME, :id => @deferred.size - 1).to_html
232
+ end
233
+
234
+ def [](key)
235
+ @attributes[key]
236
+ end
286
237
 
287
- def method_missing(name, *args)
288
- @begin_tags.reverse_each do |state|
289
- if state.node.respond_to? name
290
- return state.node.send(name, *args)
238
+ def lookup(tag)
239
+ if override = @overrides[tag.name]
240
+ if override.respond_to? :call
241
+ return override.call(tag)
242
+ elsif String === override
243
+ return Tag.new(override, tag.attributes)
244
+ else
245
+ return override
291
246
  end
247
+ else
248
+ return tag
292
249
  end
250
+ end
251
+
252
+ def call(transaction)
253
+ @content = @buffer.string
254
+ @buffer = StringIO.new
255
+
256
+ if node.respond_to? :call
257
+ node.call(transaction, self)
258
+ else
259
+ transaction.parse_markup(@content)
260
+ end
261
+
262
+ return @buffer.string
263
+ end
264
+
265
+ def cdata(text)
266
+ @buffer.write(text)
267
+ end
268
+
269
+ def markup(text)
270
+ cdata(text)
271
+ end
272
+
273
+ def tag_complete(tag)
274
+ tag.write_full_html(@buffer)
275
+ end
276
+
277
+ def tag_begin(tag)
278
+ @tags << tag
279
+ tag.write_open_html(@buffer)
280
+ end
281
+
282
+ def tag_end(tag)
283
+ raise UnbalancedTagError(tag) unless @tags.pop.name == tag.name
293
284
 
294
- super
285
+ tag.write_close_html(@buffer)
295
286
  end
296
287
  end
297
288
  end
@@ -0,0 +1,50 @@
1
+ # Copyright, 2016, 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_relative 'middleware'
22
+
23
+ module Utopia
24
+ # A faster implementation of Rack::ContentLength which doesn't rewrite body, but does expect it to either be an Array or an object that responds to #bytesize.
25
+ class ContentLength
26
+ def initialize(app)
27
+ @app = app
28
+ end
29
+
30
+ def content_length_of(body)
31
+ if body.is_a? Array
32
+ return body.map(&:bytesize).reduce(0, :+)
33
+ else
34
+ return body.bytesize
35
+ end
36
+ end
37
+
38
+ def call(env)
39
+ response = @app.call(env)
40
+
41
+ unless response[2].empty? or response[1].include?(Rack::CONTENT_LENGTH)
42
+ if content_length = self.content_length_of(response[2])
43
+ response[1][Rack::CONTENT_LENGTH] = content_length
44
+ end
45
+ end
46
+
47
+ return response
48
+ end
49
+ end
50
+ end
@@ -47,6 +47,10 @@ module Utopia
47
47
  class Controller
48
48
  CONTROLLER_RB = 'controller.rb'.freeze
49
49
 
50
+ def self.[] request
51
+ request.env[VARIABLES_KEY]
52
+ end
53
+
50
54
  def initialize(app, root: nil, cache_controllers: false)
51
55
  @app = app
52
56
  @root = root || Utopia::default_root
@@ -62,7 +62,7 @@ module Utopia
62
62
 
63
63
  if controller = self.top
64
64
  controller.instance_variables.each do |name|
65
- key = name[1..-1]
65
+ key = name[1..-1].to_sym
66
66
 
67
67
  attributes[key] = controller.instance_variable_get(name)
68
68
  end
@@ -45,6 +45,7 @@ module Utopia
45
45
  :unsupported_method => 405,
46
46
  :gone => 410,
47
47
  :teapot => 418,
48
+ :unprocessible => 422, # The best status code for a client-side ArgumentError.
48
49
  :error => 500,
49
50
  :unimplemented => 501,
50
51
  :unavailable => 503
@@ -79,6 +80,7 @@ module Utopia
79
80
  409 => 'Request Conflict'.freeze,
80
81
  410 => 'Resource Removed'.freeze,
81
82
  416 => 'Byte range unsatisfiable'.freeze,
83
+ 422 => 'Unprocessible Entity'.freeze,
82
84
  500 => 'Internal Server Error'.freeze,
83
85
  501 => 'Not Implemented'.freeze,
84
86
  503 => 'Service Unavailable'.freeze