shaf 1.4.1 → 1.5.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
  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