utopia 1.9.11 → 2.0.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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +3 -2
  3. data/.gitignore +4 -1
  4. data/.rspec +1 -0
  5. data/.travis.yml +4 -0
  6. data/.yardopts +2 -0
  7. data/Gemfile +8 -1
  8. data/README.md +2 -2
  9. data/Rakefile +10 -10
  10. data/benchmarks/call_vs_check.rb +36 -0
  11. data/benchmarks/const_vs_hash.rb +33 -0
  12. data/documentation/Gemfile +5 -0
  13. data/documentation/Guardfile +20 -0
  14. data/documentation/config.ru +6 -13
  15. data/documentation/config/puma.rb +20 -0
  16. data/documentation/pages/_editor.xnode +64 -0
  17. data/documentation/pages/_heading.xnode +2 -2
  18. data/documentation/pages/_page.xnode +1 -2
  19. data/documentation/pages/errors/exception.xnode +3 -3
  20. data/documentation/pages/errors/file-not-found.xnode +3 -3
  21. data/documentation/pages/wiki/bower-integration/content.md +1 -1
  22. data/documentation/pages/wiki/content.md +6 -8
  23. data/documentation/pages/wiki/controller.rb +3 -3
  24. data/documentation/pages/wiki/edit.xnode +7 -19
  25. data/documentation/pages/wiki/middleware/content/content.md +4 -10
  26. data/documentation/pages/wiki/{controller → middleware/controller}/actions/content.md +0 -0
  27. data/documentation/pages/wiki/{controller → middleware/controller}/links.yaml +0 -0
  28. data/documentation/pages/wiki/{controller → middleware/controller}/rewrite/content.md +3 -3
  29. data/documentation/pages/wiki/show.xnode +4 -6
  30. data/documentation/pages/wiki/updating-utopia/content.md +55 -0
  31. data/documentation/pages/wiki/your-first-page/content.md +5 -3
  32. data/documentation/public/materials +1 -0
  33. data/lib/utopia.rb +3 -4
  34. data/lib/utopia/command.rb +4 -284
  35. data/lib/utopia/command/server.rb +115 -0
  36. data/lib/utopia/command/setup.rb +78 -0
  37. data/lib/utopia/command/site.rb +183 -0
  38. data/lib/utopia/content.rb +83 -59
  39. data/lib/utopia/content/{transaction.rb → document.rb} +116 -110
  40. data/lib/utopia/content/link.rb +7 -2
  41. data/lib/utopia/content/links.rb +2 -1
  42. data/lib/utopia/content/markup.rb +7 -2
  43. data/lib/utopia/{tags/deferred.rb → content/namespace.rb} +25 -6
  44. data/lib/utopia/content/node.rb +74 -76
  45. data/lib/utopia/content/response.rb +22 -3
  46. data/lib/utopia/content/tags.rb +66 -0
  47. data/lib/utopia/controller.rb +10 -18
  48. data/lib/utopia/controller/actions.rb +10 -0
  49. data/lib/utopia/controller/base.rb +2 -1
  50. data/lib/utopia/controller/respond.rb +1 -1
  51. data/lib/utopia/controller/rewrite.rb +8 -4
  52. data/lib/utopia/exceptions.rb +1 -0
  53. data/lib/utopia/exceptions/handler.rb +7 -2
  54. data/lib/utopia/exceptions/mailer.rb +33 -12
  55. data/lib/utopia/{tags/node.rb → extensions/array_split.rb} +11 -9
  56. data/lib/utopia/{tags/environment.rb → extensions/date_comparisons.rb} +24 -14
  57. data/lib/utopia/http.rb +2 -0
  58. data/lib/utopia/locale.rb +1 -0
  59. data/lib/utopia/localization.rb +37 -28
  60. data/lib/utopia/logger.rb +1 -0
  61. data/lib/utopia/logger/compact_formatter.rb +1 -0
  62. data/lib/utopia/middleware.rb +11 -1
  63. data/lib/utopia/path.rb +1 -0
  64. data/lib/utopia/path/matcher.rb +14 -2
  65. data/lib/utopia/redirection.rb +13 -16
  66. data/lib/utopia/session.rb +14 -6
  67. data/lib/utopia/setup.rb +3 -1
  68. data/lib/utopia/static.rb +11 -12
  69. data/lib/utopia/version.rb +1 -1
  70. data/setup/server/git/hooks/post-receive +0 -4
  71. data/setup/site/.gitignore +9 -0
  72. data/setup/site/.rspec +1 -0
  73. data/setup/site/Gemfile +4 -0
  74. data/setup/site/Guardfile +17 -0
  75. data/setup/site/Rakefile +2 -2
  76. data/setup/site/config.ru +5 -12
  77. data/setup/site/pages/_heading.xnode +2 -2
  78. data/setup/site/pages/_page.xnode +1 -1
  79. data/setup/site/pages/errors/exception.xnode +3 -3
  80. data/setup/site/pages/errors/file-not-found.xnode +3 -3
  81. data/setup/site/pages/welcome/index.xnode +3 -3
  82. data/setup/site/public/_static/site.css +4 -0
  83. data/setup/site/spec/spec_helper.rb +29 -0
  84. data/setup/site/tasks/deploy.rake +13 -0
  85. data/setup/site/tasks/development.rake +34 -0
  86. data/setup/site/tasks/environment.rake +17 -0
  87. data/spec/mock_node.rb +15 -0
  88. data/spec/spec_helper.rb +29 -0
  89. data/{lib/utopia/extensions/date.rb → spec/utopia/content/document_spec.rb} +31 -21
  90. data/spec/utopia/content/markup_spec.rb +2 -2
  91. data/spec/utopia/content/{tag_spec.rb → namespace_spec.rb} +17 -10
  92. data/spec/utopia/content/tags_spec.rb +80 -0
  93. data/spec/utopia/content_spec.rb +1 -1
  94. data/spec/utopia/content_spec.ru +1 -6
  95. data/spec/utopia/content_spec/_heading.xnode +1 -1
  96. data/spec/utopia/content_spec/content/test-partial.xnode +1 -1
  97. data/spec/utopia/content_spec/index.xnode +1 -1
  98. data/spec/utopia/controller/middleware_spec.ru +1 -3
  99. data/spec/utopia/controller/respond_spec.rb +2 -22
  100. data/spec/utopia/controller/respond_spec.ru +1 -5
  101. data/spec/utopia/controller/respond_spec/errors/file-not-found.xnode +7 -6
  102. data/spec/utopia/exceptions/handler_spec.ru +1 -2
  103. data/spec/utopia/exceptions/mailer_spec.ru +1 -2
  104. data/spec/utopia/extensions_spec.rb +2 -2
  105. data/spec/utopia/localization_spec.ru +1 -2
  106. data/spec/utopia/performance_spec.rb +2 -6
  107. data/spec/utopia/performance_spec/config.ru +5 -12
  108. data/spec/utopia/performance_spec/pages/_heading.xnode +2 -2
  109. data/spec/utopia/performance_spec/pages/_page.xnode +1 -1
  110. data/spec/utopia/performance_spec/pages/errors/exception.xnode +3 -3
  111. data/spec/utopia/performance_spec/pages/errors/file-not-found.xnode +3 -3
  112. data/spec/utopia/performance_spec/pages/welcome/index.xnode +3 -3
  113. data/spec/utopia/setup_spec.rb +79 -15
  114. data/utopia.gemspec +3 -3
  115. metadata +41 -27
  116. data/.simplecov +0 -9
  117. data/documentation/pages/welcome/index.xnode +0 -41
  118. data/lib/utopia/content/tag.rb +0 -90
  119. data/lib/utopia/extensions/array.rb +0 -29
  120. data/lib/utopia/tags/override.rb +0 -33
  121. data/setup/site/.simplecov +0 -9
  122. data/setup/site/tasks/test.rake +0 -10
  123. data/setup/site/tasks/utopia.rake +0 -41
  124. data/spec/utopia/controller/respond_spec/rewrite/controller.rb +0 -12
@@ -20,12 +20,14 @@
20
20
 
21
21
  require_relative 'markup'
22
22
  require_relative 'links'
23
- require_relative 'transaction'
23
+
24
+ require_relative 'document'
24
25
 
25
26
  require 'pathname'
26
27
 
27
28
  module Utopia
28
29
  class Content
30
+ # Represents an immutable node within the content hierarchy.
29
31
  class Node
30
32
  def initialize(controller, uri_path, request_path, file_path)
31
33
  @controller = controller
@@ -39,12 +41,16 @@ module Utopia
39
41
  attr :uri_path
40
42
  attr :file_path
41
43
 
44
+ def name
45
+ @uri_path.basename
46
+ end
47
+
42
48
  def link
43
49
  return Link.new(:file, uri_path)
44
50
  end
45
51
 
46
52
  def lookup_node(path)
47
- @controller.lookup_node(path)
53
+ @controller.lookup_node(@uri_path + Path[path])
48
54
  end
49
55
 
50
56
  def local_path(path = '.', base = nil)
@@ -60,17 +66,6 @@ module Utopia
60
66
  end
61
67
  end
62
68
 
63
- def lookup(tag)
64
- from_path = parent_path
65
-
66
- # If the current node is called 'foo', we can't lookup 'foo' in the current directory or we will have infinite recursion.
67
- if tag.name == @uri_path.basename
68
- from_path = from_path.dirname
69
- end
70
-
71
- return @controller.lookup_tag(tag.name, from_path)
72
- end
73
-
74
69
  def parent_path
75
70
  uri_path.dirname
76
71
  end
@@ -105,83 +100,86 @@ module Utopia
105
100
  def sibling_links(**options)
106
101
  return Links.index(@controller.root, siblings_path, options)
107
102
  end
108
-
109
- def call(transaction, state)
103
+
104
+ # Lookup the given tag which is being rendered within the given node. Invoked by {Document}.
105
+ # @return [Node] The node which will be used to render the tag.
106
+ def lookup_tag(tag)
107
+ return @controller.lookup_tag(tag.name, self)
108
+ end
109
+
110
+ # Invoked when the node is being rendered by {Document}.
111
+ def call(document, state)
110
112
  # Load the template:
111
113
  template = @controller.fetch_template(@file_path)
112
114
 
113
115
  # Evaluate the template/code:
114
- context = Context.new(transaction, state)
116
+ context = Context.new(document, state)
115
117
  markup = template.to_buffer(context)
116
118
 
117
- # Render the resulting markup into the transaction:
118
- transaction.parse_markup(markup)
119
+ # Render the resulting markup into the document:
120
+ document.parse_markup(markup)
119
121
  end
120
-
122
+
121
123
  def process!(request, attributes = {})
122
- transaction = Transaction.new(request)
123
-
124
- output = transaction.render_node(self, attributes)
125
-
126
- return [200, transaction.headers, [output]]
124
+ Document.render(self, request, attributes).to_a
127
125
  end
128
- end
129
126
 
130
- # This is a special context in which a limited set of well defined methods are exposed in the content view.
131
- Node::Context = Struct.new(:transaction, :state) do
132
- def partial(*args, &block)
133
- if block_given?
134
- state.defer(&block)
135
- else
136
- state.defer do |transaction|
137
- transaction.tag(*args)
127
+ # This is a special context in which a limited set of well defined methods are exposed in the content view.
128
+ Context = Struct.new(:document, :state) do
129
+ def partial(*args, &block)
130
+ if block_given?
131
+ state.defer(&block)
132
+ else
133
+ state.defer do |document|
134
+ document.tag(*args)
135
+ end
138
136
  end
139
137
  end
140
- end
141
-
142
- alias deferred_tag partial
143
-
144
- def controller
145
- transaction.controller
146
- end
147
-
148
- def localization
149
- transaction.localization
150
- end
151
-
152
- def request
153
- transaction.request
154
- end
155
-
156
- def response
157
- transaction
158
- end
159
-
160
- def attributes
161
- state.attributes
162
- end
163
-
164
- def [] key
165
- state.attributes.fetch(key) {transaction.attributes[key]}
166
- end
167
-
168
- alias current state
169
-
170
- def content
171
- transaction.content
172
- end
138
+
139
+ alias deferred_tag partial
140
+
141
+ def controller
142
+ document.controller
143
+ end
144
+
145
+ def localization
146
+ document.localization
147
+ end
148
+
149
+ def request
150
+ document.request
151
+ end
152
+
153
+ def response
154
+ document
155
+ end
156
+
157
+ def attributes
158
+ state.attributes
159
+ end
160
+
161
+ def [] key
162
+ state.attributes.fetch(key) {document.attributes[key]}
163
+ end
164
+
165
+ alias current state
166
+
167
+ def content
168
+ document.content
169
+ end
173
170
 
174
- def parent
175
- transaction.parent
176
- end
171
+ def parent
172
+ document.parent
173
+ end
177
174
 
178
- def first
179
- transaction.first
180
- end
181
-
182
- def links(*arguments, &block)
183
- state.node.links(*arguments, &block)
175
+ def first
176
+ document.first
177
+ end
178
+
179
+ def links(*arguments, &block)
180
+ state.node.links(*arguments, &block)
181
+ end
184
182
  end
185
183
  end
186
184
  end
187
- end
185
+ end
@@ -24,25 +24,44 @@ module Utopia
24
24
  EXPIRES = 'Expires'.freeze
25
25
  CACHE_CONTROL = 'Cache-Control'.freeze
26
26
  CONTENT_TYPE = 'Content-Type'.freeze
27
+ NO_CACHE = 'no-cache'.freeze
27
28
 
29
+ # A basic content response, including useful defaults for typical HTML5 content.
28
30
  class Response
29
31
  def initialize
30
32
  @status = 200
31
33
  @headers = {}
34
+ @body = []
35
+
36
+ # The default content type:
37
+ self.content_type = "text/html; charset=utf-8"
32
38
  end
33
39
 
34
40
  attr :status
35
41
  attr :headers
42
+ attr :body
43
+
44
+ def content
45
+ @body.join
46
+ end
47
+
48
+ def lookup(tag)
49
+ return nil
50
+ end
51
+
52
+ def to_a
53
+ [@status, @headers, @body]
54
+ end
36
55
 
37
56
  # Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
38
57
  def do_not_cache!
39
58
  @headers[CACHE_CONTROL] = "no-cache, must-revalidate"
40
59
  @headers[EXPIRES] = Time.now.httpdate
41
60
  end
42
-
43
- # Specify that the content should be cached.
61
+
62
+ # Specify that the content could be cached.
44
63
  def cache!(duration = 3600, access: "public")
45
- unless @headers[CACHE_CONTROL] =~ /no-cache/
64
+ unless cache_control = @headers[CACHE_CONTROL] and cache_control.include?(NO_CACHE)
46
65
  @headers[CACHE_CONTROL] = "#{access}, max-age=#{duration}"
47
66
  @headers[EXPIRES] = (Time.now + duration).httpdate
48
67
  end
@@ -0,0 +1,66 @@
1
+ # Copyright, 2017, 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 'namespace'
22
+
23
+ module Utopia
24
+ class Content
25
+ # Tags which provide intrinsic behaviour within the content middleware.
26
+ module Tags
27
+ extend Namespace
28
+
29
+ # Invokes a node and renders a single node to the output stream.
30
+ # @param path [String] The path of the node to invoke.
31
+ tag('node') do |document, state|
32
+ path = Path[state[:path]]
33
+
34
+ node = document.lookup_node(path)
35
+
36
+ document.render_node(node)
37
+ end
38
+
39
+ # Invokes a deferred tag from the current state. Works together with {Document::State#defer}.
40
+ # @param id [String] The id of the deferred to invoke.
41
+ tag('deferred') do |document, state|
42
+ id = state[:id].to_i
43
+
44
+ deferred = document.parent.deferred[id]
45
+
46
+ deferred.call(document, state)
47
+ end
48
+
49
+ # Renders the content of the parent node into the output of the document.
50
+ tag('content') do |document, state|
51
+ # We are invoking this node within a parent who has content, and we want to generate output equal to that.
52
+ document.write(document.parent.content)
53
+ end
54
+
55
+ # Render the contents only if in the correct environment.
56
+ # @param only [String] A comma separated list of environments to check.
57
+ tag('environment') do |document, state|
58
+ environment = document.attributes.fetch(:environment){RACK_ENV}.to_s
59
+
60
+ if state[:only].split(',').include?(environment)
61
+ document.parse_markup(state.content)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -54,39 +54,31 @@ module Utopia
54
54
  request.env[VARIABLES_KEY]
55
55
  end
56
56
 
57
- def initialize(app, root: nil, cache_controllers: false, base: nil)
57
+ # @param root [String] The content root where controllers will be loaded from.
58
+ # @param base [Class] The base class for controllers.
59
+ def initialize(app, root: Utopia::default_root, base: Controller::Base)
58
60
  @app = app
59
- @root = root || Utopia::default_root
61
+ @root = root
60
62
 
61
- if cache_controllers
62
- @controller_cache = Concurrent::Map.new
63
- else
64
- @controller_cache = nil
65
- end
63
+ @controller_cache = Concurrent::Map.new
66
64
 
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)
65
+ @base = base
70
66
  end
71
67
 
72
68
  attr :app
73
69
 
74
70
  def freeze
75
- @root.freeze
71
+ return self if frozen?
76
72
 
77
- # Should we freeze the base class?
78
- # @base.freeze
73
+ @root.freeze
74
+ @base.freeze
79
75
 
80
76
  super
81
77
  end
82
78
 
83
79
  # Fetch the controller for the given relative path. May be cached.
84
80
  def lookup_controller(path)
85
- if @controller_cache
86
- @controller_cache.fetch_or_store(path.to_s) do
87
- load_controller_file(path)
88
- end
89
- else
81
+ @controller_cache.fetch_or_store(path.to_s) do
90
82
  load_controller_file(path)
91
83
  end
92
84
  end
@@ -22,11 +22,17 @@ require_relative '../http'
22
22
 
23
23
  module Utopia
24
24
  class Controller
25
+ # A controller layer which invokes functinality based on the request path.
26
+ # @example
27
+ # on '*' do |request, path|
28
+ # succeed! content: 'Hello World'
29
+ # end
25
30
  module Actions
26
31
  def self.prepended(base)
27
32
  base.extend(ClassMethods)
28
33
  end
29
34
 
35
+ # A nested action lookup hash table.
30
36
  class Action < Hash
31
37
  def initialize(options = {}, &block)
32
38
  @options = options
@@ -53,7 +59,10 @@ module Utopia
53
59
  super and @callback == other.callback and @options == other.options
54
60
  end
55
61
 
62
+ # Matches 0 or more path components.
56
63
  WILDCARD_GREEDY = '**'.freeze
64
+
65
+ # Matches any 1 path component.
57
66
  WILDCARD = '*'.freeze
58
67
 
59
68
  # Given a path, iterate over all actions that match. Actions match from most specific to most general.
@@ -112,6 +121,7 @@ module Utopia
112
121
  end
113
122
  end
114
123
 
124
+ # Exposed to the controller class.
115
125
  module ClassMethods
116
126
  def self.extended(klass)
117
127
  klass.instance_eval do
@@ -22,6 +22,7 @@ require_relative '../http'
22
22
 
23
23
  module Utopia
24
24
  class Controller
25
+ # The base implementation of a controller class.
25
26
  class Base
26
27
  # A string which is the full path to the directory which contains the controller.
27
28
  def self.base_path
@@ -87,7 +88,7 @@ module Utopia
87
88
  end
88
89
 
89
90
  # Request relative redirect. Respond with a redirect to the given target.
90
- def redirect! (target, status = 302)
91
+ def redirect!(target, status = 302)
91
92
  status = HTTP::Status.new(status, 300...400)
92
93
  location = target.to_s
93
94
 
@@ -23,7 +23,7 @@ require_relative '../path/matcher'
23
23
 
24
24
  module Utopia
25
25
  class Controller
26
- # This controller layer provides a convenient way to respond to different requested content types. The order in which you add converters matters, as it determins how the incoming Accept: header is mapped, e.g. the first converter is also defined as matching the media range */*.
26
+ # A controller layer which provides a convenient way to respond to different requested content types. The order in which you add converters matters, as it determines how the incoming Accept: header is mapped, e.g. the first converter is also defined as matching the media range */*.
27
27
  module Respond
28
28
  def self.prepended(base)
29
29
  base.extend(ClassMethods)
@@ -25,15 +25,16 @@ module Utopia
25
25
  class Controller
26
26
  # This controller layer rewrites the path before executing controller actions. When the rule matches, the supplied block is executed.
27
27
  # @example
28
- # prepend Rewrite
29
- # rewrite.extract_prefix id: Integer do
30
- # @user = User.find(@id)
31
- # end
28
+ # prepend Rewrite
29
+ # rewrite.extract_prefix id: Integer do
30
+ # @user = User.find(@id)
31
+ # end
32
32
  module Rewrite
33
33
  def self.prepended(base)
34
34
  base.extend(ClassMethods)
35
35
  end
36
36
 
37
+ # A abstract rule which can match against a request path.
37
38
  class Rule
38
39
  def apply_match_to_context(match_data, context)
39
40
  match_data.names.each do |name|
@@ -42,6 +43,7 @@ module Utopia
42
43
  end
43
44
  end
44
45
 
46
+ # A rule which extracts a prefix pattern from the request path.
45
47
  class ExtractPrefixRule < Rule
46
48
  def initialize(patterns, block)
47
49
  @matcher = Path::Matcher.new(patterns)
@@ -70,6 +72,7 @@ module Utopia
70
72
  end
71
73
  end
72
74
 
75
+ # Rewrite a request path based on a set of defined rules.
73
76
  class Rewriter
74
77
  def initialize
75
78
  @rules = []
@@ -94,6 +97,7 @@ module Utopia
94
97
  end
95
98
  end
96
99
 
100
+ # Exposed to the controller class.
97
101
  module ClassMethods
98
102
  def rewrite
99
103
  @rewriter ||= Rewriter.new