utopia 1.7.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -3
  3. data/README.md +142 -11
  4. data/benchmarks/string_vs_symbol.rb +12 -0
  5. data/lib/utopia/command.rb +16 -13
  6. data/lib/utopia/content.rb +1 -5
  7. data/lib/utopia/content/node.rb +9 -4
  8. data/lib/utopia/{extensions/rack.rb → content/response.rb} +33 -30
  9. data/lib/utopia/content/tag.rb +14 -17
  10. data/lib/utopia/content/transaction.rb +19 -17
  11. data/lib/utopia/controller.rb +29 -8
  12. data/lib/utopia/controller/actions.rb +148 -0
  13. data/lib/utopia/controller/base.rb +9 -49
  14. data/lib/utopia/controller/respond.rb +1 -1
  15. data/lib/utopia/controller/rewrite.rb +9 -1
  16. data/lib/utopia/controller/variables.rb +1 -0
  17. data/lib/utopia/localization.rb +4 -1
  18. data/lib/utopia/middleware.rb +0 -2
  19. data/lib/utopia/path.rb +9 -0
  20. data/lib/utopia/path/matcher.rb +0 -1
  21. data/lib/utopia/redirection.rb +3 -2
  22. data/lib/utopia/session.rb +119 -2
  23. data/lib/utopia/session/lazy_hash.rb +1 -3
  24. data/lib/utopia/setup.rb +73 -0
  25. data/lib/utopia/static.rb +9 -2
  26. data/lib/utopia/version.rb +1 -1
  27. data/setup/examples/wiki/controller.rb +41 -0
  28. data/setup/examples/wiki/edit.xnode +15 -0
  29. data/setup/examples/wiki/index.xnode +10 -0
  30. data/setup/examples/wiki/welcome/content.md +3 -0
  31. data/setup/server/config/environment.yaml +1 -0
  32. data/setup/server/git/hooks/post-receive +4 -5
  33. data/setup/site/Gemfile +5 -0
  34. data/setup/site/config.ru +2 -1
  35. data/setup/site/config/environment.rb +5 -17
  36. data/setup/site/pages/_page.xnode +4 -2
  37. data/setup/site/pages/links.yaml +1 -1
  38. data/setup/site/pages/welcome/index.xnode +33 -15
  39. data/setup/site/public/_static/site.css +72 -4
  40. data/setup/site/tasks/utopia.rake +8 -0
  41. data/spec/utopia/{rack_spec.rb → content/response_spec.rb} +12 -19
  42. data/spec/utopia/content_spec.rb +2 -3
  43. data/spec/utopia/controller/{action_spec.rb → actions_spec.rb} +18 -32
  44. data/spec/utopia/controller/middleware_spec.rb +10 -10
  45. data/spec/utopia/controller/middleware_spec/controller/controller.rb +3 -3
  46. data/spec/utopia/controller/middleware_spec/controller/nested/controller.rb +1 -1
  47. data/spec/utopia/controller/middleware_spec/redirect/controller.rb +1 -1
  48. data/spec/utopia/controller/respond_spec.rb +3 -2
  49. data/spec/utopia/controller/respond_spec/api/controller.rb +2 -2
  50. data/spec/utopia/controller/respond_spec/errors/controller.rb +1 -1
  51. data/spec/utopia/controller/rewrite_spec.rb +1 -1
  52. data/spec/utopia/controller/sequence_spec.rb +12 -16
  53. data/spec/utopia/exceptions/handler_spec/controller.rb +2 -2
  54. data/spec/utopia/performance_spec/config.ru +1 -0
  55. data/spec/utopia/session_spec.rb +34 -1
  56. data/spec/utopia/session_spec.ru +3 -3
  57. data/spec/utopia/setup_spec.rb +2 -2
  58. data/utopia.gemspec +2 -2
  59. metadata +18 -12
  60. data/lib/utopia/controller/action.rb +0 -116
  61. data/lib/utopia/session/encrypted_cookie.rb +0 -118
@@ -56,52 +56,49 @@ module Utopia
56
56
  def [](key)
57
57
  @attributes[key]
58
58
  end
59
-
60
- def to_html(content = nil, buffer = StringIO.new)
61
- write_full_html(buffer, content)
62
-
63
- return buffer.string
64
- end
65
59
 
66
60
  def to_hash
67
61
  @attributes
68
62
  end
69
63
 
70
64
  def to_s(content = nil)
71
- buffer = StringIO.new
65
+ buffer = String.new
66
+
72
67
  write_full_html(buffer, content)
73
- return buffer.string
68
+
69
+ return buffer
74
70
  end
75
71
 
72
+ alias to_html to_s
73
+
76
74
  def write_open_html(buffer, terminate = false)
77
- buffer ||= StringIO.new
78
- buffer.write "<#{name}"
75
+ buffer << "<#{name}"
79
76
 
80
77
  @attributes.each do |key, value|
81
78
  if value
82
- buffer.write " #{key}=\"#{value}\""
79
+ buffer << " #{key}=\"#{value}\""
83
80
  else
84
- buffer.write " #{key}"
81
+ buffer << " #{key}"
85
82
  end
86
83
  end
87
84
 
88
85
  if terminate
89
- buffer.write "/>"
86
+ buffer << "/>"
90
87
  else
91
- buffer.write ">"
88
+ buffer << ">"
92
89
  end
93
90
  end
94
91
 
95
92
  def write_close_html(buffer)
96
- buffer.write "</#{name}>"
93
+ buffer << "</#{name}>"
97
94
  end
98
95
 
99
96
  def write_full_html(buffer, content = nil)
100
- if @closed && content == nil
97
+ if @closed and content.nil?
101
98
  write_open_html(buffer, true)
102
99
  else
103
100
  write_open_html(buffer)
104
- buffer.write(content)
101
+ buffer << content if content
105
102
  write_close_html(buffer)
106
103
  end
107
104
  end
@@ -20,6 +20,8 @@
20
20
 
21
21
  require_relative 'links'
22
22
 
23
+ require_relative 'response'
24
+
23
25
  module Utopia
24
26
  class Content
25
27
  # This error is thrown if a tag doesn't match up when parsing the
@@ -37,19 +39,21 @@ module Utopia
37
39
  CONTENT_TAG_NAME = "content".freeze
38
40
 
39
41
  # A single request through content middleware. We use a struct to hide instance varibles since we instance_exec within this context.
40
- class Transaction
41
- # extend Gem::Deprecate
42
-
43
- def initialize(request, response, attributes = {})
42
+ class Transaction < Response
43
+ def initialize(request, attributes = {})
44
44
  @request = request
45
- @response = response
46
45
 
47
46
  @attributes = attributes
48
47
 
49
48
  @begin_tags = []
50
49
  @end_tags = []
50
+
51
+ super()
51
52
  end
52
53
 
54
+ attr :status
55
+ attr :headers
56
+
53
57
  # A helper method for accessing controller variables from view:
54
58
  def controller
55
59
  @controller ||= Utopia::Controller[request]
@@ -66,9 +70,6 @@ module Utopia
66
70
  # The Rack::Request for this transaction.
67
71
  attr :request
68
72
 
69
- # The mutable Rack::Response for this transaction.
70
- attr :response
71
-
72
73
  # Per-transaction global attributes.
73
74
  attr :attributes
74
75
 
@@ -133,7 +134,6 @@ module Utopia
133
134
  # Get the current tag which we are completing/ending:
134
135
  top = current
135
136
 
136
-
137
137
  if top.tags.empty?
138
138
  if top.node.respond_to? :tag_end
139
139
  top.node.tag_end(self, top)
@@ -156,7 +156,7 @@ module Utopia
156
156
 
157
157
  return nil
158
158
  end
159
-
159
+
160
160
  def render_node(node, attributes = {})
161
161
  self.begin_tags << State.new(attributes, node)
162
162
 
@@ -206,8 +206,10 @@ module Utopia
206
206
  class Transaction::State
207
207
  def initialize(tag, node, attributes = tag.to_hash)
208
208
  @node = node
209
-
210
- @buffer = StringIO.new
209
+
210
+ # The contents of the output
211
+ @buffer = String.new
212
+
211
213
  @overrides = {}
212
214
 
213
215
  @tags = []
@@ -228,7 +230,7 @@ module Utopia
228
230
  def defer(value = nil, &block)
229
231
  @deferred << block
230
232
 
231
- Tag.closed(DEFERRED_TAG_NAME, :id => @deferred.size - 1).to_html
233
+ Tag.closed(DEFERRED_TAG_NAME, :id => @deferred.size - 1).to_s
232
234
  end
233
235
 
234
236
  def [](key)
@@ -250,8 +252,8 @@ module Utopia
250
252
  end
251
253
 
252
254
  def call(transaction)
253
- @content = @buffer.string
254
- @buffer = StringIO.new
255
+ @content = @buffer
256
+ @buffer = String.new
255
257
 
256
258
  if node.respond_to? :call
257
259
  node.call(transaction, self)
@@ -259,11 +261,11 @@ module Utopia
259
261
  transaction.parse_markup(@content)
260
262
  end
261
263
 
262
- return @buffer.string
264
+ return @buffer
263
265
  end
264
266
 
265
267
  def cdata(text)
266
- @buffer.write(text)
268
+ @buffer << text
267
269
  end
268
270
 
269
271
  def markup(text)
@@ -22,15 +22,16 @@ require_relative 'path'
22
22
 
23
23
  require_relative 'middleware'
24
24
  require_relative 'controller/variables'
25
- require_relative 'controller/action'
26
25
  require_relative 'controller/base'
27
26
 
28
27
  require_relative 'controller/rewrite'
29
28
  require_relative 'controller/respond'
29
+ require_relative 'controller/actions'
30
30
 
31
31
  require 'concurrent/map'
32
32
 
33
33
  module Utopia
34
+ # A container for controller classes which are loaded from disk.
34
35
  module Controllers
35
36
  def self.class_name_for_controller(controller)
36
37
  controller.uri_path.to_a.collect{|_| _.capitalize}.join + "_#{controller.object_id}"
@@ -44,14 +45,16 @@ module Utopia
44
45
  end
45
46
  end
46
47
 
48
+ # A middleware which loads controller classes and invokes functionality based on the requested path.
47
49
  class Controller
50
+ # The controller filename.
48
51
  CONTROLLER_RB = 'controller.rb'.freeze
49
52
 
50
53
  def self.[] request
51
54
  request.env[VARIABLES_KEY]
52
55
  end
53
56
 
54
- def initialize(app, root: nil, cache_controllers: false)
57
+ def initialize(app, root: nil, cache_controllers: false, base: nil)
55
58
  @app = app
56
59
  @root = root || Utopia::default_root
57
60
 
@@ -60,6 +63,10 @@ module Utopia
60
63
  else
61
64
  @controller_cache = nil
62
65
  end
66
+
67
+ warn "Controller middleware is automatically prepending Actions! Will be deprecated in 2.x" if $VERBOSE and base.nil?
68
+
69
+ @base = base || Controller::Base.dup.prepend(Controller::Actions)
63
70
  end
64
71
 
65
72
  attr :app
@@ -67,9 +74,13 @@ module Utopia
67
74
  def freeze
68
75
  @root.freeze
69
76
 
77
+ # Should we freeze the base class?
78
+ # @base.freeze
79
+
70
80
  super
71
81
  end
72
82
 
83
+ # Fetch the controller for the given relative path. May be cached.
73
84
  def lookup_controller(path)
74
85
  if @controller_cache
75
86
  @controller_cache.fetch_or_store(path.to_s) do
@@ -80,6 +91,7 @@ module Utopia
80
91
  end
81
92
  end
82
93
 
94
+ # Loads the controller file for the given relative url_path.
83
95
  def load_controller_file(uri_path)
84
96
  base_path = File.join(@root, uri_path.components)
85
97
 
@@ -87,7 +99,7 @@ module Utopia
87
99
  # puts "load_controller_file(#{path.inspect}) => #{controller_path}"
88
100
 
89
101
  if File.exist?(controller_path)
90
- klass = Class.new(Base)
102
+ klass = Class.new(@base)
91
103
 
92
104
  # base_path is expected to be a string representing a filesystem path:
93
105
  klass.const_set(:BASE_PATH, base_path.freeze)
@@ -112,13 +124,22 @@ module Utopia
112
124
  end
113
125
  end
114
126
 
127
+ # Invoke the controller layer for a given request. The request path may be rewritten.
115
128
  def invoke_controllers(request)
116
- relative_path = Path[request.path_info]
129
+ request_path = Path.from_string(request.path_info)
130
+
131
+ # The request path must be absolute. We could handle this internally but it is probably better for this to be an error:
132
+ raise ArgumentError.new("Invalid request path #{request_path}") unless request_path.absolute?
133
+
134
+ # The controller path contains the current complete path being evaluated:
117
135
  controller_path = Path.new
136
+
137
+ # Controller instance variables which eventually get processed by the view:
118
138
  variables = request.env[VARIABLES_KEY]
119
139
 
120
- while relative_path.components.any?
121
- controller_path.components << relative_path.components.shift
140
+ while request_path.components.any?
141
+ # We copy one path component from the relative path to the controller path at a time. The controller, when invoked, can modify the relative path (by assigning to relative_path.components). This allows for controller-relative rewrites, but only the remaining path postfix can be modified.
142
+ controller_path.components << request_path.components.shift
122
143
 
123
144
  if controller = lookup_controller(controller_path)
124
145
  # Don't modify the original controller:
@@ -127,13 +148,13 @@ module Utopia
127
148
  # Append the controller to the set of controller variables, updates the controller with all current instance variables.
128
149
  variables << controller
129
150
 
130
- if result = controller.process!(request, relative_path)
151
+ if result = controller.process!(request, request_path)
131
152
  return result
132
153
  end
133
154
  end
134
155
  end
135
156
 
136
- # The controllers may have rewriten the path so we update the path info:
157
+ # Controllers can directly modify relative_path, which is copied into controller_path. The controllers may have rewriten the path so we update the path info:
137
158
  request.env[Rack::PATH_INFO] = controller_path.to_s
138
159
 
139
160
  # No controller gave a useful result:
@@ -0,0 +1,148 @@
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 '../http'
22
+
23
+ module Utopia
24
+ class Controller
25
+ module Actions
26
+ def self.prepended(base)
27
+ base.extend(ClassMethods)
28
+ end
29
+
30
+ class Action < Hash
31
+ def initialize(options = {}, &block)
32
+ @options = options
33
+ @callback = block
34
+
35
+ super()
36
+ end
37
+
38
+ attr_accessor :callback, :options
39
+
40
+ def callback?
41
+ @callback != nil
42
+ end
43
+
44
+ def eql? other
45
+ super and @callback.eql? other.callback and @options.eql? other.options
46
+ end
47
+
48
+ def hash
49
+ [super, @callback, @options].hash
50
+ end
51
+
52
+ def == other
53
+ super and @callback == other.callback and @options == other.options
54
+ end
55
+
56
+ WILDCARD_GREEDY = '**'.freeze
57
+ WILDCARD = '*'.freeze
58
+
59
+ # Given a path, iterate over all actions that match. Actions match from most specific to most general.
60
+ # @return nil if nothing matched, or true if something matched.
61
+ def apply(path, index = -1, &block)
62
+ # ** is greedy, it always matches if possible and matches all remaining input.
63
+ if match_all = self[WILDCARD_GREEDY] and match_all.callback?
64
+ matched = true; yield(match_all)
65
+ end
66
+
67
+ if name = path[index]
68
+ # puts "Matching #{name} in #{self.keys.inspect}"
69
+
70
+ if match_name = self[name]
71
+ # puts "Matched against exact name #{name}: #{match_name}"
72
+ matched = match_name.apply(path, index-1, &block) || matched
73
+ end
74
+
75
+ if match_one = self[WILDCARD]
76
+ # puts "Match against #{WILDCARD}: #{match_one}"
77
+ matched = match_one.apply(path, index-1, &block) || matched
78
+ end
79
+ elsif self.callback?
80
+ # Got to end, matched completely:
81
+ matched = true; yield(self)
82
+ end
83
+
84
+ return matched
85
+ end
86
+
87
+ def matching(path, &block)
88
+ to_enum(:apply, path).to_a
89
+ end
90
+
91
+ def define(path, **options, &callback)
92
+ # puts "Defining path: #{path.inspect}"
93
+ current = self
94
+
95
+ path.reverse_each do |name|
96
+ current = (current[name] ||= Action.new)
97
+ end
98
+
99
+ current.options = options
100
+ current.callback = callback
101
+
102
+ return current
103
+ end
104
+
105
+ def inspect
106
+ if callback?
107
+ "<action " + super + ":#{callback.source_location}(#{options})>"
108
+ else
109
+ "<action " + super + ">"
110
+ end
111
+ end
112
+ end
113
+
114
+ module ClassMethods
115
+ def actions
116
+ @actions ||= Action.new
117
+ end
118
+
119
+ def on(first, *path, **options, &block)
120
+ if first.is_a? Symbol
121
+ first = ['**', first.to_s]
122
+ end
123
+
124
+ actions.define(Path.split(first) + path, options, &block)
125
+ end
126
+
127
+ def dispatch(controller, request, path)
128
+ if @actions
129
+ @actions.apply(path.components) do |action|
130
+ controller.instance_exec(request, path, &action.callback)
131
+ end || controller.otherwise(request, path)
132
+ end
133
+ end
134
+ end
135
+
136
+ def otherwise(request, path)
137
+ end
138
+
139
+ # Given a request, call associated actions if at least one exists.
140
+ def process!(request, path)
141
+ # puts "Actions\#process!(..., #{path.inspect})"
142
+ catch_response do
143
+ self.class.dispatch(self, request, path)
144
+ end || super
145
+ end
146
+ end
147
+ end
148
+ end
@@ -23,14 +23,17 @@ require_relative '../http'
23
23
  module Utopia
24
24
  class Controller
25
25
  class Base
26
+ # A string which is the full path to the directory which contains the controller.
26
27
  def self.base_path
27
28
  self.const_get(:BASE_PATH)
28
29
  end
29
-
30
+
31
+ # A relative path to the controller directory relative to the controller root directory.
30
32
  def self.uri_path
31
33
  self.const_get(:URI_PATH)
32
34
  end
33
-
35
+
36
+ # The controller middleware itself.
34
37
  def self.controller
35
38
  self.const_get(:CONTROLLER)
36
39
  end
@@ -48,32 +51,6 @@ module Utopia
48
51
  def direct?(path)
49
52
  path.dirname == uri_path
50
53
  end
51
-
52
- def actions
53
- @actions ||= Action.new
54
- end
55
-
56
- def on(first, *path, **options, &block)
57
- if first.is_a? Symbol
58
- first = ['**', first]
59
- end
60
-
61
- actions.define(Path.split(first) + path, options, &block)
62
- end
63
-
64
- def lookup(path)
65
- if @actions
66
- relative_path = (path - uri_path).to_a
67
- return @actions.select(relative_path)
68
- else
69
- []
70
- end
71
- end
72
- end
73
-
74
- # Given a path, look up all matched actions.
75
- def actions_for_request(request, path)
76
- self.class.lookup(path)
77
54
  end
78
55
 
79
56
  def catch_response
@@ -82,18 +59,8 @@ module Utopia
82
59
  end
83
60
  end
84
61
 
85
- # Given a request, call associated actions if at least one exists.
86
- def passthrough(request, path)
87
- actions = actions_for_request(request, path)
88
-
89
- unless actions.empty?
90
- return catch_response do
91
- actions.each do |action|
92
- action.call(self, request, path)
93
- end
94
- end
95
- end
96
-
62
+ # 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.
63
+ def process!(request, relative_path)
97
64
  return nil
98
65
  end
99
66
 
@@ -114,7 +81,7 @@ module Utopia
114
81
  throw :response, response
115
82
  end
116
83
 
117
- # This will cause the controller middleware to pass on the request.
84
+ # This will cause the controller middleware to pass on the request.
118
85
  def ignore!
119
86
  throw :response, nil
120
87
  end
@@ -140,6 +107,7 @@ module Utopia
140
107
  respond! [status.to_i, {}, [message]]
141
108
  end
142
109
 
110
+ # Succeed the request and immediately respond.
143
111
  def succeed!(status: 200, headers: {}, **options)
144
112
  status = HTTP::Status.new(status, 200...300)
145
113
 
@@ -159,14 +127,6 @@ module Utopia
159
127
  return [content]
160
128
  end
161
129
  end
162
-
163
- # Legacy method name:
164
- alias success! succeed!
165
-
166
- # 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.
167
- def process!(request, path)
168
- passthrough(request, path)
169
- end
170
130
  end
171
131
  end
172
132
  end