utopia 1.4.0 → 1.5.0

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 (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