utopia 1.7.1 → 1.8.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 (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