utopia 1.9.11 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -22,6 +22,7 @@ require_relative 'exceptions/handler'
22
22
  require_relative 'exceptions/mailer'
23
23
 
24
24
  module Utopia
25
+ # Middleware for handling exceptional situations.
25
26
  module Exceptions
26
27
  end
27
28
  end
@@ -20,8 +20,9 @@
20
20
 
21
21
  module Utopia
22
22
  module Exceptions
23
- # Catches exceptions and performs an internal redirect.
23
+ # A middleware which catches exceptions and performs an internal redirect.
24
24
  class Handler
25
+ # @param location [String] Peform an internal redirect to this location when an exception is raised.
25
26
  def initialize(app, location = '/errors/exception')
26
27
  @app = app
27
28
 
@@ -29,6 +30,8 @@ module Utopia
29
30
  end
30
31
 
31
32
  def freeze
33
+ return self if frozen?
34
+
32
35
  @location.freeze
33
36
 
34
37
  super
@@ -74,7 +77,9 @@ module Utopia
74
77
  end
75
78
  end
76
79
 
77
- private def write_exception_to_stream(stream, env, exception, include_backtrace = false)
80
+ private
81
+
82
+ def write_exception_to_stream(stream, env, exception, include_backtrace = false)
78
83
  buffer = []
79
84
 
80
85
  buffer << "While requesting resource #{env[Rack::PATH_INFO].inspect}, a fatal error occurred:"
@@ -23,7 +23,7 @@ require 'mail'
23
23
 
24
24
  module Utopia
25
25
  module Exceptions
26
- # Catches all exceptions raised from the app it wraps and sends a useful email with the exception, stacktrace, and contents of the environment.
26
+ # A middleware which catches all exceptions raised from the app it wraps and sends a useful email with the exception, stacktrace, and contents of the environment.
27
27
  class Mailer
28
28
  # A basic local non-authenticated SMTP server.
29
29
  LOCAL_SMTP = [:smtp, {
@@ -32,17 +32,36 @@ module Utopia
32
32
  :enable_starttls_auto => false
33
33
  }]
34
34
 
35
- def initialize(app, config = {})
35
+ DEFAULT_FROM = (ENV['USER'] || 'rack') + "@localhost"
36
+ DEFAULT_SUBJECT = '%{exception} [PID %{pid} : %{cwd}]'
37
+
38
+ # @param to [String] The address to email error reports to.
39
+ # @param from [String] The from address for error reports.
40
+ # @param subject [String] The subject template which can access attributes defined by `#attributes_for`.
41
+ # @param delivery_method [Object] The delivery method as required by the mail gem.
42
+ # @param dump_environment [Boolean] Attach `env` as `environment.yaml` to the error report.
43
+ def initialize(app, to: "postmaster", from: DEFAULT_FROM, subject: DEFAULT_SUBJECT, delivery_method: LOCAL_SMTP, dump_environment: false)
36
44
  @app = app
37
45
 
38
- @to = config[:to] || "postmaster"
39
- @from = config.fetch(:from) {(ENV['USER'] || 'rack') + "@localhost"}
40
- @subject = config[:subject] || '%{exception} [PID %{pid} : %{cwd}]'
41
- @delivery_method = config.fetch(:delivery_method, LOCAL_SMTP)
46
+ @to = to
47
+ @from = from
48
+ @subject = subject
49
+ @delivery_method = delivery_method
50
+ @dump_environment = dump_environment
51
+ end
52
+
53
+ def freeze
54
+ return self if frozen?
55
+
56
+ @to.freeze
57
+ @from.freeze
58
+ @subject.freeze
59
+ @delivery_method.freeze
60
+ @dump_environment.freeze
42
61
 
43
- @dump_environment = config.fetch(:dump_environment, false)
62
+ super
44
63
  end
45
-
64
+
46
65
  def call(env)
47
66
  begin
48
67
  return @app.call(env)
@@ -100,17 +119,19 @@ module Utopia
100
119
  return io.string
101
120
  end
102
121
 
103
- def generate_mail(exception, env)
104
- attributes = {
122
+ def attributes_for(exception, env)
123
+ {
105
124
  exception: exception.class.name,
106
125
  pid: $$,
107
126
  cwd: Dir.getwd,
108
127
  }
109
-
128
+ end
129
+
130
+ def generate_mail(exception, env)
110
131
  mail = Mail.new(
111
132
  :from => @from,
112
133
  :to => @to,
113
- :subject => @subject % attributes
134
+ :subject => @subject % attributes_for(exception, env)
114
135
  )
115
136
 
116
137
  mail.text_part = Mail::Part.new
@@ -19,15 +19,17 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Utopia
22
- module Tags
23
- class Node
24
- def self.call(transaction, state)
25
- path = Path[state[:path]]
26
-
27
- node = transaction.lookup_node(path)
28
-
29
- transaction.render_node(node)
22
+ module Extensions
23
+ module ArraySplit
24
+ def split_at(*args, &block)
25
+ if middle = index(*args, &block)
26
+ [self[0...middle], self[middle], self[middle+1..-1]]
27
+ else
28
+ [[], nil, []]
29
+ end
30
30
  end
31
31
  end
32
+
33
+ ::Array.prepend(ArraySplit)
32
34
  end
33
- end
35
+ end
@@ -18,24 +18,34 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
+ require 'date'
22
+
21
23
  module Utopia
22
- module Tags
23
- class Environment
24
- def self.for(environment)
25
- self.new(environment)
26
- end
27
-
28
- def initialize(environment)
29
- @environment = environment
24
+ module Extensions
25
+ # Provides comparison operator extensions.
26
+ module TimeDateComparison
27
+ def <=>(other)
28
+ if Date === other or DateTime === other
29
+ self.to_datetime <=> other
30
+ else
31
+ super
32
+ end
30
33
  end
31
-
32
- def call(transaction, state)
33
- only = state[:only].split(",").collect(&:to_sym) rescue []
34
+ end
35
+
36
+ ::Time.prepend(TimeDateComparison)
34
37
 
35
- if defined?(@environment) and only.include?(@environment)
36
- transaction.parse_markup(state.content)
38
+ # Provides comparison operator extensions.
39
+ module DateTimeComparison
40
+ def <=>(other)
41
+ if Time === other
42
+ self.to_datetime <=> other.to_datetime
43
+ else
44
+ super
37
45
  end
38
46
  end
39
47
  end
48
+
49
+ ::Date.prepend(DateTimeComparison)
40
50
  end
41
- end
51
+ end
@@ -23,7 +23,9 @@ require 'rack'
23
23
  require 'http/accept'
24
24
 
25
25
  module Utopia
26
+ # HTTP protocol implementation.
26
27
  module HTTP
28
+ # Pull in {::HTTP::Accept} for parsing.
27
29
  Accept = ::HTTP::Accept
28
30
 
29
31
  # A list of commonly used HTTP status codes.
@@ -19,6 +19,7 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Utopia
22
+ # A structured representation of locale based on RFC3066.
22
23
  Locale = Struct.new(:language, :country, :variant) do
23
24
  def to_s
24
25
  to_a.compact.join('-')
@@ -21,7 +21,7 @@
21
21
  require_relative 'middleware'
22
22
 
23
23
  module Utopia
24
- # If you request a URL which has localized content, a localized redirect would be returned based on the content requested.
24
+ # A middleware which attempts to find localized content.
25
25
  class Localization
26
26
  # A wrapper to provide easy access to locale related data in the request.
27
27
  class Wrapper
@@ -59,38 +59,42 @@ module Utopia
59
59
  LOCALIZATION_KEY = 'utopia.localization'.freeze
60
60
  CURRENT_LOCALE_KEY = 'utopia.localization.current_locale'.freeze
61
61
 
62
- DEFAULT_LOCALE = 'en'
63
-
64
- def initialize(app, **options)
62
+ # @param locales [Array<String>] An array of all supported locales.
63
+ # @param default_locale [String] The default locale if none is provided.
64
+ # @param default_locales [String] The locales to try in order if none is provided.
65
+ # @param hosts [Hash<Pattern, String>] Specify a mapping of the HTTP_HOST header to a given locale.
66
+ # @param ignore [Array<Pattern>] A list of patterns matched against PATH_INFO which will not be localized.
67
+ def initialize(app, locales:, default_locale: nil, default_locales: nil, hosts: {}, ignore: [], **options)
65
68
  @app = app
66
69
 
67
- @all_locales = HTTP::Accept::Languages::Locales.new(options[:locales])
70
+ @all_locales = HTTP::Accept::Languages::Locales.new(locales)
68
71
 
69
- # Locales here are represented as an array of strings, e.g. ['en', 'ja', 'cn', 'de'].
70
- unless @default_locales = options[:default_locales]
72
+ # Locales here are represented as an array of strings, e.g. ['en', 'ja', 'cn', 'de'] and are used in order if no locale is specified by the user.
73
+ unless @default_locales = default_locales
71
74
  # We append nil, i.e. no localization.
72
75
  @default_locales = @all_locales.names + [nil]
73
76
  end
74
77
 
75
- if @default_locale = options[:default_locale]
76
- @default_locales.unshift(default_locale)
77
- else
78
- @default_locale = @default_locales.first
79
- end
78
+ @default_locale = default_locale || @default_locales.first
80
79
 
81
- @hosts = options[:hosts] || {}
80
+ unless @default_locales.include? @default_locale
81
+ @default_locales.unshift(@default_locale)
82
+ end
82
83
 
83
- @nonlocalized = options.fetch(:nonlocalized, [])
84
+ # Select a localization based on a request host name:
85
+ @hosts = hosts
84
86
 
85
- self.freeze
87
+ @ignore = ignore || options[:nonlocalized]
86
88
  end
87
89
 
88
90
  def freeze
91
+ return self if frozen?
92
+
89
93
  @all_locales.freeze
90
94
  @default_locales.freeze
91
95
  @default_locale.freeze
92
96
  @hosts.freeze
93
- @nonlocalized.freeze
97
+ @ignore.freeze
94
98
 
95
99
  super
96
100
  end
@@ -104,7 +108,7 @@ module Utopia
104
108
  # Keep track of what locales have been tried:
105
109
  locales = Set.new
106
110
 
107
- host_preferred_locales(env).each do |locale|
111
+ host_preferred_locales(env) do |locale|
108
112
  yield env.merge(CURRENT_LOCALE_KEY => locale) if locales.add? locale
109
113
  end
110
114
 
@@ -124,16 +128,13 @@ module Utopia
124
128
  end
125
129
  end
126
130
 
127
- HTTP_HOST = 'HTTP_HOST'.freeze
128
-
129
131
  def host_preferred_locales(env)
130
132
  http_host = env[Rack::HTTP_HOST]
131
133
 
132
- # Get a list of all hosts which match the incoming htt_host:
133
- matching_hosts = @hosts.select{|host_pattern, locale| http_host =~ host_pattern}
134
-
135
- # Extract all the valid locales:
136
- matching_hosts.flat_map{|host_pattern, locale| locale}
134
+ # Yield all hosts which match the incoming http_host:
135
+ @hosts.each do |pattern, locale|
136
+ yield locale if http_host[pattern]
137
+ end
137
138
  end
138
139
 
139
140
  def request_preferred_locale(env)
@@ -163,10 +164,18 @@ module Utopia
163
164
  return []
164
165
  end
165
166
 
166
- def nonlocalized?(env)
167
+ SAFE_METHODS = ['GET', 'HEAD']
168
+
169
+ def localized?(env)
170
+ # Only SAFE_METHODS can be localized:
171
+ request_method = env[Rack::REQUEST_METHOD]
172
+ return false unless SAFE_METHODS.include?(request_method)
173
+
174
+ # Ignore requests which match the ignored paths:
167
175
  path_info = env[Rack::PATH_INFO]
176
+ return false if @ignore.any? { |pattern| path_info[pattern] != nil }
168
177
 
169
- @nonlocalized.any? { |pattern| path_info[pattern] != nil }
178
+ return true
170
179
  end
171
180
 
172
181
  # Set the Vary: header on the response to indicate that this response should include the header in the cache key.
@@ -190,8 +199,8 @@ module Utopia
190
199
  end
191
200
 
192
201
  def call(env)
193
- # Pass the request through with no localization if it is a nonlocalized path:
194
- return @app.call(env) if nonlocalized?(env)
202
+ # Pass the request through if it shouldn't be localized:
203
+ return @app.call(env) unless localized?(env)
195
204
 
196
205
  env[LOCALIZATION_KEY] = self
197
206
 
@@ -21,6 +21,7 @@
21
21
  require_relative 'logger/compact_formatter'
22
22
 
23
23
  module Utopia
24
+ # Manages an instance of Logger with useful defaults. Used in rake `tasks/log.rake`.
24
25
  module Logger
25
26
  def self.new(output: STDERR, level: ::Logger::WARN)
26
27
  log = ::Logger.new(output)
@@ -23,6 +23,7 @@ require 'rainbow'
23
23
 
24
24
  module Utopia
25
25
  module Logger
26
+ # Provides a concise log output format.
26
27
  class CompactFormatter
27
28
  def initialize
28
29
  @start = Time.now
@@ -24,12 +24,22 @@ require_relative 'http'
24
24
  require_relative 'path'
25
25
 
26
26
  module Utopia
27
+ # The default pages path for {Utopia::Content} middleware.
27
28
  PAGES_PATH = 'pages'.freeze
28
29
 
29
- # This is used for shared controller variables which get consumed by the content middleware:
30
+ # This is used for shared controller variables which get consumed by the content middleware.
30
31
  VARIABLES_KEY = 'utopia.variables'.freeze
31
32
 
33
+ # The default root directory for middleware to operate within, e.g. the web-site directory. Convention over configuration.
34
+ # @param subdirectory [String] Appended to the default root to make a more specific path.
35
+ # @param pwd [String] The working directory for the current site.
32
36
  def self.default_root(subdirectory = PAGES_PATH, pwd = Dir.pwd)
33
37
  File.expand_path(subdirectory, pwd)
34
38
  end
39
+
40
+ # The same as {default_root} but returns an instance of {Path}.
41
+ # @return [Path] The path as requested.
42
+ def self.default_path(*args)
43
+ Path[default_root(*args)]
44
+ end
35
45
  end
@@ -65,6 +65,7 @@ module Utopia
65
65
  end
66
66
  end
67
67
 
68
+ # Represents a path as an array of path components. Useful for efficient URL manipulation.
68
69
  class Path
69
70
  include Comparable
70
71
 
@@ -22,14 +22,23 @@ require_relative '../path'
22
22
 
23
23
  module Utopia
24
24
  class Path
25
+ # Performs structured, efficient, matches against {Path} instances. Supports regular expressions, type-casts and constants.
26
+ # @example
27
+ # path = Utopia::Path['users/20/edit']
28
+ # matcher = Utopia::Path::Matcher[users: /users/, id: Integer, action: String]
29
+ # match_data = matcher.match(path)
25
30
  class Matcher
31
+ # The result of matching against a {Path}.
26
32
  class MatchData
27
33
  def initialize(named_parts, post_match)
28
34
  @named_parts = named_parts
29
35
  @post_match = Path[post_match]
30
36
  end
31
37
 
38
+ # Matched components by name.
32
39
  attr :named_parts
40
+
41
+ # Any remaining part past the end of the explicitly matched components.
33
42
  attr :post_match
34
43
 
35
44
  def [] key
@@ -41,7 +50,7 @@ module Utopia
41
50
  end
42
51
  end
43
52
 
44
- # patterns = {key: /\d+/, 'foo', }
53
+ # @param patterns [Hash<Symbol,Pattern>] An ordered list of things to match.
45
54
  def initialize(patterns = [])
46
55
  @patterns = patterns
47
56
  end
@@ -64,14 +73,16 @@ module Utopia
64
73
  return nil
65
74
  end
66
75
 
67
- # This is a path prefix matching algorithm. The pattern is an array of String, Symbol, Regexp, or nil. The components is an array of String.
76
+ # This is a path prefix matching algorithm. The pattern is an array of String, Symbol, Regexp, or nil. The path is an array of String.
68
77
  def match(path)
69
78
  components = path.to_a
70
79
 
80
+ # Can't possibly match if not enough components:
71
81
  return nil if components.size < @patterns.size
72
82
 
73
83
  named_parts = {}
74
84
 
85
+ # Try to match each component against the pattern:
75
86
  @patterns.each_with_index do |(key, pattern), index|
76
87
  component = components[index]
77
88
 
@@ -83,6 +94,7 @@ module Utopia
83
94
  if result = pattern.match(component)
84
95
  named_parts[key] = result
85
96
  else
97
+ # Couldn't match:
86
98
  return nil
87
99
  end
88
100
  else