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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0fb046791da67c397e853162649620dc2b530d3e
4
- data.tar.gz: 390a488937297cec03a267206d4a096d4c74a497
3
+ metadata.gz: 452905bf411c5f82c5fe02c778c50a27b25234cb
4
+ data.tar.gz: 43df0cc23493c453bddf7d19b58b02e65bb15619
5
5
  SHA512:
6
- metadata.gz: 9bd0a3820ebe9422484bb3c1bf048c99b7f7d0e9907eb0cdfccf292ecb6c55e8927cbdeacf87f96b4ad759b7047290f7622639438997d2fa53c96ced3fd24e26
7
- data.tar.gz: 9cca58be165173f26f72cee8bde2b7b5b1052018c2a1fff7946476228cc09c63eb48e4d51a85da50893e59894b33c504afede627b5cfeb51dd15c466af3b94fa
6
+ metadata.gz: 0ab0769846592da4bb8ce571d48cfc7465d3b773b64739781bae3fd4c6900603d162ad0a7e72d49ad049ca011858256a87191d9d13a9027fc703934bccf2224f
7
+ data.tar.gz: 2988e596315c47ee2026d8fe5f64d075bb3b99e3fe3fdf35e3ab0217716cee83cb9f472f16dface2e2526d4c086b21fe9a7732f1f535126e3ebfc47667094c21
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .tags*
@@ -1,12 +1,16 @@
1
1
  language: ruby
2
2
  sudo: false
3
+ before_install:
4
+ # For testing purposes:
5
+ - git config --global user.email "you@example.com"
6
+ - git config --global user.name "Your Name"
3
7
  rvm:
4
8
  - 2.1.8
5
9
  - 2.2.4
6
10
  - 2.3.0
7
11
  - ruby-head
8
12
  - rbx-2
9
- env: COVERAGE=true
13
+ env: COVERAGE=true BENCHMARK=true
10
14
  matrix:
11
15
  allow_failures:
12
16
  - rvm: rbx-2
data/Gemfile CHANGED
@@ -11,6 +11,9 @@ group :development do
11
11
  end
12
12
 
13
13
  group :test do
14
+ gem 'benchmark-ips'
15
+ gem 'ruby-prof'
16
+
14
17
  gem 'rack-test'
15
18
  gem 'simplecov'
16
19
  gem 'coveralls', require: false
data/README.md CHANGED
@@ -74,6 +74,17 @@ Then, Nginx is configured like so:
74
74
  rewrite ^ http://www.example.com$uri permanent;
75
75
  }
76
76
 
77
+ #### Compression
78
+
79
+ We suggest [enabling gzip compression](https://zoompf.com/blog/2012/02/lose-the-wait-http-compression):
80
+
81
+ gzip on;
82
+ gzip_vary on;
83
+ gzip_comp_level 6;
84
+ gzip_http_version 1.1;
85
+ gzip_proxied any;
86
+ gzip_types text/* image/svg+xml application/json application/javascript;
87
+
77
88
  ## Usage
78
89
 
79
90
  Utopia builds on top of Rack with the following middleware:
@@ -0,0 +1,52 @@
1
+ require 'benchmark/ips'
2
+ require 'ostruct'
3
+
4
+ # This benchmark compares accessing an instance variable vs accessing a struct member (via a function). The actual method dispatch is about 25% slower.
5
+
6
+ puts "Ruby #{RUBY_VERSION} at #{Time.now}"
7
+
8
+ NAME = "Test Name"
9
+ EMAIL = "test@example.org"
10
+
11
+ test = nil
12
+
13
+ class ObjectHash
14
+ def []= key, value
15
+ instance_variable_set(key, value)
16
+ end
17
+
18
+ def [] key
19
+ instance_variable_get(key)
20
+ end
21
+ end
22
+
23
+ # There IS a measuarble difference:
24
+ Benchmark.ips do |x|
25
+ x.report("Hash") do |i|
26
+ i.times do
27
+ p = {name: NAME, email: EMAIL}
28
+
29
+ test = p[:name] + p[:email]
30
+ end
31
+ end
32
+
33
+ x.report("OpenStruct") do |i|
34
+ i.times do
35
+ p = OpenStruct.new(name: NAME, email: EMAIL)
36
+
37
+ test = p.name + p.email
38
+ end
39
+ end
40
+
41
+ x.report("ObjectHash") do |i|
42
+ i.times do
43
+ o = ObjectHash.new
44
+ o[:@name] = NAME
45
+ o[:@email] = EMAIL
46
+
47
+ test = o[:@name] + o[:@email]
48
+ end
49
+ end
50
+
51
+ x.compare!
52
+ end
@@ -0,0 +1,89 @@
1
+ require 'benchmark/ips'
2
+
3
+ # This benchmark compares accessing an instance variable vs accessing a struct member (via a function). The actual method dispatch is about 25% slower.
4
+
5
+ puts "Ruby #{RUBY_VERSION} at #{Time.now}"
6
+
7
+ ItemsStruct = Struct.new(:items) do
8
+ def initialize
9
+ super []
10
+ end
11
+
12
+ def push_me_pull_you(value = :x)
13
+ items = self.items
14
+
15
+ items << value
16
+ items.pop
17
+ end
18
+
19
+ def empty?
20
+ self.items.empty?
21
+ end
22
+ end
23
+
24
+ class ItemsClass
25
+ def initialize
26
+ @items = []
27
+ end
28
+
29
+ def push_me_pull_you(value = :x)
30
+ items = @items
31
+
32
+ items << value
33
+ items.pop
34
+ end
35
+
36
+ def empty?
37
+ @items.empty?
38
+ end
39
+ end
40
+
41
+ # There IS a measuarble difference:
42
+ Benchmark.ips do |x|
43
+ x.report("Struct#empty?") do |times|
44
+ i = 0
45
+ instance = ItemsStruct.new
46
+
47
+ while i < times
48
+ break unless instance.empty?
49
+ i += 1
50
+ end
51
+ end
52
+
53
+ x.report("Class#empty?") do |times|
54
+ i = 0
55
+ instance = ItemsClass.new
56
+
57
+ while i < times
58
+ break unless instance.empty?
59
+ i += 1
60
+ end
61
+ end
62
+
63
+ x.compare!
64
+ end
65
+
66
+ # This shows that in the presence of additional work, the difference is neglegible.
67
+ Benchmark.ips do |x|
68
+ x.report("Struct#push_me_pull_you") do |times|
69
+ i = 0
70
+ a = A.new
71
+
72
+ while i < times
73
+ a.push_me_pull_you(i)
74
+ i += 1
75
+ end
76
+ end
77
+
78
+ x.report("Class#push_me_pull_you") do |times|
79
+ i = 0
80
+ b = B.new
81
+
82
+ while i < times
83
+ b.push_me_pull_you(i)
84
+ i += 1
85
+ end
86
+ end
87
+
88
+ x.compare!
89
+ end
data/bin/utopia CHANGED
@@ -20,14 +20,11 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  # THE SOFTWARE.
22
22
 
23
- require 'rubygems'
24
- require 'rake'
25
-
26
23
  require_relative '../lib/utopia/version'
27
24
 
25
+ require 'rake'
28
26
  require 'fileutils'
29
27
  require 'find'
30
- require 'rake'
31
28
 
32
29
  $app = Rake.application = Rake::Application.new
33
30
  $app.init(File.basename($0))
@@ -61,7 +58,7 @@ desc "Create a local utopia instance which includes a basic website template.\n"
61
58
  task :create do
62
59
  destination_root = File.expand_path(ARGV.last || '.', Dir.getwd)
63
60
 
64
- $stderr.puts "Setting up initial site in #{destination_root}..."
61
+ $stderr.puts "Setting up initial site in #{destination_root} for Utopia v#{Utopia::VERSION}..."
65
62
 
66
63
  Setup::Site::DIRECTORIES.each do |directory|
67
64
  FileUtils.mkdir_p(File.join(destination_root, directory))
@@ -93,6 +90,8 @@ task :create do
93
90
  end
94
91
 
95
92
  Dir.chdir(destination_root) do
93
+ puts "Setting up site in #{destination_root}..."
94
+
96
95
  if `which bundle`.strip != ''
97
96
  puts "Generating initial package list with bundle..."
98
97
  sh("bundle", "install", "--quiet")
@@ -26,6 +26,7 @@ require_relative 'utopia/localization'
26
26
  require_relative 'utopia/exceptions'
27
27
  require_relative 'utopia/redirection'
28
28
  require_relative 'utopia/static'
29
+ require_relative 'utopia/content_length'
29
30
 
30
31
  require_relative 'utopia/tags/deferred'
31
32
  require_relative 'utopia/tags/environment'
@@ -57,22 +57,18 @@ module Utopia
57
57
 
58
58
  attr :root
59
59
 
60
- def fetch_xml(path)
60
+ def fetch_template(path)
61
61
  if @template_cache
62
62
  @template_cache.fetch_or_store(path.to_s) do
63
- Trenni::Template.load(path)
63
+ Trenni::Template.load_file(path)
64
64
  end
65
65
  else
66
- Trenni::Template.load(path)
66
+ Trenni::Template.load_file(path)
67
67
  end
68
68
  end
69
69
 
70
- # Look up a named tag such as <entry />
71
- def lookup_tag(name, parent_path)
72
- if @tags.key? name
73
- return @tags[name]
74
- end
75
-
70
+ # This function looks up a named tag in a given path. It's a hotspot and needs improvement.
71
+ private def fetch_tag(name, parent_path)
76
72
  if String === name && name.index('/')
77
73
  name = Path.create(name)
78
74
  end
@@ -84,26 +80,39 @@ module Utopia
84
80
  else
85
81
  name_path = name + XNODE_EXTENSION
86
82
  end
87
-
88
- parent_path.ascend do |dir|
89
- tag_path = File.join(root, dir.components, name_path)
83
+
84
+ components = parent_path.components.dup
85
+
86
+ while components.any?
87
+ tag_path = File.join(root, components, name_path)
90
88
 
91
89
  if File.exist? tag_path
92
- return Node.new(self, dir + name, parent_path + name, tag_path)
90
+ return Node.new(self, Path[components] + name, parent_path + name, tag_path)
93
91
  end
94
92
 
95
93
  if String === name_path
96
- tag_path = File.join(root, dir.components, '_' + name_path)
94
+ tag_path = File.join(root, components, '_' + name_path)
97
95
 
98
96
  if File.exist? tag_path
99
- return Node.new(self, dir + name, parent_path + name, tag_path)
97
+ return Node.new(self, Path[components] + name, parent_path + name, tag_path)
100
98
  end
101
99
  end
100
+
101
+ components.pop
102
102
  end
103
103
 
104
104
  return nil
105
105
  end
106
106
 
107
+ # Look up a named tag such as <entry />
108
+ def lookup_tag(name, parent_path)
109
+ if @tags.key? name
110
+ return @tags[name]
111
+ end
112
+
113
+ return fetch_tag(name, parent_path)
114
+ end
115
+
107
116
  # The request_path is an absolute uri path, e.g. /foo/bar. If an xnode file exists on disk for this exact path, it is instantiated, otherwise nil.
108
117
  def lookup_node(request_path)
109
118
  name = request_path.last
@@ -107,14 +107,80 @@ module Utopia
107
107
  end
108
108
 
109
109
  def call(transaction, state)
110
- xml_data = @controller.fetch_xml(@file_path).evaluate(transaction)
110
+ template = @controller.fetch_template(@file_path)
111
111
 
112
- transaction.parse_xml(xml_data)
112
+ context = Context.new(transaction, state)
113
+ markup = template.evaluate(context)
114
+
115
+ transaction.parse_markup(markup)
113
116
  end
114
117
 
115
118
  def process!(request, response, attributes = {})
116
119
  transaction = Transaction.new(request, response)
117
- response.write(transaction.render_node(self, attributes))
120
+ output = transaction.render_node(self, attributes)
121
+ response.write(output)
122
+ end
123
+ end
124
+
125
+ # This is a special context in which a limited set of well defined methods are exposed in the content view.
126
+ Node::Context = Struct.new(:transaction, :state) do
127
+ def initialize(transaction, state)
128
+ # We expose all attributes as instance variables within the context:
129
+ state.attributes.each do |key, value|
130
+ self.instance_variable_set("@#{key}".to_sym, value)
131
+ end
132
+
133
+ super
134
+ end
135
+
136
+ def partial(*args, &block)
137
+ if block_given?
138
+ state.defer(&block)
139
+ else
140
+ state.defer do |transaction|
141
+ transaction.tag(*args)
142
+ end
143
+ end
144
+ end
145
+
146
+ alias deferred_tag partial
147
+
148
+ def controller
149
+ transaction.controller
150
+ end
151
+
152
+ def localization
153
+ transaction.localization
154
+ end
155
+
156
+ def request
157
+ transaction.request
158
+ end
159
+
160
+ def response
161
+ transaction.response
162
+ end
163
+
164
+ def attributes
165
+ state.attributes
166
+ end
167
+
168
+ def [] key
169
+ state.attributes.fetch(key) {transaction.attributes[key]}
170
+ end
171
+
172
+ alias current state
173
+
174
+ def content
175
+ transaction.content
176
+ end
177
+
178
+ def parent
179
+ transaction.parent
180
+ end
181
+
182
+ def first
183
+ transaction.first
118
184
  end
119
185
  end
120
186
  end
@@ -25,11 +25,34 @@ require_relative 'tag'
25
25
 
26
26
  module Utopia
27
27
  class Content
28
+ class SymbolicHash < Hash
29
+ def [] key
30
+ raise KeyError.new("attribute #{key} is a string, prefer a symbol") if key.is_a? String
31
+ super key.to_sym
32
+ end
33
+
34
+ def []= key, value
35
+ super key.to_sym, value
36
+ end
37
+
38
+ def fetch(key, *args, &block)
39
+ key = key.to_sym
40
+
41
+ super
42
+ end
43
+
44
+ def include? key
45
+ key = key.to_sym
46
+
47
+ super
48
+ end
49
+ end
50
+
28
51
  class Processor
29
- def self.parse_xml(xml_data, delegate)
52
+ def self.parse_markup(markup, delegate)
30
53
  processor = self.new(delegate)
31
54
 
32
- processor.parse(xml_data)
55
+ processor.parse(markup)
33
56
  end
34
57
 
35
58
  class UnbalancedTagError < StandardError
@@ -73,6 +96,10 @@ module Utopia
73
96
  def begin_parse(scanner)
74
97
  @scanner = scanner
75
98
  end
99
+
100
+ def doctype(attributes)
101
+ @delegate.cdata("<!DOCTYPE#{attributes}>")
102
+ end
76
103
 
77
104
  def text(text)
78
105
  @delegate.cdata(text)
@@ -83,12 +110,12 @@ module Utopia
83
110
  end
84
111
 
85
112
  def comment(text)
86
- @delegate.cdata("<!#{text}>")
113
+ @delegate.cdata("<!--#{text}-->")
87
114
  end
88
115
 
89
116
  def begin_tag(tag_name, begin_tag_type)
90
117
  if begin_tag_type == :opened
91
- @stack << [Tag.new(tag_name, {}), @scanner.pos]
118
+ @stack << [Tag.new(tag_name, SymbolicHash.new), @scanner.pos]
92
119
  else
93
120
  current_tag, current_position = @stack.pop
94
121
 
@@ -116,7 +143,7 @@ module Utopia
116
143
  end
117
144
 
118
145
  def attribute(name, value)
119
- @stack.last[0].attributes[name] = value
146
+ @stack.last[0].attributes[name.to_sym] = value
120
147
  end
121
148
 
122
149
  def instruction(content)