shaf 1.3.0 → 1.4.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: 319f7f6a1725c5e3e5d9ab293531dc72cced237cf5967d2beb3ef3f47c5be8e7
4
- data.tar.gz: 4f37d5e9c0424c31c6dda4ec2c5e628920963d01dd61d367681504cd6f50f0e0
3
+ metadata.gz: d6502c5b061eb5e2420e621d91a12866f4d5c8a2a295c8d2b129df12a71d6668
4
+ data.tar.gz: 19b4fa57c6b99c69b084b493769dd7291253a9f62e7d0607c5ffdcf8f24f8549
5
5
  SHA512:
6
- metadata.gz: 3431790d086472d8cbac8ec9e51ed8fb02ddc4a402ec993a5ed7d17ea1eb048e89902693b3c4c3f88a3630c226fddc175dd4c0024136cac4adb43cd4d2869531
7
- data.tar.gz: 004321671f62d6578e35c42ed1a1b3b1345bc9308743710dc5cc645195303af8235e30d4288ec6f8048c3ad7ab04ca6087912d27ea983b5ecc45768b997b6347
6
+ metadata.gz: d7c299b50eb8b8b62c7dd1ab4b5ebff9f85f1dc286a858ce7a0925e1a7ef30fd4639303b9293c79971118b7a80c2582f0142f063ef131511ab645138bf0c7b44
7
+ data.tar.gz: 93b4102fb1b5efd7fe020eab71522ff314a343ee3e67ed19b7a2cdad6b9243188c60b7d56a5ce1c0bb10746045e8f8a2842247880717e8951eae47c622ea806a
checksums.yaml.gz.sig CHANGED
@@ -1,4 +1 @@
1
- x��=����3.�
2
- p?�2,x�)���F# �
3
-
4
- OWO���א�ൠ4T�pDkZ�|ߌJ�;��,�ZgB|i:�8���l�y��Ԣ\u�.Ԛ]%D��,z y���Pr�<���B+���XC�p�,yޝԒ�wM�1�<x�M�KvK�H�VӤ��iC<;e �F��0#�G]3r�;{X�͕)<�Gң���I�
1
+ 8uj^��tyAߘ������u�dHzxJ��� �5�2|��/Ț�!v�IW?Y��h�������~�N������8�>i�
data.tar.gz.sig CHANGED
Binary file
@@ -1,28 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
+ require 'shaf/responder'
4
5
 
5
6
  module Shaf
6
7
  module Payload
7
8
  EXCLUDED_FORM_PARAMS = ['captures', 'splat'].freeze
8
9
 
9
- def supported_response_types(resource)
10
- [
11
- mime_type(:hal),
12
- mime_type(:json),
13
- mime_type(:html)
14
- ]
15
- end
16
-
17
- def preferred_response_type(resource)
18
- supported_types = supported_response_types(resource)
19
- request.preferred_type(supported_types)
20
- end
21
-
22
- def prefer_html?
23
- request.preferred_type.to_s == mime_type(:html)
24
- end
25
-
26
10
  private
27
11
 
28
12
  def payload
@@ -95,58 +79,31 @@ module Shaf
95
79
  def respond_with(resource, status: 200, serializer: nil, collection: false, **kwargs)
96
80
  status(status)
97
81
 
98
- preferred_response = preferred_response_type(resource)
99
- http_cache = kwargs.delete(:http_cache) { Settings.http_cache }
100
-
101
- serializer ||= HALPresenter.lookup_presenter(resource)
102
- serialized = serialize(resource, serializer, collection, **kwargs)
103
- add_cache_headers(serialized) if http_cache
82
+ kwargs.merge!(
83
+ profile: profile,
84
+ serializer: serializer,
85
+ collection: collection
86
+ )
104
87
 
105
88
  log.info "#{request.request_method} #{request.path_info} => #{status}"
106
-
107
- if preferred_response == mime_type(:html)
108
- respond_with_html(resource, serialized)
89
+ payload = Responder.for(request, resource).call(self, resource, **kwargs)
90
+ add_cache_headers(payload, kwargs)
91
+ rescue StandardError => err
92
+ log.error "Failure: #{err.message}\n#{err.backtrace}"
93
+ if status == 500
94
+ content_type mime_type(:json)
95
+ body JSON.generate(failure: err.message)
109
96
  else
110
- respond_with_hal(resource, serialized, serializer)
97
+ respond_with(Errors::ServerError.new(err.message), status: 500)
111
98
  end
112
99
  end
113
100
 
114
- def serialize(resource, serializer, collection, **options)
115
- if collection
116
- serializer.to_collection(resource, current_user: current_user, **options)
117
- else
118
- serializer.to_hal(resource, current_user: current_user, **options)
119
- end
120
- end
121
-
122
- def respond_with_hal(resource, serialized, serializer)
123
- log.debug "Response payload (#{resource.class}): #{serialized}"
124
- content_type :hal, content_type_params(serializer)
125
- body serialized
126
- end
127
-
128
- def respond_with_html(resource, serialized)
129
- log.debug "Responding with html. Output payload (#{resource.class}): #{serialized}"
130
- content_type :html
131
- case resource
132
- when Formable::Form
133
- body erb(:form, locals: {form: resource, serialized: serialized})
134
- else
135
- body erb(:payload, locals: {serialized: serialized})
136
- end
137
- end
138
-
139
- def add_cache_headers(payload)
101
+ def add_cache_headers(payload, kwargs)
102
+ return unless kwargs.delete(:http_cache) { Settings.http_cache }
140
103
  return if payload.nil? || payload.empty?
141
104
 
142
105
  sha1 = Digest::SHA1.hexdigest payload
143
106
  etag sha1, :weak # Weak or Strong??
144
107
  end
145
-
146
- def content_type_params(serializer)
147
- return {profile: profile} if profile
148
-
149
- {profile: serializer.semantic_profile}.compact
150
- end
151
108
  end
152
109
  end
@@ -0,0 +1,67 @@
1
+ require 'set'
2
+
3
+ module Shaf
4
+ module Responder
5
+ class << self
6
+ def register(responder)
7
+ uninitialized << responder
8
+ end
9
+
10
+ def unregister(responder)
11
+ responders.delete_if { |_, r| r == responder }
12
+ supported_responders.each do |_klass, responders|
13
+ responders.delete_if { |r| r == responder }
14
+ end
15
+ end
16
+
17
+ def for(request, resource)
18
+ types = supported_responders_for(resource).map(&:mime_type)
19
+ mime = request.preferred_type(types)
20
+ responders[mime]
21
+ end
22
+
23
+ def default=(responder)
24
+ responders.default = responder
25
+ end
26
+
27
+ private
28
+
29
+ def supported_responders_for(resource)
30
+ klass = resource.is_a?(Class) ? resource: resource.class
31
+ if supported_responders[klass].empty?
32
+ responders.each do |_mime, responder|
33
+ next unless responder.can_handle? resource
34
+ supported_responders[klass] << responder
35
+ end
36
+ end
37
+ supported_responders[klass].to_a
38
+ end
39
+
40
+ def supported_responders
41
+ @supported_responders ||= Hash.new { |hash, key| hash[key] = Set.new }
42
+ end
43
+
44
+ def uninitialized
45
+ @uninitialized ||= []
46
+ end
47
+
48
+ def init_responders!
49
+ while responder = uninitialized.shift
50
+ mime = responder.mime_type
51
+ @responders[mime] = responder
52
+ end
53
+ end
54
+
55
+ def responders
56
+ (@responders ||= {}).tap do
57
+ init_responders!
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ require 'shaf/responder/base'
65
+ require 'shaf/responder/hal'
66
+ require 'shaf/responder/html'
67
+ require 'shaf/responder/problem_json'
@@ -0,0 +1,112 @@
1
+ module Shaf
2
+ module Responder
3
+ class Response
4
+ attr_reader :content_type, :body, :resource, :serialized
5
+
6
+ def initialize(content_type, body, resource, serialized = nil)
7
+ @content_type = content_type
8
+ @body = body
9
+ @resource = resource
10
+ @serialized = serialized
11
+ end
12
+ end
13
+
14
+ class Base
15
+ class << self
16
+ def mime_type(type = nil, value = nil)
17
+ if type
18
+ @mime_type = type
19
+ @mime_type = Sinatra::Base.mime_type(type, value) if type.is_a? Symbol
20
+ Responder.register(self)
21
+ elsif defined? @mime_type
22
+ @mime_type
23
+ else
24
+ raise Error, "Class #{self} must register a mime type"
25
+ end
26
+ end
27
+
28
+ def use_as_default!
29
+ Responder.default = self
30
+ end
31
+
32
+ def call(controller, resource, **kwargs)
33
+ responder = new(controller, resource, **kwargs)
34
+ response = responder.response
35
+ log_response(controller, response)
36
+ write_response(controller, response)
37
+ end
38
+
39
+ def can_handle?(_obj)
40
+ true
41
+ end
42
+
43
+ private
44
+
45
+ def log_response(controller, response)
46
+ return unless controller.respond_to? :log
47
+
48
+ controller.log.debug(
49
+ "Response (#{response.resource.class}) payload: #{response.serialized}"
50
+ )
51
+ end
52
+
53
+ def write_response(controller, response)
54
+ controller.content_type(response.content_type)
55
+ controller.body(response.body)
56
+ end
57
+ end
58
+
59
+ attr_reader :controller, :resource, :options
60
+
61
+ def initialize(controller, resource, **options)
62
+ @controller = controller
63
+ @resource = resource
64
+ @options = options
65
+ end
66
+
67
+ def body
68
+ raise NotImplementedError, "#{self.class} must implement #body"
69
+ end
70
+
71
+ def serialized
72
+ @serialize
73
+ end
74
+
75
+ def response
76
+ Response.new(mime_type, body, resource, serialized)
77
+ end
78
+
79
+ private
80
+
81
+ def mime_type
82
+ self.class.mime_type
83
+ end
84
+
85
+ def collection?
86
+ !!options.fetch(:collection, false)
87
+ end
88
+
89
+ def user
90
+ options.fetch(:current_user) do
91
+ controller.current_user if controller.respond_to? :current_user
92
+ end
93
+ 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
+ end
110
+ end
111
+ end
112
+
@@ -0,0 +1,24 @@
1
+ module Shaf
2
+ module Responder
3
+ class Hal < Base
4
+ mime_type :hal, 'application/hal+json'
5
+ use_as_default!
6
+
7
+ def body
8
+ serialize
9
+ end
10
+
11
+ private
12
+
13
+ def mime_type
14
+ type = super
15
+ type = "#{type};profile=#{profile}" if profile
16
+ type
17
+ end
18
+
19
+ def profile
20
+ @profile ||= options[:profile] || serializer.semantic_profile
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ module Shaf
2
+ module Responder
3
+ class Html < Base
4
+ mime_type :html
5
+
6
+ def body
7
+ case resource
8
+ when Formable::Form
9
+ controller.erb(:form, locals: {form: resource, serialized: serialize})
10
+ else
11
+ controller.erb(:payload, locals: {serialized: serialize})
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ module Shaf
2
+ module Responder
3
+ class ProblemJson < Base
4
+ mime_type :problem_json, 'application/problem+json'
5
+
6
+ def self.can_handle?(resource)
7
+ klass = resource.is_a?(Class) ? resource: resource.class
8
+ klass <= StandardError
9
+ end
10
+
11
+ def body
12
+ JSON.generate(hash)
13
+ end
14
+
15
+ private
16
+
17
+ def hash
18
+ {
19
+ status: controller.status,
20
+ type: code,
21
+ title: title,
22
+ detail: resource.message,
23
+ }
24
+ end
25
+
26
+ def status
27
+ return resource.http_status if resource.respond_to? :http_status
28
+ controller.status
29
+ end
30
+
31
+ def code
32
+ return resource.code if resource.respond_to? :code
33
+ 'about:blank'
34
+ end
35
+
36
+ def title
37
+ return resource.title if resource.respond_to? :title
38
+ resource.class.to_s
39
+ end
40
+ end
41
+ end
42
+ end
43
+
data/lib/shaf/router.rb CHANGED
@@ -5,6 +5,22 @@ require 'set'
5
5
 
6
6
  module Shaf
7
7
  class Router
8
+ class MethodNotAllowedResponder
9
+ attr_reader :supported_methods
10
+
11
+ def initialize(supported_methods)
12
+ @supported_methods = supported_methods
13
+ end
14
+
15
+ def allowed
16
+ supported_methods.join(', ')
17
+ end
18
+
19
+ def call(env)
20
+ [405, {'Allow' => allowed}, '']
21
+ end
22
+ end
23
+
8
24
  class << self
9
25
  def mount(controller, default: false)
10
26
  @default_controller = controller if default
@@ -28,13 +44,14 @@ module Shaf
28
44
  attr_reader :controllers
29
45
 
30
46
  def init_routes
31
- @routes = {}
47
+ @routes = Hash.new do |hash, key|
48
+ hash[key] = Hash.new { |hash, key| hash[key] = Set.new }
49
+ end
32
50
  controllers.each { |controller| init_routes_for(controller) }
33
51
  end
34
52
 
35
53
  def init_routes_for(controller)
36
54
  controller.routes.each do |method, controller_routes|
37
- routes[method] ||= Hash.new { |hash, key| hash[key] = [] }
38
55
  routes[method][controller] += controller_routes.map(&:first)
39
56
  end
40
57
  end
@@ -74,6 +91,11 @@ module Shaf
74
91
  yield ctrlr unless ctrlr == controller
75
92
  end
76
93
 
94
+ supported_methods = supported_methods_for(path)
95
+ if !supported_methods.empty? && !supported_methods.include?(http_method)
96
+ yield MethodNotAllowedResponder.new(supported_methods)
97
+ end
98
+
77
99
  yield default_controller
78
100
  end
79
101
 
@@ -105,6 +127,17 @@ module Shaf
105
127
  end
106
128
  end
107
129
 
130
+ def supported_methods_for(path)
131
+ methods = Set.new
132
+ routes.each do |http_method, controllers|
133
+ controllers.each do |_, patterns|
134
+ next unless patterns.any? { |pattern| pattern.match(path) }
135
+ methods << http_method
136
+ end
137
+ end
138
+ methods.to_a
139
+ end
140
+
108
141
  def cascade?(result)
109
142
  result.dig(1, 'X-Cascade') == 'pass'
110
143
  end
@@ -114,6 +147,8 @@ module Shaf
114
147
  end
115
148
 
116
149
  def add_cache(controller, http_method, path)
150
+ return unless controller
151
+
117
152
  key = cache_key(http_method, path)
118
153
  cache[key] << controller
119
154
  end
data/lib/shaf/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Shaf
2
- VERSION = "1.3.0"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -6,7 +6,6 @@ class BaseController < Sinatra::Base
6
6
  disable :static
7
7
  enable :logging
8
8
  enable :method_override
9
- mime_type :hal, 'application/hal+json'
10
9
  set :views, Shaf::Settings.views_folder
11
10
  set :static, !production?
12
11
  set :public_folder, Shaf::Settings.public_folder
@@ -38,6 +37,7 @@ class BaseController < Sinatra::Base
38
37
 
39
38
  before do
40
39
  log.info "Processing: #{request.request_method} #{request.path_info}"
40
+ log.debug "Headers: #{request_headers}"
41
41
  log.debug "Payload: #{payload || 'empty'}"
42
42
  set_vary_header
43
43
  end
@@ -17,8 +17,9 @@ class DocumentationSerializer < BaseSerializer
17
17
  hash[attr] = resource.attribute(attr)
18
18
  else
19
19
  hash[:attributes] = resource.attributes
20
- hash[:rels] = resource.links
21
- hash[:embeds] = resource.embeds
20
+ hash[:relations] = resource.links
21
+ embeds = resource.embeds
22
+ hash[:embedded_resources] = embeds unless embeds.empty?
22
23
  end
23
24
  end
24
25
  end
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.3.0
4
+ version: 1.4.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-10-24 00:00:00.000000000 Z
33
+ date: 2019-11-10 00:00:00.000000000 Z
34
34
  dependencies:
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: minitest
@@ -136,6 +136,20 @@ dependencies:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: '3.5'
139
+ - !ruby/object:Gem::Dependency
140
+ name: faraday
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.17'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.17'
139
153
  description: A framework for building hypermedia driven APIs with sinatra and sequel.
140
154
  email: sammy.henningsson@gmail.com
141
155
  executables:
@@ -213,6 +227,11 @@ files:
213
227
  - lib/shaf/rake/test.rb
214
228
  - lib/shaf/registrable_factory.rb
215
229
  - lib/shaf/resource_doc.rb
230
+ - lib/shaf/responder.rb
231
+ - lib/shaf/responder/base.rb
232
+ - lib/shaf/responder/hal.rb
233
+ - lib/shaf/responder/html.rb
234
+ - lib/shaf/responder/problem_json.rb
216
235
  - lib/shaf/router.rb
217
236
  - lib/shaf/settings.rb
218
237
  - lib/shaf/spec.rb
@@ -280,6 +299,7 @@ files:
280
299
  - upgrades/1.2.0.tar.gz
281
300
  - upgrades/1.2.1.tar.gz
282
301
  - upgrades/1.3.0.tar.gz
302
+ - upgrades/1.4.0.tar.gz
283
303
  homepage:
284
304
  licenses:
285
305
  - MIT
metadata.gz.sig CHANGED
Binary file