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 +4 -4
- checksums.yaml.gz.sig +1 -4
- data.tar.gz.sig +0 -0
- data/lib/shaf/helpers/payload.rb +16 -59
- data/lib/shaf/responder.rb +67 -0
- data/lib/shaf/responder/base.rb +112 -0
- data/lib/shaf/responder/hal.rb +24 -0
- data/lib/shaf/responder/html.rb +16 -0
- data/lib/shaf/responder/problem_json.rb +43 -0
- data/lib/shaf/router.rb +37 -2
- data/lib/shaf/version.rb +1 -1
- data/templates/api/controllers/base_controller.rb +1 -1
- data/templates/api/serializers/documentation_serializer.rb +3 -2
- data/upgrades/1.4.0.tar.gz +0 -0
- metadata +22 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6502c5b061eb5e2420e621d91a12866f4d5c8a2a295c8d2b129df12a71d6668
|
4
|
+
data.tar.gz: 19b4fa57c6b99c69b084b493769dd7291253a9f62e7d0607c5ffdcf8f24f8549
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7c299b50eb8b8b62c7dd1ab4b5ebff9f85f1dc286a858ce7a0925e1a7ef30fd4639303b9293c79971118b7a80c2582f0142f063ef131511ab645138bf0c7b44
|
7
|
+
data.tar.gz: 93b4102fb1b5efd7fe020eab71522ff314a343ee3e67ed19b7a2cdad6b9243188c60b7d56a5ce1c0bb10746045e8f8a2842247880717e8951eae47c622ea806a
|
checksums.yaml.gz.sig
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
data/lib/shaf/helpers/payload.rb
CHANGED
@@ -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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
108
|
-
|
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
|
-
|
97
|
+
respond_with(Errors::ServerError.new(err.message), status: 500)
|
111
98
|
end
|
112
99
|
end
|
113
100
|
|
114
|
-
def
|
115
|
-
|
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
@@ -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[:
|
21
|
-
|
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.
|
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
|
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
|