utopia 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +5 -1
- data/Gemfile +3 -0
- data/README.md +11 -0
- data/benchmarks/hash_vs_openstruct.rb +52 -0
- data/benchmarks/struct_vs_class.rb +89 -0
- data/bin/utopia +4 -5
- data/lib/utopia.rb +1 -0
- data/lib/utopia/content.rb +24 -15
- data/lib/utopia/content/node.rb +69 -3
- data/lib/utopia/content/processor.rb +32 -5
- data/lib/utopia/content/transaction.rb +138 -147
- data/lib/utopia/content_length.rb +50 -0
- data/lib/utopia/controller.rb +4 -0
- data/lib/utopia/controller/variables.rb +1 -1
- data/lib/utopia/http.rb +2 -0
- data/lib/utopia/localization.rb +4 -8
- data/lib/utopia/path.rb +13 -13
- data/lib/utopia/static.rb +25 -14
- data/lib/utopia/tags/environment.rb +1 -1
- data/lib/utopia/tags/override.rb +1 -1
- data/lib/utopia/version.rb +1 -1
- data/setup/server/git/hooks/post-receive +32 -24
- data/setup/site/Gemfile +8 -0
- data/setup/site/Rakefile +29 -6
- data/setup/site/config.ru +8 -8
- data/setup/site/pages/_heading.xnode +1 -1
- data/setup/site/pages/_page.xnode +2 -2
- data/spec/utopia/content_spec.rb +3 -3
- data/spec/utopia/controller/sequence_spec.rb +1 -1
- data/spec/utopia/controller/variables_spec.rb +1 -1
- data/spec/utopia/pages/node/index.xnode +1 -1
- data/spec/utopia/performance_spec.rb +90 -0
- data/spec/utopia/performance_spec/cache/head/readme.txt +1 -0
- data/spec/utopia/performance_spec/cache/meta/readme.txt +1 -0
- data/spec/utopia/performance_spec/config.ru +39 -0
- data/spec/utopia/performance_spec/lib/readme.txt +1 -0
- data/spec/utopia/performance_spec/pages/_heading.xnode +2 -0
- data/spec/utopia/performance_spec/pages/_page.xnode +26 -0
- data/spec/utopia/performance_spec/pages/api/controller.rb +7 -0
- data/spec/utopia/performance_spec/pages/errors/exception.xnode +5 -0
- data/spec/utopia/performance_spec/pages/errors/file-not-found.xnode +5 -0
- data/spec/utopia/performance_spec/pages/links.yaml +2 -0
- data/spec/utopia/performance_spec/pages/welcome/index.xnode +17 -0
- data/spec/utopia/rack_helper.rb +5 -2
- data/spec/utopia/setup_spec.rb +93 -0
- data/utopia.gemspec +1 -1
- metadata +34 -5
- data/lib/utopia/mail_exceptions.rb +0 -136
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 452905bf411c5f82c5fe02c778c50a27b25234cb
|
4
|
+
data.tar.gz: 43df0cc23493c453bddf7d19b58b02e65bb15619
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ab0769846592da4bb8ce571d48cfc7465d3b773b64739781bae3fd4c6900603d162ad0a7e72d49ad049ca011858256a87191d9d13a9027fc703934bccf2224f
|
7
|
+
data.tar.gz: 2988e596315c47ee2026d8fe5f64d075bb3b99e3fe3fdf35e3ab0217716cee83cb9f472f16dface2e2526d4c086b21fe9a7732f1f535126e3ebfc47667094c21
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -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
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")
|
data/lib/utopia.rb
CHANGED
@@ -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'
|
data/lib/utopia/content.rb
CHANGED
@@ -57,22 +57,18 @@ module Utopia
|
|
57
57
|
|
58
58
|
attr :root
|
59
59
|
|
60
|
-
def
|
60
|
+
def fetch_template(path)
|
61
61
|
if @template_cache
|
62
62
|
@template_cache.fetch_or_store(path.to_s) do
|
63
|
-
Trenni::Template.
|
63
|
+
Trenni::Template.load_file(path)
|
64
64
|
end
|
65
65
|
else
|
66
|
-
Trenni::Template.
|
66
|
+
Trenni::Template.load_file(path)
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
70
|
-
#
|
71
|
-
def
|
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.
|
89
|
-
|
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,
|
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,
|
94
|
+
tag_path = File.join(root, components, '_' + name_path)
|
97
95
|
|
98
96
|
if File.exist? tag_path
|
99
|
-
return Node.new(self,
|
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
|
data/lib/utopia/content/node.rb
CHANGED
@@ -107,14 +107,80 @@ module Utopia
|
|
107
107
|
end
|
108
108
|
|
109
109
|
def call(transaction, state)
|
110
|
-
|
110
|
+
template = @controller.fetch_template(@file_path)
|
111
111
|
|
112
|
-
|
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
|
-
|
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.
|
52
|
+
def self.parse_markup(markup, delegate)
|
30
53
|
processor = self.new(delegate)
|
31
54
|
|
32
|
-
processor.parse(
|
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("
|
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,
|
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)
|