shaf 1.4.1 → 1.5.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
  SHA256:
3
- metadata.gz: 2b68c64b495d1413c70562c4deb0796113c93c4e3c4e269899d31a93baaa7574
4
- data.tar.gz: ee75b02c4006994eb0f1ea1da93114687d0a5ee5dd64d1dcf62d5dc647210301
3
+ metadata.gz: 21af40642344c3526a2b34df81fe891ce407ac8ef822e125938d8ecc415fcba0
4
+ data.tar.gz: 8be6b04b3a460979d6494fda33a5a88797b23d6c3266bb51e5505184b70707da
5
5
  SHA512:
6
- metadata.gz: 0f84860a9c7442d473378ecbd2ca61325bf71f848af65f1e742e01888a37ecb52e0c8bca9e8938eb9b70d5d167501616ce46096cdcba1f87e31299aa47184100
7
- data.tar.gz: 7ed67fe1fe15cc5300b658eb3e0cfb3c79325829eb2969308a0f31ef06d26bcb5365b8600f716c1e75b85c2fcabdee4b7e17f5b16c999c8fdf6d0d9cfbc1f597
6
+ metadata.gz: 505ab287d131637d261d3391eeb58517cfe9aa6160d1e51bfb619544299bfea360b90a63ffb1ad0d2c70edaf4da7ca64c84f4de3d624a001c07d2eb0fdb27c10
7
+ data.tar.gz: 8dc0a97ed2b062b8bbb85cbcdce50a624e42ca010c12638a998deb33d9e7d1438d18a04d58ac8cdb5339286f9744a2948f8c63e0f739636b61940c35b0b8f0db
Binary file
data.tar.gz.sig CHANGED
@@ -1,2 +1,3 @@
1
-
2
- )�ϲ�
1
+ )��pK�iІ�����j��p#�nt��g�CY�]��Y�
2
+ �����u*wS1�^�S� 3�r��|�\�2��a�I2����� A��5�$���Ov����=�IG&`.����Ta^Up�� X^F
3
+ �Q��+���vC�+L��yi��*��vӥ���j��~i��b�zQuI���}���l��D�����~+�� ��Z�MI��2�J%8�J9�*H��jlT� 5ʬ���u Ns��GV8��v+R��dO��3�p�,��
@@ -1,3 +1,4 @@
1
+ require 'shaf/extensions/log'
1
2
  require 'shaf/extensions/resource_uris'
2
3
  require 'shaf/extensions/controller_hooks'
3
4
  require 'shaf/extensions/current_user'
@@ -7,6 +8,7 @@ require 'shaf/extensions/symbolic_routes'
7
8
  module Shaf
8
9
  def self.extensions
9
10
  [
11
+ Log,
10
12
  ResourceUris,
11
13
  ControllerHooks,
12
14
  CurrentUser,
@@ -0,0 +1,19 @@
1
+ require 'logger'
2
+
3
+ module Shaf
4
+ module Log
5
+ def self.registered(app)
6
+ app.helpers Helpers
7
+ end
8
+
9
+ def log
10
+ $logger ||= Logger.new('/dev/nul')
11
+ end
12
+ end
13
+
14
+ module Helpers
15
+ def log
16
+ self.class.log
17
+ end
18
+ end
19
+ end
@@ -2,8 +2,11 @@ module Shaf
2
2
  module JsonHtml
3
3
 
4
4
  def json2html(json)
5
- o = JSON.parse(json)
6
- "<pre><code>#{to_html(o)}</code></pre>"
5
+ as_html JSON.parse(json)
6
+ end
7
+
8
+ def as_html(obj)
9
+ "<pre><code>#{to_html(obj)}</code></pre>"
7
10
  end
8
11
 
9
12
  private
@@ -66,7 +66,7 @@ module Shaf
66
66
  @profile = value
67
67
  end
68
68
 
69
- def respond_with_collection(resource, status: 200, serializer: nil, **kwargs)
69
+ def respond_with_collection(resource, status: nil, serializer: nil, preload: [], **kwargs)
70
70
  respond_with(
71
71
  resource,
72
72
  status: status,
@@ -76,7 +76,8 @@ module Shaf
76
76
  )
77
77
  end
78
78
 
79
- def respond_with(resource, status: 200, serializer: nil, collection: false, **kwargs)
79
+ def respond_with(resource, status: nil, serializer: nil, collection: false, preload: [], **kwargs)
80
+ status ||= resource.respond_to?(:http_status) ? resource.http_status : 200
80
81
  status(status)
81
82
 
82
83
  kwargs.merge!(
@@ -86,15 +87,16 @@ module Shaf
86
87
  )
87
88
 
88
89
  log.info "#{request.request_method} #{request.path_info} => #{status}"
89
- payload = Responder.for(request, resource).call(self, resource, **kwargs)
90
+ payload = Responder.for(request, resource).call(self, resource, preload: preload, **kwargs)
90
91
  add_cache_headers(payload, kwargs)
92
+ payload
91
93
  rescue StandardError => err
92
94
  log.error "Failure: #{err.message}\n#{err.backtrace}"
93
95
  if status == 500
94
96
  content_type mime_type(:json)
95
97
  body JSON.generate(failure: err.message)
96
98
  else
97
- respond_with(Errors::ServerError.new(err.message), status: 500)
99
+ respond_with(Errors::ServerError.new(err.message))
98
100
  end
99
101
  end
100
102
 
@@ -1,17 +1,23 @@
1
1
  module Shaf
2
2
  module Responder
3
3
  class Response
4
- attr_reader :content_type, :body, :resource, :serialized
4
+ attr_reader :content_type, :body, :serialized_hash, :resource
5
5
 
6
- def initialize(content_type, body, resource, serialized = nil)
6
+ def initialize(content_type:, body:, serialized_hash: {}, resource: nil)
7
7
  @content_type = content_type
8
8
  @body = body
9
+ @serialized_hash = serialized_hash
9
10
  @resource = resource
10
- @serialized = serialized
11
+ end
12
+
13
+ def log_entry
14
+ "Response (#{resource.class}) payload: #{body}"
11
15
  end
12
16
  end
13
17
 
14
18
  class Base
19
+ PRELOAD_FAILED_MSG = "Failed to preload '%s'. Link could not be extracted from response"
20
+
15
21
  class << self
16
22
  def mime_type(type = nil, value = nil)
17
23
  if type
@@ -29,11 +35,12 @@ module Shaf
29
35
  Responder.default = self
30
36
  end
31
37
 
32
- def call(controller, resource, **kwargs)
38
+ def call(controller, resource, preload: [], **kwargs)
33
39
  responder = new(controller, resource, **kwargs)
34
- response = responder.response
40
+ response = responder.build_response
35
41
  log_response(controller, response)
36
- write_response(controller, response)
42
+ preload_links = preload_links(preload, responder, response, controller)
43
+ write_response(controller, response, preload_links)
37
44
  end
38
45
 
39
46
  def can_handle?(_obj)
@@ -43,17 +50,43 @@ module Shaf
43
50
  private
44
51
 
45
52
  def log_response(controller, response)
53
+ log(controller, response.log_entry)
54
+ end
55
+
56
+ def log(controller, msg, type: :debug)
46
57
  return unless controller.respond_to? :log
58
+ controller.log.send(type, msg)
59
+ end
47
60
 
48
- controller.log.debug(
49
- "Response (#{response.resource.class}) payload: #{response.serialized}"
50
- )
61
+ def preload_links(rels, responder, response, controller = nil)
62
+ Array(rels).map do |rel|
63
+ links = responder.lookup_rel(rel, response)
64
+ links = [links].compact unless links.is_a? Array
65
+ log(controller, PRELOAD_FAILED_MSG % rel) if links.empty?
66
+ links
67
+ end
51
68
  end
52
69
 
53
- def write_response(controller, response)
70
+ def write_response(controller, response, preload_links)
54
71
  controller.content_type(response.content_type)
72
+ add_preload_links(controller, response, preload_links)
55
73
  controller.body(response.body)
56
74
  end
75
+
76
+ def add_preload_links(controller, response, preload_links)
77
+ preload_links.each do |links|
78
+ links.each do |link|
79
+ next unless link[:href]
80
+ # Nginx http2_push_preload only processes relative URIs with absolute path
81
+ href = link[:href].sub(%r{https?://[^/]+}, "")
82
+ type = link.fetch(:as, 'fetch')
83
+ xorigin = link.fetch(:crossorigin, 'anonymous')
84
+ links = (controller.headers['Link'] || "").split(',').map(&:strip)
85
+ links << "<#{href}>; rel=preload; as=#{type}; crossorigin=#{xorigin}"
86
+ controller.headers["Link"] = links.join(', ') unless links.empty?
87
+ end
88
+ end
89
+ end
57
90
  end
58
91
 
59
92
  attr_reader :controller, :resource, :options
@@ -68,12 +101,21 @@ module Shaf
68
101
  raise NotImplementedError, "#{self.class} must implement #body"
69
102
  end
70
103
 
71
- def serialized
72
- @serialize
104
+ def serialized_hash
105
+ {}
73
106
  end
74
107
 
75
- def response
76
- Response.new(mime_type, body, resource, serialized)
108
+ def build_response
109
+ Response.new(
110
+ content_type: mime_type,
111
+ body: body,
112
+ serialized_hash: serialized_hash,
113
+ resource: resource
114
+ )
115
+ end
116
+
117
+ def lookup_rel(_rel, _response)
118
+ []
77
119
  end
78
120
 
79
121
  private
@@ -82,30 +124,11 @@ module Shaf
82
124
  self.class.mime_type
83
125
  end
84
126
 
85
- def collection?
86
- !!options.fetch(:collection, false)
87
- end
88
-
89
127
  def user
90
128
  options.fetch(:current_user) do
91
129
  controller.current_user if controller.respond_to? :current_user
92
130
  end
93
131
  end
94
-
95
- def serializer
96
- @serializer ||= options[:serializer] || HALPresenter.lookup_presenter(resource)
97
- end
98
-
99
- def serialize
100
- return "" unless serializer
101
-
102
- @serialize ||=
103
- if collection?
104
- serializer.to_collection(resource, current_user: user, **options)
105
- else
106
- serializer.to_hal(resource, current_user: user, **options)
107
- end
108
- end
109
132
  end
110
133
  end
111
134
  end
@@ -1,11 +1,15 @@
1
+ require 'shaf/responder/hal_serializable'
2
+
1
3
  module Shaf
2
4
  module Responder
3
5
  class Hal < Base
4
- mime_type :hal, 'application/hal+json'
6
+ include HalSerializable
7
+
5
8
  use_as_default!
9
+ mime_type :hal, 'application/hal+json'
6
10
 
7
11
  def body
8
- serialize
12
+ @body ||= JSON.generate(serialized_hash)
9
13
  end
10
14
 
11
15
  private
@@ -0,0 +1,54 @@
1
+ require 'hal_presenter'
2
+ require 'shaf/errors'
3
+
4
+ module Shaf
5
+ module Responder
6
+ module HalSerializable
7
+ def lookup_rel(rel, response)
8
+ hal = response.serialized_hash
9
+ return [] unless hal
10
+
11
+ links = hal.dig(:_links, rel.to_sym)
12
+ return [] unless links
13
+
14
+ links = [links] unless links.is_a? Array
15
+ links.map do |link|
16
+ {
17
+ href: link[:href],
18
+ as: 'fetch',
19
+ crossorigin: 'anonymous'
20
+ }
21
+ end
22
+ end
23
+
24
+ def collection?
25
+ !!options.fetch(:collection, false)
26
+ end
27
+
28
+ def serializer
29
+ @serializer ||= options[:serializer] || HALPresenter.lookup_presenter(resource)
30
+ end
31
+
32
+ def serialized_hash
33
+ return {} unless serializer
34
+
35
+ @serialized_hash ||=
36
+ if collection?
37
+ serializer.to_collection(resource, current_user: user, as_hash: true, **options)
38
+ else
39
+ serializer.to_hal(resource, current_user: user, as_hash: true, **options)
40
+ end
41
+
42
+ # hal_presenter versions before v1.5.0 does not understand the :as_hash
43
+ # keyword argument and will always return a String from
44
+ # to_hal/to_collection, thus we need to parse it if its a String.
45
+ if @serialized_hash.is_a? String
46
+ @body = @serialized_hash
47
+ @serialized_hash = JSON.parse(@serialized_hash, symbolize_names: true)
48
+ end
49
+
50
+ @serialized_hash
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,14 +1,18 @@
1
+ require 'shaf/responder/hal_serializable'
2
+
1
3
  module Shaf
2
4
  module Responder
3
5
  class Html < Base
6
+ include HalSerializable
7
+
4
8
  mime_type :html
5
9
 
6
10
  def body
7
11
  case resource
8
12
  when Formable::Form
9
- controller.erb(:form, locals: {form: resource, serialized: serialize})
13
+ controller.erb(:form, locals: {form: resource, serialized: serialized_hash})
10
14
  else
11
- controller.erb(:payload, locals: {serialized: serialize})
15
+ controller.erb(:payload, locals: {serialized: serialized_hash})
12
16
  end
13
17
  end
14
18
  end
@@ -1,3 +1,3 @@
1
1
  module Shaf
2
- VERSION = "1.4.1"
2
+ VERSION = "1.5.0"
3
3
  end
@@ -19,19 +19,6 @@ class BaseController < Sinatra::Base
19
19
 
20
20
  Shaf::Router.mount(self, default: true)
21
21
 
22
- def self.inherited(controller)
23
- super
24
- Shaf::Router.mount controller
25
- end
26
-
27
- def self.log
28
- $logger
29
- end
30
-
31
- def log
32
- self.class.log
33
- end
34
-
35
22
  register(*Shaf.extensions)
36
23
  helpers(*Shaf.helpers)
37
24
 
@@ -42,13 +29,6 @@ class BaseController < Sinatra::Base
42
29
  set_vary_header
43
30
  end
44
31
 
45
- def set_vary_header
46
- return unless settings.auth_token_header
47
- return unless [:get, :head].include? request.request_method.downcase.to_sym
48
-
49
- headers('Vary' => Shaf::Settings.auth_token_header)
50
- end
51
-
52
32
  not_found do
53
33
  err = NotFoundError.new "Resource \"#{request.path_info}\" does not exist"
54
34
  respond_with(err, status: err.http_status)
@@ -59,12 +39,22 @@ class BaseController < Sinatra::Base
59
39
  log.error err.message
60
40
  Array(err.backtrace).each(&log.method(:error))
61
41
 
62
- api_error = to_api_error(err)
42
+ respond_with api_error(err)
43
+ end
63
44
 
64
- respond_with(api_error, status: api_error.http_status)
45
+ def self.inherited(controller)
46
+ super
47
+ Shaf::Router.mount controller
48
+ end
49
+
50
+ def set_vary_header
51
+ return unless settings.auth_token_header
52
+ return unless [:get, :head].include? request.request_method.downcase.to_sym
53
+
54
+ headers('Vary' => Shaf::Settings.auth_token_header)
65
55
  end
66
56
 
67
- def to_api_error(err)
57
+ def api_error(err)
68
58
  case err
69
59
  when Shaf::Authorize::PolicyViolationError
70
60
  ForbiddenError.new
@@ -46,7 +46,6 @@ end
46
46
  %i[lib_dir src_dir app_dir].each do |cfg|
47
47
  dir = Shaf::Settings.send cfg
48
48
  next unless Dir.exist? dir
49
- $LOAD_PATH.unshift dir
50
49
 
51
50
  Dir.chdir(dir) do
52
51
  require_ruby_files
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  app_root = File.expand_path('../', __dir__)
4
- Shaf::Settings.app_root = app_root
5
- Shaf::Settings.app_dir = File.expand_path('api', app_root)
6
- Shaf::Settings.src_dir = File.expand_path('src', app_root)
7
- Shaf::Settings.lib_dir = File.expand_path('lib', app_root)
8
- Shaf::Settings.spec_dir = File.expand_path('spec', app_root)
4
+ [
5
+ Shaf::Settings.app_root = app_root,
6
+ Shaf::Settings.app_dir = File.expand_path('api', app_root),
7
+ Shaf::Settings.src_dir = File.expand_path('src', app_root),
8
+ Shaf::Settings.lib_dir = File.expand_path('lib', app_root),
9
+ Shaf::Settings.spec_dir = File.expand_path('spec', app_root)
10
+ ].each do |dir|
11
+ $LOAD_PATH.unshift(dir) if Dir.exist? dir
12
+ end
@@ -10,7 +10,7 @@
10
10
  </section>
11
11
  <section class="section">
12
12
  <div class="hal-payload">
13
- <%= json2html(serialized) %>
13
+ <%= as_html(serialized) %>
14
14
  </div>
15
15
  </section>
16
16
  </div>
@@ -2,7 +2,7 @@
2
2
  <div class="container">
3
3
  <section class="section">
4
4
  <div class="hal-payload">
5
- <%= json2html(serialized) %>
5
+ <%= as_html(serialized) %>
6
6
  </div>
7
7
  </section>
8
8
  </div>
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shaf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sammy Henningsson
@@ -30,7 +30,7 @@ cert_chain:
30
30
  ZMhjYR7sRczGJx+GxGU2EaR0bjRsPVlC4ywtFxoOfRG3WaJcpWGEoAoMJX6Z0bRv
31
31
  M40=
32
32
  -----END CERTIFICATE-----
33
- date: 2019-12-22 00:00:00.000000000 Z
33
+ date: 2020-02-02 00:00:00.000000000 Z
34
34
  dependencies:
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: minitest
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
180
  version: '2.1'
181
+ - !ruby/object:Gem::Dependency
182
+ name: hal_presenter
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '1.5'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '1.5'
181
195
  description: A framework for building hypermedia driven APIs with sinatra and sequel.
182
196
  email: sammy.henningsson@gmail.com
183
197
  executables:
@@ -208,6 +222,7 @@ files:
208
222
  - lib/shaf/extensions/authorize.rb
209
223
  - lib/shaf/extensions/controller_hooks.rb
210
224
  - lib/shaf/extensions/current_user.rb
225
+ - lib/shaf/extensions/log.rb
211
226
  - lib/shaf/extensions/resource_uris.rb
212
227
  - lib/shaf/extensions/symbolic_routes.rb
213
228
  - lib/shaf/formable.rb
@@ -258,6 +273,7 @@ files:
258
273
  - lib/shaf/responder.rb
259
274
  - lib/shaf/responder/base.rb
260
275
  - lib/shaf/responder/hal.rb
276
+ - lib/shaf/responder/hal_serializable.rb
261
277
  - lib/shaf/responder/html.rb
262
278
  - lib/shaf/responder/problem_json.rb
263
279
  - lib/shaf/router.rb
@@ -328,6 +344,7 @@ files:
328
344
  - upgrades/1.2.1.tar.gz
329
345
  - upgrades/1.3.0.tar.gz
330
346
  - upgrades/1.4.0.tar.gz
347
+ - upgrades/1.5.0.tar.gz
331
348
  homepage:
332
349
  licenses:
333
350
  - MIT
metadata.gz.sig CHANGED
@@ -1 +1,2 @@
1
- ra��TNݚ�Fѫ����T$�>+"�>..��S�J�d�tJ0/�Ȝ&N�gޡ"���&T&���.�ph�b���?Y�W;.)����Ns`ҟ�б�43�6��������P��J��8�{��`��e9?��e�⹾GkFk9��S����(��D��1J��l"-��G*�����]\���s4���L �����x����w�~�P5�G��Q����S��`q�x��=C���ln�j�%�R3ߑ[a
1
+ ZN��l�|��>�d�
2
+ ne�H��t̊9��%����D