shaf 1.3.0 → 1.4.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: 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