utopia 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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)