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 +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
|