shaf 1.5.0 → 2.0.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 +0 -0
- data.tar.gz.sig +0 -0
- data/iana_link_relations.csv.gz +0 -0
- data/lib/shaf.rb +6 -0
- data/lib/shaf/alps/attribute_serializer.rb +41 -0
- data/lib/shaf/alps/json_serializer.rb +50 -0
- data/lib/shaf/alps/relation_serializer.rb +70 -0
- data/lib/shaf/api_doc/link_relations.rb +77 -0
- data/lib/shaf/app.rb +12 -5
- data/lib/shaf/authenticator.rb +56 -0
- data/lib/shaf/authenticator/base.rb +161 -0
- data/lib/shaf/authenticator/basic_auth.rb +25 -0
- data/lib/shaf/authenticator/challenge.rb +32 -0
- data/lib/shaf/authenticator/parameter.rb +31 -0
- data/lib/shaf/authenticator/request.rb +17 -0
- data/lib/shaf/command/console.rb +1 -1
- data/lib/shaf/command/generate.rb +5 -2
- data/lib/shaf/command/new.rb +20 -7
- data/lib/shaf/command/templates/Gemfile.erb +1 -0
- data/{templates/config/settings.yml → lib/shaf/command/templates/config/settings.yml.erb} +1 -5
- data/lib/shaf/errors.rb +11 -0
- data/lib/shaf/extensions.rb +3 -3
- data/lib/shaf/extensions/api_routes.rb +60 -0
- data/lib/shaf/extensions/authorize.rb +11 -9
- data/lib/shaf/extensions/log.rb +1 -1
- data/lib/shaf/extensions/resource_uris.rb +139 -63
- data/lib/shaf/extensions/symbolic_routes.rb +22 -19
- data/lib/shaf/formable.rb +1 -2
- data/lib/shaf/formable/form.rb +1 -1
- data/lib/shaf/generator.rb +2 -0
- data/lib/shaf/generator/base.rb +2 -3
- data/lib/shaf/generator/controller.rb +11 -7
- data/lib/shaf/generator/doc.rb +17 -0
- data/lib/shaf/generator/forms.rb +1 -0
- data/lib/shaf/generator/helper.rb +2 -1
- data/lib/shaf/generator/migration/base.rb +7 -3
- data/lib/shaf/generator/migration/type.rb +4 -26
- data/lib/shaf/generator/migration/types.rb +45 -16
- data/lib/shaf/generator/model.rb +1 -2
- data/lib/shaf/generator/profile.rb +52 -0
- data/lib/shaf/generator/serializer.rb +38 -73
- data/lib/shaf/generator/templates/api/policy.rb.erb +2 -2
- data/lib/shaf/generator/templates/api/profile.rb.erb +16 -0
- data/lib/shaf/generator/templates/api/serializer.rb.erb +2 -2
- data/lib/shaf/generator/templates/spec/integration_spec.rb.erb +1 -2
- data/lib/shaf/generator/templates/spec/serializer_spec.rb.erb +5 -5
- data/lib/shaf/helpers.rb +4 -0
- data/lib/shaf/helpers/authentication.rb +79 -0
- data/lib/shaf/helpers/cache_control.rb +1 -2
- data/lib/shaf/helpers/json_html.rb +58 -18
- data/lib/shaf/helpers/payload.rb +27 -41
- data/lib/shaf/helpers/vary.rb +8 -0
- data/lib/shaf/logger.rb +12 -0
- data/lib/shaf/parser.rb +65 -0
- data/lib/shaf/parser/base.rb +44 -0
- data/lib/shaf/parser/form_data.rb +15 -0
- data/lib/shaf/parser/json.rb +26 -0
- data/lib/shaf/profile.rb +110 -0
- data/lib/shaf/profile/attribute.rb +29 -0
- data/lib/shaf/profile/evaluator.rb +46 -0
- data/lib/shaf/profile/relation.rb +29 -0
- data/lib/shaf/profile/unique_id.rb +58 -0
- data/lib/shaf/profiles.rb +42 -0
- data/lib/shaf/profiles/shaf_basic.rb +20 -0
- data/lib/shaf/profiles/shaf_error.rb +48 -0
- data/lib/shaf/profiles/shaf_form.rb +109 -0
- data/lib/shaf/responder.rb +41 -2
- data/lib/shaf/responder/alps_json.rb +25 -0
- data/lib/shaf/responder/base.rb +20 -17
- data/lib/shaf/responder/hal.rb +62 -7
- data/lib/shaf/responder/html.rb +65 -9
- data/lib/shaf/responder/problem_json.rb +1 -1
- data/lib/shaf/serializer.rb +31 -0
- data/lib/shaf/settings.rb +25 -12
- data/lib/shaf/spec.rb +1 -0
- data/lib/shaf/spec/authenticator.rb +13 -0
- data/lib/shaf/spec/base.rb +1 -1
- data/lib/shaf/spec/http_method_utils.rb +1 -1
- data/lib/shaf/spec/integration_spec.rb +25 -13
- data/lib/shaf/spec/payload_utils.rb +2 -2
- data/lib/shaf/supported_http_methods.rb +15 -0
- data/lib/shaf/tasks/api_doc_task.rb +24 -3
- data/lib/shaf/tasks/routes_task.rb +14 -17
- data/lib/shaf/upgrade/manifest.rb +11 -2
- data/lib/shaf/upgrade/package.rb +78 -49
- data/lib/shaf/upgrade/version.rb +11 -10
- data/lib/shaf/utils.rb +19 -5
- data/lib/shaf/version.rb +3 -1
- data/lib/shaf/yard.rb +34 -0
- data/lib/shaf/yard/attribute_method_handler.rb +19 -0
- data/lib/shaf/yard/attribute_object.rb +30 -0
- data/lib/shaf/yard/base_method_handler.rb +30 -0
- data/lib/shaf/yard/link_method_handler.rb +39 -0
- data/lib/shaf/yard/link_object.rb +60 -0
- data/lib/shaf/yard/nested_attributes.rb +37 -0
- data/lib/shaf/yard/parser.rb +64 -0
- data/lib/shaf/yard/profile_method_handler.rb +51 -0
- data/lib/shaf/yard/profile_object.rb +21 -0
- data/lib/shaf/yard/resource_object.rb +55 -0
- data/lib/shaf/yard/serializer_handler.rb +27 -0
- data/templates/api/controllers/base_controller.rb +0 -10
- data/templates/api/controllers/docs_controller.rb +5 -3
- data/templates/api/controllers/root_controller.rb +7 -1
- data/templates/api/policies/base_policy.rb +2 -0
- data/templates/api/serializers/base_serializer.rb +1 -3
- data/templates/api/serializers/error_serializer.rb +1 -5
- data/templates/api/serializers/form_serializer.rb +1 -5
- data/templates/api/serializers/validation_error_serializer.rb +1 -5
- data/templates/config.ru +1 -1
- data/templates/config/bootstrap.rb +1 -2
- data/templates/config/directories.rb +52 -44
- data/templates/config/helpers.rb +1 -1
- data/templates/config/initializers.rb +52 -8
- data/templates/config/initializers/authentication.rb +18 -0
- data/templates/config/initializers/db_migrations.rb +2 -2
- data/templates/config/initializers/logging.rb +2 -2
- data/templates/frontend/assets/css/main.css +33 -1
- data/templates/frontend/views/headers.erb +20 -0
- data/templates/frontend/views/layout.erb +7 -1
- data/templates/frontend/views/payload.erb +1 -0
- data/templates/spec/spec_helper.rb +2 -0
- data/upgrades/0.5.0.tar.gz +0 -0
- data/upgrades/1.0.4.tar.gz +0 -0
- data/upgrades/1.1.0.tar.gz +0 -0
- data/upgrades/1.6.0.tar.gz +0 -0
- data/upgrades/1.6.1.tar.gz +0 -0
- data/upgrades/2.0.0.tar.gz +0 -0
- data/yard_templates/api_doc/doc_index/html/body.erb +3 -0
- data/yard_templates/api_doc/doc_index/setup.rb +8 -0
- data/yard_templates/api_doc/html/css/api-doc.css +222 -0
- data/yard_templates/api_doc/html/favicon.ico +0 -0
- data/yard_templates/api_doc/html/js/switch_tab.js +17 -0
- data/yard_templates/api_doc/html/setup.rb +59 -0
- data/yard_templates/api_doc/layout/html/footer.erb +3 -0
- data/yard_templates/api_doc/layout/html/header.erb +7 -0
- data/yard_templates/api_doc/layout/html/layout.erb +13 -0
- data/yard_templates/api_doc/layout/setup.rb +24 -0
- data/yard_templates/api_doc/profile/html/attributes.erb +10 -0
- data/yard_templates/api_doc/profile/html/profile.erb +6 -0
- data/yard_templates/api_doc/profile/html/relations.erb +10 -0
- data/yard_templates/api_doc/profile/setup.rb +38 -0
- data/yard_templates/api_doc/profile_attribute/html/attribute.erb +23 -0
- data/yard_templates/api_doc/profile_attribute/setup.rb +21 -0
- data/yard_templates/api_doc/profile_relation/html/relation.erb +37 -0
- data/yard_templates/api_doc/profile_relation/setup.rb +41 -0
- data/yard_templates/api_doc/resource/html/attributes.erb +10 -0
- data/yard_templates/api_doc/resource/html/profile.erb +14 -0
- data/yard_templates/api_doc/resource/html/relations.erb +10 -0
- data/yard_templates/api_doc/resource/html/resource.erb +5 -0
- data/yard_templates/api_doc/resource/setup.rb +56 -0
- data/yard_templates/api_doc/resource_attribute/html/attribute.erb +23 -0
- data/yard_templates/api_doc/resource_attribute/setup.rb +20 -0
- data/yard_templates/api_doc/resource_relation/html/relation.erb +47 -0
- data/yard_templates/api_doc/resource_relation/setup.rb +80 -0
- data/yard_templates/api_doc/setup.rb +31 -0
- data/yard_templates/api_doc/sidebar/html/profile_list.erb +8 -0
- data/yard_templates/api_doc/sidebar/html/search.erb +7 -0
- data/yard_templates/api_doc/sidebar/html/serializer_list.erb +8 -0
- data/yard_templates/api_doc/sidebar/html/sidebar.erb +13 -0
- data/yard_templates/api_doc/sidebar/setup.rb +56 -0
- metadata +140 -30
- metadata.gz.sig +1 -2
- data/lib/shaf/extensions/current_user.rb +0 -48
- data/lib/shaf/responder/hal_serializable.rb +0 -54
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'shaf/errors'
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
module Profiles
|
5
|
+
class ProfileNotFoundError < Errors::NotFoundError
|
6
|
+
def initialize(name)
|
7
|
+
msg = %Q(Profile with name "#{name}" does not exist)
|
8
|
+
super(msg, id: name)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def register(clazz)
|
14
|
+
profiles << clazz
|
15
|
+
end
|
16
|
+
|
17
|
+
def find(name)
|
18
|
+
name = String(name)
|
19
|
+
return if name.empty?
|
20
|
+
|
21
|
+
profiles.find { |profile| profile.match? name }
|
22
|
+
end
|
23
|
+
|
24
|
+
def find!(name)
|
25
|
+
find(name) or raise ProfileNotFoundError, name
|
26
|
+
end
|
27
|
+
|
28
|
+
def profiles
|
29
|
+
@profiles ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
@profiles.clear
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
require 'shaf/profile'
|
40
|
+
require 'shaf/profiles/shaf_form'
|
41
|
+
require 'shaf/profiles/shaf_error'
|
42
|
+
require 'shaf/profiles/shaf_basic'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
module Profiles
|
5
|
+
class ShafBasic < Shaf::Profile
|
6
|
+
name 'shaf-basic'
|
7
|
+
|
8
|
+
rel :delete,
|
9
|
+
http_method: :delete,
|
10
|
+
doc: <<~DOC
|
11
|
+
When a resource contains a link with rel 'delete', this
|
12
|
+
means that the autenticated user (or any user if the
|
13
|
+
current user has not been authenticated), may send a
|
14
|
+
DELETE request to the href of the link.
|
15
|
+
If a DELETE request is sent to this href then the corresponding
|
16
|
+
resource will be deleted.
|
17
|
+
DOC
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
module Profiles
|
5
|
+
class ShafError < Shaf::Profile
|
6
|
+
name 'shaf-error'
|
7
|
+
|
8
|
+
doc 'This profile describes a set of descriptors for generic error messages.'
|
9
|
+
example <<~EXAMPLE
|
10
|
+
{
|
11
|
+
"title": "Invalid entity",
|
12
|
+
"message": "The user could not be saved, due to unfulfilled requirements",
|
13
|
+
"code": "VALIDATION_ERROR",
|
14
|
+
"fields": {
|
15
|
+
"email": ["cannot be empty"]
|
16
|
+
}
|
17
|
+
}
|
18
|
+
EXAMPLE
|
19
|
+
|
20
|
+
example <<~EXAMPLE
|
21
|
+
{
|
22
|
+
"title": "Unpermitted action",
|
23
|
+
"message": "User is not allowed to edit this resource",
|
24
|
+
"code": "FORBIDDEN_ERROR",
|
25
|
+
}
|
26
|
+
EXAMPLE
|
27
|
+
|
28
|
+
attribute :code,
|
29
|
+
type: :string,
|
30
|
+
doc: 'An identifier that describes the type of error.'
|
31
|
+
|
32
|
+
attribute :title,
|
33
|
+
type: :string,
|
34
|
+
doc: 'A short string used for labeling the error.'
|
35
|
+
|
36
|
+
attribute :message,
|
37
|
+
type: :string,
|
38
|
+
doc: 'A description with details about the error.'
|
39
|
+
|
40
|
+
attribute :fields,
|
41
|
+
type: :string,
|
42
|
+
doc: 'An object describing validation errors. ' \
|
43
|
+
'The keys correspond to attributes of a resource. ' \
|
44
|
+
'The values are an array of strings describing validation errors ' \
|
45
|
+
'for the corresponding attribute.'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
module Profiles
|
5
|
+
class ShafForm < Shaf::Profile
|
6
|
+
name 'shaf-form'
|
7
|
+
|
8
|
+
doc 'This profile describes how a set of descriptors should be interpreted to ' \
|
9
|
+
'turn a generic media type into a form (similar to HTML forms). For ' \
|
10
|
+
'example, the HAL media type (application/hal+json) does not specify any ' \
|
11
|
+
'semantics about forms, however we can add semantics about forms to a HAL ' \
|
12
|
+
'document using this profile.'
|
13
|
+
|
14
|
+
example <<~EXAMPLE
|
15
|
+
Given the following form:
|
16
|
+
|
17
|
+
```json
|
18
|
+
"create-form": {
|
19
|
+
"method": "POST",
|
20
|
+
"name": "create-post",
|
21
|
+
"title": "Create Post",
|
22
|
+
"href": "/posts",
|
23
|
+
"type": "application/json",
|
24
|
+
"_links": {
|
25
|
+
"self": {
|
26
|
+
"href": "http://localhost:3000/posts/form"
|
27
|
+
}
|
28
|
+
},
|
29
|
+
"fields": [
|
30
|
+
{
|
31
|
+
"name": "title",
|
32
|
+
"type": "string",
|
33
|
+
},
|
34
|
+
{
|
35
|
+
"name": "message",
|
36
|
+
"type": "string",
|
37
|
+
}
|
38
|
+
]
|
39
|
+
}
|
40
|
+
```
|
41
|
+
|
42
|
+
A client should then present a user interface with the possiblity to fill in two fields accepting string values (if applicable the interface should be titled "Create Post"). The entered values should be mapped to the keys title resp. message. (The client may of course fill in the details itself whenever possible). When the form is to be submitted, the client constructs a json string with the keys and values and makes a POST request to http://localhost:3000/posts/form with the header Content-Type header set to application/json and the json string as body.
|
43
|
+
|
44
|
+
Using Curl, the form above could be submitted using:
|
45
|
+
|
46
|
+
```sh
|
47
|
+
curl -H "Content-Type: application/json" \
|
48
|
+
-X POST \
|
49
|
+
-d '{"title": "hello", "message": "world"}' \
|
50
|
+
localhost:3000/posts
|
51
|
+
```
|
52
|
+
EXAMPLE
|
53
|
+
|
54
|
+
attribute :method,
|
55
|
+
type: :string,
|
56
|
+
doc: 'The HTTP method used for submitting the form'
|
57
|
+
|
58
|
+
attribute :href,
|
59
|
+
type: :string,
|
60
|
+
doc: 'The target uri that the form should be submitted to'
|
61
|
+
|
62
|
+
attribute :name,
|
63
|
+
type: :string,
|
64
|
+
doc: 'A string used to indentify the form.'
|
65
|
+
|
66
|
+
attribute :title,
|
67
|
+
type: :string,
|
68
|
+
doc: 'A string used as title/label when showing the form in a user interface'
|
69
|
+
|
70
|
+
attribute :type,
|
71
|
+
type: :string,
|
72
|
+
doc: 'The media type that must be set in the CONTENT-TYPE header when submiting the form'
|
73
|
+
|
74
|
+
attribute :submit,
|
75
|
+
type: :string,
|
76
|
+
doc: 'A string used as label for the CallToAction (e.g. the submit button text)'
|
77
|
+
|
78
|
+
attribute :fields, doc: 'A list of field descriptors' do
|
79
|
+
|
80
|
+
attribute :name,
|
81
|
+
type: :string,
|
82
|
+
doc: 'A string that should be used as key for the field.'
|
83
|
+
|
84
|
+
attribute :type,
|
85
|
+
type: :string,
|
86
|
+
doc: 'A string specifying the possible values accepted by the field.'
|
87
|
+
|
88
|
+
attribute :title,
|
89
|
+
type: :string,
|
90
|
+
doc: 'A string used to present this field in a UI. If not present name may be used instead.'
|
91
|
+
|
92
|
+
attribute :value,
|
93
|
+
doc: 'A prefilled value. Should be shown in a user interface (unless hidden) and sent back on submission unless changed.'
|
94
|
+
|
95
|
+
attribute :required,
|
96
|
+
type: :boolean,
|
97
|
+
doc: 'A boolean specifying that the field must be given a value before the form is submitted.'
|
98
|
+
|
99
|
+
attribute :hidden,
|
100
|
+
type: :boolean,
|
101
|
+
doc: 'A boolean specifying that the field should not be shown in a user interface.'
|
102
|
+
|
103
|
+
attribute :accepted_values,
|
104
|
+
type: :array,
|
105
|
+
doc: 'A list of av acceptable values. Submitting a value not present in this list SHOULD result in a client error response.'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/shaf/responder.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
require 'set'
|
2
|
+
require 'shaf/errors'
|
2
3
|
|
3
4
|
module Shaf
|
4
5
|
module Responder
|
6
|
+
MEDIA_TYPE_SUFFIX_PATTERN = %r{(.*)/([^+]+)\+(.*)}.freeze
|
7
|
+
|
5
8
|
class << self
|
6
9
|
def register(responder)
|
7
10
|
uninitialized << responder
|
@@ -15,15 +18,23 @@ module Shaf
|
|
15
18
|
end
|
16
19
|
|
17
20
|
def for(request, resource)
|
21
|
+
return Responder::ProblemJson if resource.is_a?(Errors::NotAcceptableError)
|
22
|
+
|
18
23
|
types = supported_responders_for(resource).map(&:mime_type)
|
19
|
-
|
20
|
-
|
24
|
+
types = move_html_to_last(types)
|
25
|
+
|
26
|
+
mime = preferred_type(request, types)
|
27
|
+
responders[mime] or raise Errors::NotAcceptableError
|
21
28
|
end
|
22
29
|
|
23
30
|
def default=(responder)
|
24
31
|
responders.default = responder
|
25
32
|
end
|
26
33
|
|
34
|
+
def default
|
35
|
+
responders.default
|
36
|
+
end
|
37
|
+
|
27
38
|
private
|
28
39
|
|
29
40
|
def supported_responders_for(resource)
|
@@ -41,6 +52,23 @@ module Shaf
|
|
41
52
|
@supported_responders ||= Hash.new { |hash, key| hash[key] = Set.new }
|
42
53
|
end
|
43
54
|
|
55
|
+
def preferred_type(request, types)
|
56
|
+
mime = request.preferred_type(types)
|
57
|
+
return mime if mime
|
58
|
+
|
59
|
+
request.accept.find do |accept|
|
60
|
+
next if accept.match? MEDIA_TYPE_SUFFIX_PATTERN
|
61
|
+
|
62
|
+
types.find do |type|
|
63
|
+
m = type.match MEDIA_TYPE_SUFFIX_PATTERN
|
64
|
+
next unless m
|
65
|
+
|
66
|
+
format = "#{m[1]}/#{m[3]}"
|
67
|
+
return type if accept.to_str == format
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
44
72
|
def uninitialized
|
45
73
|
@uninitialized ||= []
|
46
74
|
end
|
@@ -57,6 +85,16 @@ module Shaf
|
|
57
85
|
init_responders!
|
58
86
|
end
|
59
87
|
end
|
88
|
+
|
89
|
+
# We want to always be able to respond with text/html, but only when
|
90
|
+
# asked for (Accept header) to be able to let other more specific mime
|
91
|
+
# types take precedence we need to move text/html to the end of the
|
92
|
+
# array.
|
93
|
+
def move_html_to_last(types)
|
94
|
+
return types unless types.include? Html.mime_type
|
95
|
+
|
96
|
+
(types - [Html.mime_type]) << Html.mime_type
|
97
|
+
end
|
60
98
|
end
|
61
99
|
end
|
62
100
|
end
|
@@ -65,3 +103,4 @@ require 'shaf/responder/base'
|
|
65
103
|
require 'shaf/responder/hal'
|
66
104
|
require 'shaf/responder/html'
|
67
105
|
require 'shaf/responder/problem_json'
|
106
|
+
require 'shaf/responder/alps_json'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'shaf/alps/json_serializer'
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
module Responder
|
5
|
+
class AlpsJson < Base
|
6
|
+
mime_type :alps_json, 'application/alps+json'
|
7
|
+
|
8
|
+
def self.can_handle?(resource)
|
9
|
+
return false unless resource.is_a? Class
|
10
|
+
resource <= Shaf::Profile
|
11
|
+
end
|
12
|
+
|
13
|
+
def body
|
14
|
+
JSON.generate hash
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def hash
|
20
|
+
ALPS::JsonSerializer.call(resource)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
data/lib/shaf/responder/base.rb
CHANGED
@@ -2,12 +2,14 @@ module Shaf
|
|
2
2
|
module Responder
|
3
3
|
class Response
|
4
4
|
attr_reader :content_type, :body, :serialized_hash, :resource
|
5
|
+
attr_accessor :preload_links
|
5
6
|
|
6
7
|
def initialize(content_type:, body:, serialized_hash: {}, resource: nil)
|
7
8
|
@content_type = content_type
|
8
9
|
@body = body
|
9
10
|
@serialized_hash = serialized_hash
|
10
11
|
@resource = resource
|
12
|
+
@preload_links = []
|
11
13
|
end
|
12
14
|
|
13
15
|
def log_entry
|
@@ -36,11 +38,10 @@ module Shaf
|
|
36
38
|
end
|
37
39
|
|
38
40
|
def call(controller, resource, preload: [], **kwargs)
|
39
|
-
responder = new(controller, resource, **kwargs)
|
41
|
+
responder = new(controller, resource, preload_rels: preload, **kwargs)
|
40
42
|
response = responder.build_response
|
41
43
|
log_response(controller, response)
|
42
|
-
|
43
|
-
write_response(controller, response, preload_links)
|
44
|
+
write_response(controller, response)
|
44
45
|
end
|
45
46
|
|
46
47
|
def can_handle?(_obj)
|
@@ -58,23 +59,14 @@ module Shaf
|
|
58
59
|
controller.log.send(type, msg)
|
59
60
|
end
|
60
61
|
|
61
|
-
def
|
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
|
68
|
-
end
|
69
|
-
|
70
|
-
def write_response(controller, response, preload_links)
|
62
|
+
def write_response(controller, response)
|
71
63
|
controller.content_type(response.content_type)
|
72
|
-
add_preload_links(controller, response
|
64
|
+
add_preload_links(controller, response)
|
73
65
|
controller.body(response.body)
|
74
66
|
end
|
75
67
|
|
76
|
-
def add_preload_links(controller, response
|
77
|
-
preload_links.each do |links|
|
68
|
+
def add_preload_links(controller, response)
|
69
|
+
response.preload_links.each do |links|
|
78
70
|
links.each do |link|
|
79
71
|
next unless link[:href]
|
80
72
|
# Nginx http2_push_preload only processes relative URIs with absolute path
|
@@ -111,9 +103,20 @@ module Shaf
|
|
111
103
|
body: body,
|
112
104
|
serialized_hash: serialized_hash,
|
113
105
|
resource: resource
|
114
|
-
)
|
106
|
+
).tap do |response|
|
107
|
+
response.preload_links = preload_links(response)
|
108
|
+
end
|
115
109
|
end
|
116
110
|
|
111
|
+
def preload_links(response)
|
112
|
+
Array(options[:preload_rels]).map do |rel|
|
113
|
+
links = lookup_rel(rel, response)
|
114
|
+
links = [links].compact unless links.is_a? Array
|
115
|
+
log(controller, PRELOAD_FAILED_MSG % rel) if links.empty?
|
116
|
+
links
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
117
120
|
def lookup_rel(_rel, _response)
|
118
121
|
[]
|
119
122
|
end
|
data/lib/shaf/responder/hal.rb
CHANGED
@@ -1,17 +1,37 @@
|
|
1
|
-
require 'shaf/responder/hal_serializable'
|
2
|
-
|
3
1
|
module Shaf
|
4
2
|
module Responder
|
5
3
|
class Hal < Base
|
6
|
-
include HalSerializable
|
7
|
-
|
8
4
|
use_as_default!
|
9
5
|
mime_type :hal, 'application/hal+json'
|
10
6
|
|
7
|
+
def self.can_handle?(resource)
|
8
|
+
return false if resource.is_a? StandardError
|
9
|
+
|
10
|
+
if resource.is_a? Class
|
11
|
+
return false if resource <= Shaf::Profile
|
12
|
+
end
|
13
|
+
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
11
17
|
def body
|
12
|
-
@body ||=
|
18
|
+
@body ||= generate_json
|
13
19
|
end
|
14
20
|
|
21
|
+
def lookup_rel(rel, response)
|
22
|
+
hal = response.serialized_hash
|
23
|
+
links = hal&.dig(:_links, rel.to_sym)
|
24
|
+
return [] unless links
|
25
|
+
|
26
|
+
links = [links] unless links.is_a? Array
|
27
|
+
links.map do |link|
|
28
|
+
{
|
29
|
+
href: link[:href],
|
30
|
+
as: 'fetch',
|
31
|
+
crossorigin: 'anonymous'
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
15
35
|
private
|
16
36
|
|
17
37
|
def mime_type
|
@@ -20,10 +40,45 @@ module Shaf
|
|
20
40
|
type
|
21
41
|
end
|
22
42
|
|
43
|
+
def collection?
|
44
|
+
!!options.fetch(:collection, false)
|
45
|
+
end
|
46
|
+
|
47
|
+
def serializer
|
48
|
+
@serializer ||= options[:serializer] || HALPresenter.lookup_presenter(resource)
|
49
|
+
end
|
50
|
+
|
51
|
+
def serialized_hash
|
52
|
+
raise Errors::NotAcceptableError unless serializer
|
53
|
+
|
54
|
+
@serialized_hash ||=
|
55
|
+
if collection?
|
56
|
+
serializer.to_collection(resource, current_user: user, as_hash: true, **options)
|
57
|
+
else
|
58
|
+
serializer.to_hal(resource, current_user: user, as_hash: true, **options)
|
59
|
+
end
|
60
|
+
|
61
|
+
# hal_presenter versions before v1.5.0 does not understand the :as_hash
|
62
|
+
# keyword argument and will always return a String from
|
63
|
+
# to_hal/to_collection, thus we need to parse it if its a String.
|
64
|
+
if @serialized_hash.is_a? String
|
65
|
+
@body = @serialized_hash
|
66
|
+
@serialized_hash = JSON.parse(@serialized_hash, symbolize_names: true)
|
67
|
+
end
|
68
|
+
|
69
|
+
@serialized_hash
|
70
|
+
end
|
71
|
+
|
23
72
|
def profile
|
24
|
-
|
73
|
+
@profile ||= options[:profile]
|
74
|
+
return unless @profile || serializer
|
75
|
+
|
76
|
+
@profile ||= serializer.semantic_profile
|
77
|
+
end
|
25
78
|
|
26
|
-
|
79
|
+
def generate_json
|
80
|
+
# FIXME: change to Oj??
|
81
|
+
JSON.generate(serialized_hash)
|
27
82
|
end
|
28
83
|
end
|
29
84
|
end
|