utopia 1.2.4 → 1.3.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.
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>