utopia 1.7.1 → 1.8.0

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