utopia 1.7.1 → 1.8.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -3
  3. data/README.md +142 -11
  4. data/benchmarks/string_vs_symbol.rb +12 -0
  5. data/lib/utopia/command.rb +16 -13
  6. data/lib/utopia/content.rb +1 -5
  7. data/lib/utopia/content/node.rb +9 -4
  8. data/lib/utopia/{extensions/rack.rb → content/response.rb} +33 -30
  9. data/lib/utopia/content/tag.rb +14 -17
  10. data/lib/utopia/content/transaction.rb +19 -17
  11. data/lib/utopia/controller.rb +29 -8
  12. data/lib/utopia/controller/actions.rb +148 -0
  13. data/lib/utopia/controller/base.rb +9 -49
  14. data/lib/utopia/controller/respond.rb +1 -1
  15. data/lib/utopia/controller/rewrite.rb +9 -1
  16. data/lib/utopia/controller/variables.rb +1 -0
  17. data/lib/utopia/localization.rb +4 -1
  18. data/lib/utopia/middleware.rb +0 -2
  19. data/lib/utopia/path.rb +9 -0
  20. data/lib/utopia/path/matcher.rb +0 -1
  21. data/lib/utopia/redirection.rb +3 -2
  22. data/lib/utopia/session.rb +119 -2
  23. data/lib/utopia/session/lazy_hash.rb +1 -3
  24. data/lib/utopia/setup.rb +73 -0
  25. data/lib/utopia/static.rb +9 -2
  26. data/lib/utopia/version.rb +1 -1
  27. data/setup/examples/wiki/controller.rb +41 -0
  28. data/setup/examples/wiki/edit.xnode +15 -0
  29. data/setup/examples/wiki/index.xnode +10 -0
  30. data/setup/examples/wiki/welcome/content.md +3 -0
  31. data/setup/server/config/environment.yaml +1 -0
  32. data/setup/server/git/hooks/post-receive +4 -5
  33. data/setup/site/Gemfile +5 -0
  34. data/setup/site/config.ru +2 -1
  35. data/setup/site/config/environment.rb +5 -17
  36. data/setup/site/pages/_page.xnode +4 -2
  37. data/setup/site/pages/links.yaml +1 -1
  38. data/setup/site/pages/welcome/index.xnode +33 -15
  39. data/setup/site/public/_static/site.css +72 -4
  40. data/setup/site/tasks/utopia.rake +8 -0
  41. data/spec/utopia/{rack_spec.rb → content/response_spec.rb} +12 -19
  42. data/spec/utopia/content_spec.rb +2 -3
  43. data/spec/utopia/controller/{action_spec.rb → actions_spec.rb} +18 -32
  44. data/spec/utopia/controller/middleware_spec.rb +10 -10
  45. data/spec/utopia/controller/middleware_spec/controller/controller.rb +3 -3
  46. data/spec/utopia/controller/middleware_spec/controller/nested/controller.rb +1 -1
  47. data/spec/utopia/controller/middleware_spec/redirect/controller.rb +1 -1
  48. data/spec/utopia/controller/respond_spec.rb +3 -2
  49. data/spec/utopia/controller/respond_spec/api/controller.rb +2 -2
  50. data/spec/utopia/controller/respond_spec/errors/controller.rb +1 -1
  51. data/spec/utopia/controller/rewrite_spec.rb +1 -1
  52. data/spec/utopia/controller/sequence_spec.rb +12 -16
  53. data/spec/utopia/exceptions/handler_spec/controller.rb +2 -2
  54. data/spec/utopia/performance_spec/config.ru +1 -0
  55. data/spec/utopia/session_spec.rb +34 -1
  56. data/spec/utopia/session_spec.ru +3 -3
  57. data/spec/utopia/setup_spec.rb +2 -2
  58. data/utopia.gemspec +2 -2
  59. metadata +18 -12
  60. data/lib/utopia/controller/action.rb +0 -116
  61. data/lib/utopia/session/encrypted_cookie.rb +0 -118
@@ -0,0 +1,10 @@
1
+ <page>
2
+ <heading>#{self[:page_title]}</heading>
3
+
4
+ #{Kramdown::Document.new(self[:content]).to_html}
5
+
6
+ <footer>
7
+ Last Modified: #{File.mtime(self[:page_file]) rescue "N/A"} &mdash;
8
+ <a href="edit">Edit</a>
9
+ </footer>
10
+ </page>
@@ -0,0 +1,3 @@
1
+ Utopia is a Rack framework and build around several key pieces of middleware which provide a MVC architecture for content-centric websites.
2
+
3
+ Utopia [provides a command-line](utopia-command-line/)
@@ -0,0 +1 @@
1
+ RACK_ENV: production
@@ -41,7 +41,7 @@ WHOAMI = `whoami`.chomp!
41
41
 
42
42
  # We should find out if we need to use sudo or not:
43
43
  SUDO = if WHOAMI != DEPLOY_USER
44
- ["sudo", "-u", DEPLOY_USER]
44
+ ['sudo', '-u', DEPLOY_USER]
45
45
  end
46
46
 
47
47
  CommandFailure = Class.new(StandardError)
@@ -72,9 +72,8 @@ Dir.chdir(GIT_WORK_TREE) do
72
72
 
73
73
  if File.exist? 'Rakefile'
74
74
  sudo %W{bundle exec rake deploy}
75
+
76
+ puts "Restarting server..."
77
+ sudo %W{bundle exec rake restart}
75
78
  end
76
-
77
- puts "Restarting server..."
78
- sudo %W{mkdir -p tmp} unless File.exist?('tmp')
79
- sudo %W{touch tmp/restart.txt}
80
79
  end
@@ -16,3 +16,8 @@ group :development do
16
16
  gem "pry"
17
17
  gem "rack-test"
18
18
  end
19
+
20
+ group :production do
21
+ # Used for passenger-config to restart server after deployment:
22
+ gem "passenger"
23
+ end
@@ -32,7 +32,8 @@ use Utopia::Localization,
32
32
  :nonlocalized => ['/_static/', '/_cache/']
33
33
 
34
34
  use Utopia::Controller,
35
- cache_controllers: (RACK_ENV == :production)
35
+ cache_controllers: (RACK_ENV == :production),
36
+ base: Utopia::Controller::Base
36
37
 
37
38
  use Utopia::Static
38
39
 
@@ -1,20 +1,8 @@
1
1
 
2
- # Setup default encoding:
3
- Encoding.default_external = Encoding::UTF_8
4
- Encoding.default_internal = Encoding::UTF_8
2
+ require 'bundler/setup'
3
+ Bundler.setup
5
4
 
6
- # Load environment variables:
7
- environment_path = File.expand_path('environment.yaml', __dir__)
8
- if File.exist? environment_path
9
- require 'yaml'
10
- ENV.update(YAML.load_file(environment_path))
11
- end
5
+ require 'utopia/setup'
6
+ Utopia.setup
12
7
 
13
- # Setup the server environment:
14
- RACK_ENV = ENV.fetch('RACK_ENV', :development).to_sym unless defined?(RACK_ENV)
15
-
16
- # Allow loading library code from lib directory:
17
- $LOAD_PATH << File.expand_path('../lib', __dir__)
18
-
19
- # Load utopia framework:
20
- require 'utopia'
8
+ RACK_ENV = ENV.fetch('RACK_ENV', :development).to_sym unless defined? RACK_ENV
@@ -10,14 +10,16 @@
10
10
  <title>Utopia</title>
11
11
  <?r end ?>
12
12
 
13
+ <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous" />
14
+
13
15
  <link rel="icon" type="image/png" href="/_static/icon.png" />
14
16
  <link rel="stylesheet" href="/_static/site.css" type="text/css" media="screen" />
15
17
  </head>
16
18
 
17
19
  <body class="#{attributes[:class]}">
18
- <div id="header">
20
+ <header>
19
21
  <img src="/_static/utopia.svg" />
20
- </div>
22
+ </header>
21
23
 
22
24
  <div id="page">
23
25
  <content />
@@ -1,2 +1,2 @@
1
1
  errors:
2
- display: false
2
+ display: false
@@ -1,17 +1,35 @@
1
1
  <page class="front">
2
- <heading>Welcome to Utopia</heading>
2
+ <heading>Welcome to Utopia...</heading>
3
3
 
4
- <p><strong>Utopia is designed to simplify web development.</strong> Utopia is a fully featured stack for both content-heavy sites and dynamic web applications.</p>
5
-
6
- <h2>Server Side Tag Based Markup</h2>
7
-
8
- <p>Server side tags reduce the amount of duplicate view code especially for content-heavy sites.</p>
9
-
10
- <h2>Recursive Controller Layer</h2>
11
-
12
- <p>Nested controllers make access control a breeze.</p>
13
-
14
- <h2>Standards Based Localization</h2>
15
-
16
- <p>Automatically sniff the user's desired language and provide content in the correct language.</p>
17
- </page>
4
+ <section class="features">
5
+ <div>
6
+ <i class="fa fa-bolt"></i>
7
+ <h2>Low latency and high throughput</h2>
8
+ <p>Utopia has been carefully tuned for low-latency and algorithmic efficiency. During development, requests reload the entire stack, while in production everything is cached using <a href="http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Map.html">thread-safe data-structures</a>. Serve thousands of requests per second with ease.</p>
9
+ </div>
10
+
11
+ <div>
12
+ <i class="fa fa-cubes"></i>
13
+ <h2>Modular code and structure</h2>
14
+ <p>Utopia provides independently useful <a href="https://github.com/rack/rack">Rack</a> middleware and has been designed with simplicify in mind. Several fully-featured webapps and a ton of commercial websites have guided the development of the Utopia stack. It is capable of handling a diverse range of requirements.</p>
15
+ </div>
16
+
17
+ <div>
18
+ <i class="fa fa-code"></i>
19
+ <h2>Well tested and maintained</h2>
20
+ <p>Utopia comprises a <a href="https://github.com/ioquatix/utopia">core gem</a> and several supporting libraries, the main ones being <a href="https://github.com/ioquatix/trenni">trenni</a> for templates and parsing, and <a href="https://github.com/ioquatix/http-accept">http-accept</a> for HTTP header processing. Together, these gems have over 90% test coverage.</p>
21
+ </div>
22
+
23
+ <div>
24
+ <i class="fa fa-server"></i>
25
+ <h2>Batteries included</h2>
26
+ <p>Utopia includes both a recommended development model and a server deployment model out of the box. This is exposed via the <code>utopia</code> command. We recommend use of <a href="https://www.nginx.com">Nginx</a> and <a href="https://www.phusionpassenger.com">Passenger</a> for server side deployment.</p>
27
+ </div>
28
+
29
+ <div>
30
+ <i class="fa fa-globe"></i>
31
+ <h2>Standards compliant localization</h2>
32
+ <p>Utopia supports the <code>Accept-Language</code> header and transparently selects the correct view to render. Build multi-lingual websites and webapps easily: translate content incrementally as required, or not at all.</p>
33
+ </div>
34
+ </section>
35
+ </page>
@@ -2,11 +2,13 @@
2
2
  html {
3
3
  font-family: "PT Sans", Verdana, Helvetica, Arial, sans-serif;
4
4
  }
5
+
5
6
  @media (min-width: 48em) {
6
7
  html {
7
8
  font-size: 16px;
8
9
  }
9
10
  }
11
+
10
12
  @media (min-width: 58em) {
11
13
  html {
12
14
  font-size: 20px;
@@ -20,7 +22,7 @@ body {
20
22
  background-color: #fafafa;
21
23
  }
22
24
 
23
- #header {
25
+ body > header {
24
26
  margin: 1em 0 1em 0;
25
27
 
26
28
  background-color: white;
@@ -30,7 +32,7 @@ body {
30
32
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
31
33
  }
32
34
 
33
- #header img {
35
+ body > header img {
34
36
  display: block;
35
37
  margin: auto;
36
38
  height: 4em;
@@ -45,7 +47,6 @@ p strong {
45
47
  }
46
48
 
47
49
  h1, h2, h3, h4, h5, h6 {
48
- color: #333;
49
50
  margin: 2em 1em 1em 1em;
50
51
  color: #4E8DD9;
51
52
  }
@@ -62,7 +63,7 @@ a:hover {
62
63
  color: #55c;
63
64
  }
64
65
 
65
- p, dl, h3 {
66
+ p, ul, ol, dl, h3 {
66
67
  margin: 2em;
67
68
  }
68
69
 
@@ -85,3 +86,70 @@ body.front h1 {
85
86
 
86
87
  text-align: center;
87
88
  }
89
+
90
+ footer {
91
+ text-align: right;
92
+ margin: 2rem;
93
+ }
94
+
95
+ section.features {
96
+ display: flex;
97
+ flex-wrap: wrap;
98
+ justify-content: space-around;
99
+
100
+ margin: 1rem;
101
+ }
102
+
103
+ section.features > div {
104
+ box-sizing: border-box;
105
+
106
+ flex-basis: 20rem;
107
+ flex-grow: 1;
108
+
109
+ color: #171e42;
110
+ margin: 1rem;
111
+ padding: 1rem;
112
+
113
+ padding-left: 3rem;
114
+
115
+ position: relative;
116
+ }
117
+
118
+ section.features > div i {
119
+ position: absolute;
120
+ left: 0rem;
121
+
122
+ font-size: 1.5rem;
123
+ text-align: center;
124
+
125
+ width: 3rem;
126
+ color: #fafafa;
127
+ text-shadow: 0px 0px 1px #000;
128
+ }
129
+
130
+ section.features p {
131
+ margin: 0;
132
+ maring-bottom: 1rem;
133
+ font-size: 80%;
134
+ }
135
+
136
+ section.features h2 {
137
+ margin: 0;
138
+ font-size: 1.1rem;
139
+ padding: 0;
140
+ }
141
+
142
+ form fieldset {
143
+ border: 0;
144
+ }
145
+
146
+ form fieldset textarea {
147
+ box-sizing: border-box;
148
+
149
+ width: 100%;
150
+ height: 10rem;
151
+ }
152
+
153
+ form fieldset.footer {
154
+ text-align: right;
155
+ }
@@ -4,6 +4,14 @@ task :deploy do
4
4
  # This task is typiclly run after the site is updated but before the server is restarted.
5
5
  end
6
6
 
7
+ desc 'Restart the application server'
8
+ task :restart do
9
+ # This task is run after the deployment task above.
10
+ if passenger_config = `which passenger-config`.chomp!
11
+ sh(passenger_config, 'restart-app', '--ignore-passenger-not-running', File.dirname(__dir__))
12
+ end
13
+ end
14
+
7
15
  desc 'Set up the environment for running your web application'
8
16
  task :environment do
9
17
  require_relative '../config/environment'
@@ -18,42 +18,35 @@
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 'utopia/extensions/date'
22
- require 'utopia/extensions/rack'
21
+ require 'utopia/content/response'
23
22
 
24
- module Utopia::RackSpec
25
- describe Rack::Response do
23
+ module Utopia::Content::ResponseSpec
24
+ describe Utopia::Content::Response do
26
25
  it "should specify not to cache content" do
27
- response = Rack::Response.new
26
+ subject.cache!(1000)
27
+ subject.do_not_cache!
28
28
 
29
- response.cache!(1000)
30
- response.do_not_cache!
29
+ expect(subject.headers['Cache-Control']).to be == "no-cache, must-revalidate"
31
30
 
32
- expect(response['Cache-Control']).to be == "no-cache, must-revalidate"
33
-
34
- expires_header = Time.parse(response['Expires'])
31
+ expires_header = Time.parse(subject.headers['Expires'])
35
32
  expect(expires_header).to be <= Time.now
36
33
  end
37
34
 
38
35
  it "should specify to cache content" do
39
- response = Rack::Response.new
40
-
41
36
  duration = 120
42
37
  expires = Time.now + 100 # At least this far into the future
43
- response.cache!(duration)
38
+ subject.cache!(duration)
44
39
 
45
- expect(response['Cache-Control']).to be == "public, max-age=120"
40
+ expect(subject.headers['Cache-Control']).to be == "public, max-age=120"
46
41
 
47
- expires_header = Time.parse(response['Expires'])
42
+ expires_header = Time.parse(subject.headers['Expires'])
48
43
  expect(expires_header).to be >= expires
49
44
  end
50
45
 
51
46
  it "should set content type" do
52
- response = Rack::Response.new
53
-
54
- response.content_type! "text/html"
47
+ subject.content_type! "text/html"
55
48
 
56
- expect(response['Content-Type']).to be == "text/html"
49
+ expect(subject.headers['Content-Type']).to be == "text/html"
57
50
  end
58
51
  end
59
52
  end
@@ -80,9 +80,8 @@ module Utopia::ContentSpec
80
80
  node = content.lookup_node(path)
81
81
  expect(node).to be_kind_of Utopia::Content::Node
82
82
 
83
- output = StringIO.new
84
- node.process!({}, output, {})
85
- expect(output.string).to be == '<h1>Hello World</h1>'
83
+ status, headers, body = node.process!({}, {})
84
+ expect(body.join).to be == '<h1>Hello World</h1>'
86
85
  end
87
86
 
88
87
  it "should fetch template and use cache" do
@@ -22,54 +22,40 @@
22
22
 
23
23
  require 'utopia/controller'
24
24
 
25
- module Utopia::Controller::ActionSpec
26
- describe Utopia::Controller::Action do
27
- it "should be a hash key" do
28
- a = Utopia::Controller::Action.new
29
- b = Utopia::Controller::Action.new
30
- c = Utopia::Controller::Action.new {sleep}
31
-
32
- expect(a).to be == b
33
- expect(a.hash).to be == b.hash
34
- expect(a).to be_eql b
35
-
36
- expect(a).to_not be == c
37
- expect(a.hash).to_not be == c.hash
38
- expect(a).to_not be_eql c
25
+ module Utopia::Controller::ActionsSpec
26
+ describe Utopia::Controller::Actions::Action do
27
+ it "can be a hash key" do
28
+ expect(subject).to be == subject
29
+ expect(subject.hash).to be == subject.hash
30
+ expect(subject).to be_eql subject
39
31
  end
40
32
 
41
33
  it "should resolve callbacks" do
42
- actions = Utopia::Controller::Action.new
43
-
44
- specific_action = actions.define(['a', 'b', 'c']) {puts 'specific_action'}
45
- indirect_action = actions.define(['**']) {puts 'indirect_action'}
46
- indirect_named_action = actions.define(['**', 'r']) {puts 'indirect_named_action'}
34
+ specific_action = subject.define(['a', 'b', 'c']) {puts 'specific_action'}
35
+ indirect_action = subject.define(['**']) {puts 'indirect_action'}
36
+ indirect_named_action = subject.define(['**', 'r']) {puts 'indirect_named_action'}
47
37
 
48
38
  expect(specific_action).to_not be == indirect_action
49
39
  expect(indirect_action).to_not be == indirect_named_action
50
40
 
51
- expect(actions.select(['a', 'b', 'c'])).to be == [indirect_action, specific_action]
52
- expect(actions.select(['q'])).to be == [indirect_action]
41
+ expect(subject.matching(['a', 'b', 'c'])).to be == [indirect_action, specific_action]
42
+ expect(subject.matching(['q'])).to be == [indirect_action]
53
43
 
54
- expect(actions.select(['q', 'r'])).to be == [indirect_action, indirect_named_action]
55
- expect(actions.select(['q', 'r', 's'])).to be == [indirect_action]
44
+ expect(subject.matching(['q', 'r'])).to be == [indirect_action, indirect_named_action]
45
+ expect(subject.matching(['q', 'r', 's'])).to be == [indirect_action]
56
46
  end
57
47
 
58
48
  it "should be greedy matching" do
59
- actions = Utopia::Controller::Action.new
49
+ greedy_action = subject.define(['**', 'r']) {puts 'greedy_action'}
60
50
 
61
- greedy_action = actions.define(['**', 'r']) {puts 'greedy_action'}
62
-
63
- expect(actions.select(['g', 'r'])).to be_include greedy_action
64
- expect(actions.select(['r'])).to be_include greedy_action
51
+ expect(subject.matching(['g', 'r'])).to be_include greedy_action
52
+ expect(subject.matching(['r'])).to be_include greedy_action
65
53
  end
66
54
 
67
55
  it "should match patterns" do
68
- actions = Utopia::Controller::Action.new
69
-
70
- variable_action = actions.define(['*', 'summary', '*']) {puts 'variable_action'}
56
+ variable_action = subject.define(['*', 'summary', '*']) {puts 'variable_action'}
71
57
 
72
- expect(actions.select(['10', 'summary', '20'])).to be_include variable_action
58
+ expect(subject.matching(['10', 'summary', '20'])).to be_include variable_action
73
59
  end
74
60
  end
75
61
  end
@@ -37,28 +37,28 @@ module Utopia::Controller::MiddlewareSpec
37
37
  end
38
38
 
39
39
  it "should successfully call the controller method" do
40
- get "/controller/hello-world"
40
+ get "/controller/flat"
41
41
 
42
42
  expect(last_response.status).to be == 200
43
- expect(last_response.body).to be == 'Hello World'
43
+ expect(last_response.body).to be == 'flat'
44
44
  end
45
45
 
46
- it "should successfully call the recursive controller method" do
47
- get "/controller/recursive/hello-world"
46
+ it "should invoke controller method from the top level" do
47
+ get "/controller/hello-world"
48
48
 
49
49
  expect(last_response.status).to be == 200
50
50
  expect(last_response.body).to be == 'Hello World'
51
51
  end
52
-
53
- it "should successfully call the controller method" do
54
- get "/controller/flat"
52
+
53
+ it "should invoke the controller method with a nested path" do
54
+ get "/controller/nested/hello-world"
55
55
 
56
56
  expect(last_response.status).to be == 200
57
- expect(last_response.body).to be == 'flat'
57
+ expect(last_response.body).to be == 'Hello World'
58
58
  end
59
59
 
60
- it "should successfully call the recursive controller method" do
61
- get "/controller/recursive/flat"
60
+ it "shouldn't call the nested controller method" do
61
+ get "/controller/nested/flat"
62
62
 
63
63
  expect(last_response.status).to be == 404
64
64
  end