utopia 1.2.4 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 64d5f1e4253c9897c60653875847c8c0b7497f8a
4
- data.tar.gz: 7f5274d1ecaaa468bc18540064eb90264308a5eb
3
+ metadata.gz: 388fe83677fc504ecff20427f5838afdb1987a62
4
+ data.tar.gz: a60e58681a7329cf679ed40e0e95ae9e257553ff
5
5
  SHA512:
6
- metadata.gz: 0cfe6d3f1de41eb4b130c260f8887b3d0999a34adcc2f21d536911c7b0cf45248e9144667ba3e49801dda4a00d3233361b99e4fdf8dfadeaf9bef33a577d8261
7
- data.tar.gz: 143c2122e743a6e0bba403011319482a85275e6c442ca381306e19314ec34e66b9d8f1104010be209aad5ea16b368e88a00cc184003498e8fa0ff3e39d5f5e13
6
+ metadata.gz: 556d111b28b4b31ad303a762fb4bba67bf0789df8ca56baee29539de9d3d600561f970f6542aa455a5c743330eb0c1dde75fd24dd6c12dd89424c6193280019f
7
+ data.tar.gz: fe16120edbe602d50746e735f641139e27eee266cc6f2a4a638b376fa79c321a1a89c6ec62346cefe92dccf594b6a53ad1b4cae7507b1c5a2a41bda2358b1c32
@@ -1,7 +1,6 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  rvm:
4
- - 2.0.0
5
4
  - 2.1.8
6
5
  - 2.2.4
7
6
  - 2.3.0
@@ -10,4 +9,5 @@ rvm:
10
9
  env: COVERAGE=true
11
10
  matrix:
12
11
  allow_failures:
13
- - rvm: "rbx-2"
12
+ - rvm: rbx-2
13
+ - rvm: ruby-head
data/Gemfile CHANGED
@@ -6,6 +6,8 @@ gemspec
6
6
  group :development do
7
7
  gem 'pry'
8
8
  gem 'pry-coolline'
9
+
10
+ gem 'json'
9
11
  end
10
12
 
11
13
  group :test do
@@ -26,6 +26,7 @@ require_relative 'controller/action'
26
26
  require_relative 'controller/base'
27
27
 
28
28
  require_relative 'controller/rewrite'
29
+ require_relative 'controller/respond'
29
30
 
30
31
  require 'concurrent/map'
31
32
 
@@ -21,15 +21,14 @@
21
21
  module Utopia
22
22
  class Controller
23
23
  class Action < Hash
24
- def initialize
25
- @path = nil
24
+ def initialize(options = {}, &block)
26
25
  @options = options
27
- @callback = nil
26
+ @callback = block
28
27
 
29
- super
28
+ super()
30
29
  end
31
30
 
32
- attr_accessor :path, :callback, :options
31
+ attr_accessor :callback, :options
33
32
 
34
33
  def callback?
35
34
  @callback != nil
@@ -40,15 +39,15 @@ module Utopia
40
39
  end
41
40
 
42
41
  def eql? other
43
- super and @callback.eql? other.callback and @options.eql? other.options and @path.eql? other.path
42
+ super and @callback.eql? other.callback and @options.eql? other.options
44
43
  end
45
44
 
46
45
  def hash
47
- [super, callback, options, path].hash
46
+ [super, @callback, @options].hash
48
47
  end
49
48
 
50
49
  def == other
51
- super and @callback == other.callback and @options == other.options and @path == other.path
50
+ super and @callback == other.callback and @options == other.options
52
51
  end
53
52
 
54
53
  protected
@@ -91,22 +90,17 @@ module Utopia
91
90
  def define(path, **options, &callback)
92
91
  current = self
93
92
 
94
- path.reverse.each do |name|
93
+ path.reverse_each do |name|
95
94
  current = (current[name.to_sym] ||= Action.new)
96
95
  end
97
96
 
98
- current.path = path
99
97
  current.options = options
100
98
  current.callback = callback
101
99
 
102
100
  return current
103
101
  end
104
102
 
105
- def arity
106
- @callback ? @callback.arity : 0
107
- end
108
-
109
- def invoke!(controller, *arguments)
103
+ def call(controller, *arguments)
110
104
  controller.instance_exec(*arguments, self, &@callback)
111
105
  end
112
106
 
@@ -89,7 +89,7 @@ module Utopia
89
89
  unless actions.empty?
90
90
  return catch_response do
91
91
  actions.each do |action|
92
- action.invoke!(self, request, path)
92
+ action.call(self, request, path)
93
93
  end
94
94
  end
95
95
  end
@@ -0,0 +1,177 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative '../http'
22
+ require_relative '../path/matcher'
23
+
24
+ module Utopia
25
+ class Controller
26
+ # This controller layer provides a convenient way to respond to different requested content types.
27
+ module Respond
28
+ def self.prepended(base)
29
+ base.extend(ClassMethods)
30
+ end
31
+
32
+ module Converter
33
+ def self.update_response(response, updated_headers)
34
+ status, headers, body = response
35
+
36
+ # Generate a new body:
37
+ body = body.collect{|content| yield content}
38
+
39
+ # Update the headers with the requested content type:
40
+ headers = headers.merge(updated_headers)
41
+
42
+ return [status, headers, body]
43
+ end
44
+
45
+ class Callback < Struct.new(:content_type, :block)
46
+ def headers
47
+ {HTTP::CONTENT_TYPE => self.content_type}
48
+ end
49
+
50
+ def call(context, response, media_range)
51
+ Converter.update_response(response, headers) do |content|
52
+ context.instance_exec(content, media_range, &block)
53
+ end
54
+ end
55
+ end
56
+
57
+ def self.new(*args)
58
+ Callback.new(*args)
59
+ end
60
+
61
+ # To accept incoming requests with content-type JSON (e.g. POST with JSON data), consider using `Rack::PostBodyContentTypeParser`.
62
+ module ToJSON
63
+ APPLICATION_JSON = 'application/json'.freeze
64
+ HEADERS = {HTTP::CONTENT_TYPE => APPLICATION_JSON}.freeze
65
+
66
+ def self.content_type
67
+ APPLICATION_JSON
68
+ end
69
+
70
+ def self.serialize(content, media_range)
71
+ options = {}
72
+
73
+ if version = media_range.parameters['version']
74
+ options[:version] = version.to_s
75
+ end
76
+
77
+ return content.to_json(options)
78
+ end
79
+
80
+ def self.call(context, response, media_range)
81
+ Converter.update_response(response, HEADERS) do |content|
82
+ self.serialize(content, media_range)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ class Responder
89
+ HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
90
+ NOT_ACCEPTABLE_RESPONSE = [406, {}, []].freeze
91
+
92
+ def initialize
93
+ @converters = HTTP::Accept::MediaTypes::Map.new
94
+ @otherwise = nil
95
+ end
96
+
97
+ def freeze
98
+ @converters.freeze
99
+ @otherwise.freeze
100
+
101
+ super
102
+ end
103
+
104
+ # Parse the list of browser preferred content types and return ordered by priority.
105
+ def browser_preferred_media_types(env)
106
+ if accept_content_types = env[HTTP_ACCEPT]
107
+ HTTP::Accept::MediaTypes.parse(accept_content_types)
108
+ else
109
+ return []
110
+ end
111
+ end
112
+
113
+ # Add a converter for the specified content type. Call the block with the response content if the request accepts the specified content_type.
114
+ def with(content_type, &block)
115
+ @converters << Converter::Callback.new(content_type, block)
116
+ end
117
+
118
+ # Add a converter for JSON when requests accept 'application/json'
119
+ def with_json
120
+ @converters << Converter::ToJSON
121
+ end
122
+
123
+ # If the content type could not be matched, invoke the provided block and use it's result as the response.
124
+ def otherwise(&block)
125
+ @otherwise = block
126
+ end
127
+
128
+ # If the content type could not be matched, ignore it and don't use the result of the controller layer.
129
+ def otherwise_passthrough
130
+ @otherwise = proc { nil }
131
+ end
132
+
133
+ def call(context, request, path, response)
134
+ media_types = browser_preferred_media_types(request.env)
135
+
136
+ converter, media_range = @converters.for(media_types)
137
+
138
+ if converter
139
+ converter.call(context, response, media_range)
140
+ else
141
+ not_acceptable_response(context, response)
142
+ end
143
+ end
144
+
145
+ # Generate a not acceptable response which unless customised with `otherwise`, will result in a generic 406 Not Acceptable response.
146
+ def not_acceptable_response(context, response)
147
+ if @otherwise
148
+ context.instance_exec(response, &@otherwise)
149
+ else
150
+ NOT_ACCEPTABLE_RESPONSE
151
+ end
152
+ end
153
+ end
154
+
155
+ module ClassMethods
156
+ def respond
157
+ @responder ||= Responder.new
158
+ end
159
+
160
+ def response_for(context, request, path, response)
161
+ if @responder
162
+ @responder.call(context, request, path, response)
163
+ else
164
+ response
165
+ end
166
+ end
167
+ end
168
+
169
+ # Rewrite the path before processing the request if possible.
170
+ def passthrough(request, path)
171
+ if response = super
172
+ self.class.response_for(self, request, path, response)
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -23,9 +23,6 @@ require_relative '../path/matcher'
23
23
 
24
24
  module Utopia
25
25
  class Controller
26
- class RewriteError < ArgumentError
27
- end
28
-
29
26
  module Rewrite
30
27
  def self.prepended(base)
31
28
  base.extend(ClassMethods)
@@ -101,7 +98,7 @@ module Utopia
101
98
  return path
102
99
  end
103
100
 
104
- def invoke!(context, request, path)
101
+ def call(context, request, path)
105
102
  path.components = apply(context, request, path).components
106
103
  end
107
104
  end
@@ -113,7 +110,7 @@ module Utopia
113
110
 
114
111
  def rewrite_request(controller, request, path)
115
112
  if @rewriter
116
- @rewriter.invoke!(controller, request, path)
113
+ @rewriter.call(controller, request, path)
117
114
  end
118
115
  end
119
116
  end
@@ -20,8 +20,12 @@
20
20
 
21
21
  require 'rack'
22
22
 
23
+ require 'http/accept'
24
+
23
25
  module Utopia
24
26
  module HTTP
27
+ Accept = ::HTTP::Accept
28
+
25
29
  # A list of commonly used HTTP status codes.
26
30
  # For help choosing the right status code, see http://racksburg.com/choosing-an-http-status-code/
27
31
  STATUS_CODES = {
@@ -78,10 +82,11 @@ module Utopia
78
82
  500 => 'Internal Server Error'.freeze,
79
83
  501 => 'Not Implemented'.freeze,
80
84
  503 => 'Service Unavailable'.freeze
81
- }
85
+ }.merge(Rack::Utils::HTTP_STATUS_CODES)
82
86
 
83
87
  CONTENT_TYPE = 'Content-Type'.freeze
84
88
  LOCATION = 'Location'.freeze
89
+ # ACCEPT = 'Accept'.freeze
85
90
 
86
91
  # A small HTTP status wrapper that verifies the status code within a given range.
87
92
  class Status
@@ -68,7 +68,7 @@ module Utopia
68
68
  def initialize(app, **options)
69
69
  @app = app
70
70
 
71
- @all_locales = options[:locales]
71
+ @all_locales = HTTP::Accept::Languages::Locales.new(options[:locales])
72
72
 
73
73
  # Locales here are represented as an array of strings, e.g. ['en', 'ja', 'cn', 'de'].
74
74
  unless @default_locales = options[:default_locales]
@@ -85,8 +85,6 @@ module Utopia
85
85
 
86
86
  @nonlocalized = options.fetch(:nonlocalized, [])
87
87
 
88
- # puts "All:#{@all_locales.inspect} defaults:#{@default_locales.inspect} default:#{default_locale}"
89
-
90
88
  self.freeze
91
89
  end
92
90
 
@@ -142,9 +140,7 @@ module Utopia
142
140
  def request_preferred_locale(env)
143
141
  path = Path[env[Rack::PATH_INFO]]
144
142
 
145
- if @all_locales.include? path.first
146
- request_locale = path.first
147
-
143
+ if request_locale = @all_locales.patterns[path.first]
148
144
  # Remove the localization prefix:
149
145
  path.delete_at(0)
150
146
 
@@ -158,12 +154,11 @@ module Utopia
158
154
  # No user prefered languages:
159
155
  return [] unless accept_languages
160
156
 
161
- languages = accept_languages.split(',').map { |language|
162
- language.split(';q=').tap{|x| x[1] = (x[1] || 1.0).to_f}
163
- }.sort{|a, b| b[1] <=> a[1]}.collect(&:first)
157
+ # Extract the ordered list of languages:
158
+ languages = HTTP::Accept::Languages.parse(accept_languages)
164
159
 
165
- # Returns available languages based on the order of the first argument:
166
- return languages & @all_locales
160
+ # Returns available languages based on the order languages:
161
+ return @all_locales & languages
167
162
  end
168
163
 
169
164
  def nonlocalized?(env)
@@ -61,7 +61,58 @@ module Utopia
61
61
  :media, :text, :archive, :images, :fonts
62
62
  ]
63
63
  }
64
-
64
+
65
+ class MimeTypeLoader
66
+ def initialize(library)
67
+ @extensions = {}
68
+ @library = library
69
+ end
70
+
71
+ attr :extensions
72
+
73
+ def self.extensions_for(types, library = MIME_TYPES)
74
+ loader = self.new(library)
75
+ loader.expand(types)
76
+ return loader.extensions
77
+ end
78
+
79
+ def extract_extensions(mime_types)
80
+ mime_types.select{|mime_type| !mime_type.obsolete?}.each do |mime_type|
81
+ mime_type.extensions.each do |ext|
82
+ @extensions["." + ext] = mime_type.content_type
83
+ end
84
+ end
85
+ end
86
+
87
+ def expand(types)
88
+ types.each do |type|
89
+ current_count = @extensions.size
90
+
91
+ begin
92
+ case type
93
+ when Symbol
94
+ self.expand(MIME_TYPES[type])
95
+ when Array
96
+ @extensions["." + type[0]] = type[1]
97
+ when String
98
+ self.extract_extensions MIME::Types.of(type)
99
+ when Regexp
100
+ self.extract_extensions MIME::Types[type]
101
+ when MIME::Type
102
+ self.extract_extensions.call([type])
103
+ end
104
+ rescue
105
+ LOG.error{"#{self.class.name}: Error while processing #{type.inspect}!"}
106
+ raise $!
107
+ end
108
+
109
+ if @extensions.size == current_count
110
+ LOG.warn{"#{self.class.name}: Could not find any mime type for #{type.inspect}"}
111
+ end
112
+ end
113
+ end
114
+ end
115
+
65
116
  private
66
117
 
67
118
  class LocalFile
@@ -151,59 +202,13 @@ module Utopia
151
202
  end
152
203
  end
153
204
 
154
- def load_mime_types(types)
155
- result = {}
156
-
157
- extract_extensions = lambda do |mime_type|
158
- # LOG.info "Extracting #{mime_type.inspect}"
159
- mime_type.extensions.each{|ext| result["." + ext] = mime_type.content_type}
160
- end
161
-
162
- types.each do |type|
163
- current_count = result.size
164
- # LOG.info "Processing #{type.inspect}"
165
-
166
- begin
167
- case type
168
- when Symbol
169
- result = load_mime_types(MIME_TYPES[type]).merge(result)
170
- when Array
171
- result["." + type[0]] = type[1]
172
- when String
173
- MIME::Types.of(type).select{|mime_type| !mime_type.obsolete?}.each do |mime_type|
174
- extract_extensions.call(mime_type)
175
- end
176
- when Regexp
177
- MIME::Types[type].select{|mime_type| !mime_type.obsolete?}.each do |mime_type|
178
- extract_extensions.call(mime_type)
179
- end
180
- when MIME::Type
181
- extract_extensions.call(type)
182
- end
183
- rescue
184
- LOG.error "#{self.class.name}: Error while processing #{type.inspect}!"
185
- raise $!
186
- end
187
-
188
- if result.size == current_count
189
- LOG.warn "#{self.class.name}: Could not find any mime type for #{type.inspect}"
190
- end
191
- end
192
-
193
- return result
194
- end
195
-
196
205
  public
197
206
 
198
207
  def initialize(app, **options)
199
208
  @app = app
200
209
  @root = (options[:root] || Utopia::default_root).freeze
201
210
 
202
- if options[:types]
203
- @extensions = load_mime_types(options[:types])
204
- else
205
- @extensions = load_mime_types(MIME_TYPES[:default])
206
- end
211
+ @extensions = MimeTypeLoader.extensions_for(options[:types] || MIME_TYPES[:default])
207
212
 
208
213
  @cache_control = (options[:cache_control] || "public, max-age=3600")
209
214
 
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Utopia
22
- VERSION = "1.2.4"
22
+ VERSION = "1.3.0"
23
23
  end
@@ -0,0 +1,4 @@
1
+ {
2
+ "directory" : "public/_static/components"
3
+ }
4
+
@@ -0,0 +1 @@
1
+ ../../../../materials/utopia.svg
@@ -0,0 +1,9 @@
1
+
2
+ prepend Respond
3
+
4
+ respond.with_json
5
+ respond.otherwise_passthrough
6
+
7
+ on 'file-not-found' do
8
+ fail! 404, {message: 'File not found'}
9
+ end
@@ -0,0 +1 @@
1
+ ../pages/_static
@@ -24,6 +24,20 @@ require 'utopia/controller'
24
24
 
25
25
  module Utopia::Controller::ActionSpec
26
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
39
+ end
40
+
27
41
  it "should resolve callbacks" do
28
42
  actions = Utopia::Controller::Action.new
29
43
 
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env rspec
2
+
3
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ require 'rack/test'
24
+ require 'rack/mock'
25
+ require 'json'
26
+
27
+ require 'utopia/content'
28
+ require 'utopia/controller'
29
+
30
+ module Utopia::Controller::RespondSpec
31
+ describe Utopia::Controller do
32
+ class TestController < Utopia::Controller::Base
33
+ prepend Utopia::Controller::Respond
34
+
35
+ respond.with("application/json") do |content|
36
+ JSON::dump(content)
37
+ end
38
+
39
+ respond.with("text/plain") do |content|
40
+ content.inspect
41
+ end
42
+
43
+ on 'fetch' do |request, path|
44
+ success! content: {user_id: 10}
45
+ end
46
+
47
+ def self.uri_path
48
+ Utopia::Path['/']
49
+ end
50
+ end
51
+
52
+ let(:controller) {TestController.new}
53
+
54
+ def mock_request(*args)
55
+ request = Rack::Request.new(Rack::MockRequest.env_for(*args))
56
+ return request, Utopia::Path[request.path_info]
57
+ end
58
+
59
+ it "should serialize response as JSON" do
60
+ request, path = mock_request("/fetch")
61
+ relative_path = path - controller.class.uri_path
62
+
63
+ request.env['HTTP_ACCEPT'] = "application/json"
64
+
65
+ status, headers, body = controller.process!(request, relative_path)
66
+
67
+ expect(status).to be == 200
68
+ expect(headers['Content-Type']).to be == "application/json"
69
+ expect(body.join).to be == '{"user_id":10}'
70
+ end
71
+
72
+ it "should serialize response as text" do
73
+ request, path = mock_request("/fetch")
74
+ relative_path = path - controller.class.uri_path
75
+
76
+ request.env['HTTP_ACCEPT'] = "text/*"
77
+
78
+ status, headers, body = controller.process!(request, relative_path)
79
+
80
+ expect(status).to be == 200
81
+ expect(headers['Content-Type']).to be == "text/plain"
82
+ expect(body.join).to be == '{:user_id=>10}'
83
+ end
84
+ end
85
+
86
+ describe Utopia::Controller do
87
+ include Rack::Test::Methods
88
+
89
+ let(:app) {Rack::Builder.parse_file(File.expand_path('respond_spec.ru', __dir__)).first}
90
+
91
+ it "should get html error page" do
92
+ get '/errors/file-not-found'
93
+
94
+ expect(last_response.status).to be == 200
95
+ expect(last_response.headers['Content-Type']).to be == 'text/html'
96
+ expect(last_response.body).to be_include "<heading>File Not Found</heading>"
97
+ end
98
+
99
+ it "should get json error response" do
100
+ get '/errors/file-not-found', nil, {'HTTP_ACCEPT' => "application/json"}
101
+
102
+ expect(last_response.status).to be == 404
103
+ expect(last_response.headers['Content-Type']).to be == 'application/json'
104
+ expect(last_response.body).to be == '{"message":"File not found"}'
105
+ end
106
+
107
+ it "should get version 1 response" do
108
+ get '/api/fetch', nil, {'HTTP_ACCEPT' => "application/json;version=1"}
109
+
110
+ expect(last_response.status).to be == 200
111
+ expect(last_response.headers['Content-Type']).to be == 'application/json'
112
+ expect(last_response.body).to be == '{"message":"Hello World"}'
113
+ end
114
+
115
+ it "should get version 2 response" do
116
+ get '/api/fetch', nil, {'HTTP_ACCEPT' => "application/json;version=2"}
117
+
118
+ expect(last_response.status).to be == 200
119
+ expect(last_response.headers['Content-Type']).to be == 'application/json'
120
+ expect(last_response.body).to be == '{"message":"Goodbye World"}'
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,6 @@
1
+
2
+ use Utopia::Controller, root: File.expand_path('respond_spec', __dir__)
3
+
4
+ use Utopia::Content, root: File.expand_path('respond_spec', __dir__)
5
+
6
+ run lambda {|env| [404, {}, []]}
@@ -0,0 +1,26 @@
1
+
2
+ prepend Respond
3
+
4
+ respond.with_json
5
+
6
+ class VersionedResponse
7
+ def to_json(options = {})
8
+ JSON::dump(self.as_json(options))
9
+ end
10
+
11
+ # Modelled after http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
12
+ def as_json(options = {})
13
+ if options[:version] == '1'
14
+ {"message" => "Hello World"}
15
+ elsif options[:version] == '2'
16
+ {"message" => "Goodbye World"}
17
+ end
18
+ end
19
+ end
20
+
21
+ # To get different verions of the response, use:
22
+ # Accept: application/json;version=1
23
+ # Accept: application/json;version=2
24
+ on 'fetch' do
25
+ success! content: VersionedResponse.new
26
+ end
@@ -0,0 +1,9 @@
1
+
2
+ prepend Respond
3
+
4
+ respond.with_json
5
+ respond.otherwise_passthrough
6
+
7
+ on 'file-not-found' do
8
+ fail! 404, {message: 'File not found'}
9
+ end
@@ -0,0 +1,7 @@
1
+ <page>
2
+ <?r response.content_type = 'text/html' ?>
3
+
4
+ <heading>File Not Found</heading>
5
+
6
+ <p>The file you requested is unfortunately not available at this time!</p>
7
+ </page>
@@ -30,7 +30,9 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency 'rack', '~> 1.6'
31
31
  spec.add_dependency 'rack-cache', '~> 1.2.0'
32
32
 
33
- spec.add_dependency 'mail', '~> 2.6.1'
33
+ spec.add_dependency 'http-accept', '~> 1.1.3'
34
+
35
+ spec.add_dependency 'mail', '~> 2.6.3'
34
36
 
35
37
  spec.add_dependency 'concurrent-ruby', '~> 1.0.0'
36
38
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: utopia
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.4
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-12 00:00:00.000000000 Z
11
+ date: 2016-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: trenni
@@ -66,20 +66,34 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 1.2.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: http-accept
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.1.3
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.1.3
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: mail
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: 2.6.1
89
+ version: 2.6.3
76
90
  type: :runtime
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: 2.6.1
96
+ version: 2.6.3
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: concurrent-ruby
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -182,6 +196,7 @@ files:
182
196
  - lib/utopia/controller.rb
183
197
  - lib/utopia/controller/action.rb
184
198
  - lib/utopia/controller/base.rb
199
+ - lib/utopia/controller/respond.rb
185
200
  - lib/utopia/controller/rewrite.rb
186
201
  - lib/utopia/controller/variables.rb
187
202
  - lib/utopia/exception_handler.rb
@@ -209,6 +224,7 @@ files:
209
224
  - materials/utopia.svg
210
225
  - setup/.bowerrc
211
226
  - setup/server/git/hooks/post-receive
227
+ - setup/site/.bowerrc
212
228
  - setup/site/Gemfile
213
229
  - setup/site/README.md
214
230
  - setup/site/Rakefile
@@ -222,10 +238,12 @@ files:
222
238
  - setup/site/pages/_static/site.css
223
239
  - setup/site/pages/_static/utopia-background.svg
224
240
  - setup/site/pages/_static/utopia.svg
241
+ - setup/site/pages/errors/controller.rb
225
242
  - setup/site/pages/errors/exception.xnode
226
243
  - setup/site/pages/errors/file-not-found.xnode
227
244
  - setup/site/pages/links.yaml
228
245
  - setup/site/pages/welcome/index.xnode
246
+ - setup/site/public/_static
229
247
  - setup/site/public/readme.txt
230
248
  - setup/site/tmp/readme.txt
231
249
  - spec/utopia/content/link_spec.rb
@@ -265,6 +283,11 @@ files:
265
283
  - spec/utopia/controller/middleware_spec/empty/controller.rb
266
284
  - spec/utopia/controller/middleware_spec/redirect/controller.rb
267
285
  - spec/utopia/controller/middleware_spec/redirect/test/controller.rb
286
+ - spec/utopia/controller/respond_spec.rb
287
+ - spec/utopia/controller/respond_spec.ru
288
+ - spec/utopia/controller/respond_spec/api/controller.rb
289
+ - spec/utopia/controller/respond_spec/errors/controller.rb
290
+ - spec/utopia/controller/respond_spec/errors/file-not-found.xnode
268
291
  - spec/utopia/controller/rewrite_spec.rb
269
292
  - spec/utopia/controller/sequence_spec.rb
270
293
  - spec/utopia/exception_handler_spec.rb
@@ -314,7 +337,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
314
337
  version: '0'
315
338
  requirements: []
316
339
  rubyforge_project:
317
- rubygems_version: 2.4.6
340
+ rubygems_version: 2.5.2
318
341
  signing_key:
319
342
  specification_version: 4
320
343
  summary: Utopia is a framework for building dynamic content-driven websites.
@@ -356,6 +379,11 @@ test_files:
356
379
  - spec/utopia/controller/middleware_spec/empty/controller.rb
357
380
  - spec/utopia/controller/middleware_spec/redirect/controller.rb
358
381
  - spec/utopia/controller/middleware_spec/redirect/test/controller.rb
382
+ - spec/utopia/controller/respond_spec.rb
383
+ - spec/utopia/controller/respond_spec.ru
384
+ - spec/utopia/controller/respond_spec/api/controller.rb
385
+ - spec/utopia/controller/respond_spec/errors/controller.rb
386
+ - spec/utopia/controller/respond_spec/errors/file-not-found.xnode
359
387
  - spec/utopia/controller/rewrite_spec.rb
360
388
  - spec/utopia/controller/sequence_spec.rb
361
389
  - spec/utopia/exception_handler_spec.rb
@@ -1 +0,0 @@
1
- <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 420 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><rect x="0" y="0" width="420" height="56" style="fill:#f79433;"/><rect x="0" y="56" width="420" height="24" style="fill:#4e8dd8;"/><g><path d="M75.145,70.819c2.37,-3.097 4.173,-6.921 5.111,-11.365c0.91,-4.318 1.498,-9.261 1.498,-14.692l0,-44.762l-62.754,0l0,44.762c0,2.628 0.244,5.333 0.407,8.035c0.168,2.782 0.674,5.515 1.345,8.118c0.68,2.644 1.739,5.173 3.067,7.517c1.363,2.405 3.263,4.526 5.609,6.303c2.319,1.755 5.245,3.163 8.677,4.172c1.617,0.478 3.416,1.093 5.354,1.093l13.856,0c3.071,0 5.797,-1.058 8.131,-2.001c4.042,-1.631 7.305,-4.049 9.699,-7.18Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M151.481,18.701l0,-18.701l-62.754,-0.022l0,18.723l22.246,0l0.02,61.299l17.93,0l-0.02,-61.299l22.578,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M229.926,39.999c0,-22.051 -16.979,-39.992 -37.852,-39.992c-20.872,0 -37.851,17.942 -37.851,39.992c0,22.054 16.979,39.994 37.851,39.994c20.873,0 37.852,-17.94 37.852,-39.994Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M269.238,50.909c9.717,0 17.181,-2.066 22.183,-6.395c5.087,-4.399 7.667,-10.942 7.667,-19.575c0,-3.257 -0.393,-5.962 -1.167,-8.476c-0.778,-2.528 -1.883,-4.934 -3.281,-6.814c-1.401,-1.882 -3.098,-3.458 -5.045,-4.703c-1.895,-1.21 -4.003,-2.198 -6.264,-2.943c-2.239,-0.737 -4.64,-1.263 -7.139,-1.56c-2.464,-0.292 -5.016,-0.443 -7.587,-0.443l-29.468,0l0,80l17.93,0l0,-29.091l12.171,0Z" style="fill:#fff;fill-rule:nonzero;"/><rect x="304.879" y="0" width="17.93" height="80" style="fill:#fff;"/><path d="M362.589,0l-29.477,80l75.888,0l-31.247,-80l-15.164,0Z" style="fill:#fff;fill-rule:nonzero;"/></g></g></svg>