shaf 1.6.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/iana_link_relations.csv.gz +0 -0
  4. data/lib/shaf/alps/attribute_serializer.rb +41 -0
  5. data/lib/shaf/alps/json_serializer.rb +50 -0
  6. data/lib/shaf/alps/relation_serializer.rb +70 -0
  7. data/lib/shaf/app.rb +32 -7
  8. data/lib/shaf/authenticator/base.rb +161 -0
  9. data/lib/shaf/authenticator/basic_auth.rb +25 -0
  10. data/lib/shaf/authenticator/challenge.rb +32 -0
  11. data/lib/shaf/authenticator/parameter.rb +31 -0
  12. data/lib/shaf/authenticator/request.rb +17 -0
  13. data/lib/shaf/authenticator.rb +56 -0
  14. data/lib/shaf/command/base.rb +4 -0
  15. data/lib/shaf/command/console.rb +1 -1
  16. data/lib/shaf/command/generate.rb +5 -2
  17. data/lib/shaf/command/new.rb +24 -7
  18. data/lib/shaf/command/server.rb +5 -1
  19. data/lib/shaf/command/templates/Gemfile.erb +1 -0
  20. data/{templates/config/settings.yml → lib/shaf/command/templates/config/settings.yml.erb} +9 -12
  21. data/lib/shaf/errors.rb +11 -0
  22. data/lib/shaf/extensions/api_routes.rb +60 -0
  23. data/lib/shaf/extensions/authorize.rb +11 -9
  24. data/lib/shaf/extensions/log.rb +1 -1
  25. data/lib/shaf/extensions/resource_uris.rb +95 -137
  26. data/lib/shaf/extensions/symbolic_routes.rb +9 -19
  27. data/lib/shaf/extensions.rb +3 -3
  28. data/lib/shaf/formable/builder.rb +58 -19
  29. data/lib/shaf/formable/form.rb +14 -11
  30. data/lib/shaf/formable.rb +10 -24
  31. data/lib/shaf/generator/base.rb +84 -3
  32. data/lib/shaf/generator/controller.rb +30 -42
  33. data/lib/shaf/generator/doc.rb +17 -0
  34. data/lib/shaf/generator/forms.rb +11 -14
  35. data/lib/shaf/generator/helper.rb +2 -1
  36. data/lib/shaf/generator/migration/add_column.rb +0 -4
  37. data/lib/shaf/generator/migration/add_index.rb +0 -4
  38. data/lib/shaf/generator/migration/base.rb +15 -3
  39. data/lib/shaf/generator/migration/create_table.rb +0 -4
  40. data/lib/shaf/generator/migration/drop_column.rb +0 -4
  41. data/lib/shaf/generator/migration/rename_column.rb +0 -4
  42. data/lib/shaf/generator/migration/type.rb +4 -26
  43. data/lib/shaf/generator/migration/types.rb +45 -16
  44. data/lib/shaf/generator/model.rb +29 -15
  45. data/lib/shaf/generator/policy.rb +8 -14
  46. data/lib/shaf/generator/profile.rb +47 -0
  47. data/lib/shaf/generator/scaffold.rb +6 -9
  48. data/lib/shaf/generator/serializer.rb +64 -98
  49. data/lib/shaf/generator/templates/api/controller.rb.erb +13 -13
  50. data/lib/shaf/generator/templates/api/forms.rb.erb +2 -2
  51. data/lib/shaf/generator/templates/api/model.rb.erb +1 -1
  52. data/lib/shaf/generator/templates/api/policy.rb.erb +2 -2
  53. data/lib/shaf/generator/templates/api/profile.rb.erb +16 -0
  54. data/lib/shaf/generator/templates/api/serializer.rb.erb +3 -3
  55. data/lib/shaf/generator/templates/spec/integration_spec.rb.erb +15 -16
  56. data/lib/shaf/generator/templates/spec/serializer_spec.rb.erb +5 -5
  57. data/lib/shaf/generator.rb +2 -0
  58. data/lib/shaf/helpers/authentication.rb +79 -0
  59. data/lib/shaf/helpers/paginate.rb +1 -1
  60. data/lib/shaf/helpers/payload.rb +14 -34
  61. data/lib/shaf/helpers/vary.rb +8 -0
  62. data/lib/shaf/helpers.rb +4 -0
  63. data/lib/shaf/logger.rb +12 -0
  64. data/lib/shaf/parser/base.rb +44 -0
  65. data/lib/shaf/parser/form_data.rb +15 -0
  66. data/lib/shaf/parser/json.rb +26 -0
  67. data/lib/shaf/parser.rb +65 -0
  68. data/lib/shaf/profile/attribute.rb +29 -0
  69. data/lib/shaf/profile/evaluator.rb +46 -0
  70. data/lib/shaf/profile/relation.rb +29 -0
  71. data/lib/shaf/profile/unique_id.rb +58 -0
  72. data/lib/shaf/profile.rb +115 -0
  73. data/lib/shaf/profiles/shaf_basic.rb +20 -0
  74. data/lib/shaf/profiles/shaf_error.rb +49 -0
  75. data/lib/shaf/profiles/shaf_form.rb +110 -0
  76. data/lib/shaf/profiles.rb +46 -0
  77. data/lib/shaf/registrable_factory.rb +62 -32
  78. data/lib/shaf/responder/alps_json.rb +25 -0
  79. data/lib/shaf/responder/base.rb +20 -17
  80. data/lib/shaf/responder/hal.rb +66 -5
  81. data/lib/shaf/responder/html.rb +43 -16
  82. data/lib/shaf/responder/problem_json.rb +2 -2
  83. data/lib/shaf/responder.rb +41 -2
  84. data/lib/shaf/router.rb +65 -12
  85. data/lib/shaf/serializer.rb +35 -0
  86. data/lib/shaf/settings.rb +25 -12
  87. data/lib/shaf/spec/authenticator.rb +13 -0
  88. data/lib/shaf/spec/base.rb +1 -1
  89. data/lib/shaf/spec/http_method_utils.rb +1 -1
  90. data/lib/shaf/spec/integration_spec.rb +26 -14
  91. data/lib/shaf/spec/payload_utils.rb +2 -2
  92. data/lib/shaf/spec.rb +1 -0
  93. data/lib/shaf/supported_http_methods.rb +15 -0
  94. data/lib/shaf/tasks/routes_task.rb +14 -17
  95. data/lib/shaf/tasks.rb +0 -1
  96. data/lib/shaf/upgrade/manifest.rb +11 -2
  97. data/lib/shaf/upgrade/package.rb +73 -45
  98. data/lib/shaf/upgrade/version.rb +11 -10
  99. data/lib/shaf/utils.rb +19 -5
  100. data/lib/shaf/version.rb +3 -1
  101. data/lib/shaf/yard/attribute_method_handler.rb +19 -0
  102. data/lib/shaf/yard/attribute_object.rb +30 -0
  103. data/lib/shaf/yard/base_method_handler.rb +30 -0
  104. data/lib/shaf/yard/link_method_handler.rb +39 -0
  105. data/lib/shaf/yard/link_object.rb +60 -0
  106. data/lib/shaf/yard/nested_attributes.rb +37 -0
  107. data/lib/shaf/yard/parser.rb +64 -0
  108. data/lib/shaf/yard/profile_method_handler.rb +51 -0
  109. data/lib/shaf/yard/profile_object.rb +21 -0
  110. data/lib/shaf/yard/resource_object.rb +55 -0
  111. data/lib/shaf/yard/serializer_handler.rb +27 -0
  112. data/lib/shaf/yard.rb +34 -0
  113. data/lib/shaf.rb +6 -0
  114. data/templates/Rakefile +0 -6
  115. data/templates/api/controllers/base_controller.rb +0 -12
  116. data/templates/api/controllers/docs_controller.rb +5 -3
  117. data/templates/api/controllers/root_controller.rb +7 -1
  118. data/templates/api/policies/base_policy.rb +2 -0
  119. data/templates/api/serializers/base_serializer.rb +1 -3
  120. data/templates/api/serializers/error_serializer.rb +1 -5
  121. data/templates/api/serializers/form_serializer.rb +1 -5
  122. data/templates/api/serializers/root_serializer.rb +0 -11
  123. data/templates/api/serializers/validation_error_serializer.rb +1 -5
  124. data/templates/config/bootstrap.rb +1 -2
  125. data/templates/config/directories.rb +52 -44
  126. data/templates/config/helpers.rb +1 -1
  127. data/templates/config/initializers/authentication.rb +18 -0
  128. data/templates/config/initializers/db_migrations.rb +2 -2
  129. data/templates/config/initializers/logging.rb +2 -2
  130. data/templates/config/initializers/middleware.rb +6 -0
  131. data/templates/config/initializers.rb +52 -8
  132. data/templates/config.ru +1 -1
  133. data/templates/spec/spec_helper.rb +5 -0
  134. data/upgrades/0.5.0.tar.gz +0 -0
  135. data/upgrades/1.0.4.tar.gz +0 -0
  136. data/upgrades/1.1.0.tar.gz +0 -0
  137. data/upgrades/1.6.1.tar.gz +0 -0
  138. data/upgrades/2.0.0.tar.gz +0 -0
  139. data/upgrades/3.0.0.tar.gz +0 -0
  140. data/yard_templates/api_doc/doc_index/html/body.erb +3 -0
  141. data/yard_templates/api_doc/doc_index/setup.rb +8 -0
  142. data/yard_templates/api_doc/html/css/api-doc.css +222 -0
  143. data/yard_templates/api_doc/html/favicon.ico +0 -0
  144. data/yard_templates/api_doc/html/js/switch_tab.js +17 -0
  145. data/yard_templates/api_doc/html/setup.rb +59 -0
  146. data/yard_templates/api_doc/layout/html/footer.erb +3 -0
  147. data/yard_templates/api_doc/layout/html/header.erb +7 -0
  148. data/yard_templates/api_doc/layout/html/layout.erb +13 -0
  149. data/yard_templates/api_doc/layout/setup.rb +24 -0
  150. data/yard_templates/api_doc/profile/html/attributes.erb +10 -0
  151. data/yard_templates/api_doc/profile/html/profile.erb +6 -0
  152. data/yard_templates/api_doc/profile/html/relations.erb +10 -0
  153. data/yard_templates/api_doc/profile/setup.rb +38 -0
  154. data/yard_templates/api_doc/profile_attribute/html/attribute.erb +23 -0
  155. data/yard_templates/api_doc/profile_attribute/setup.rb +21 -0
  156. data/yard_templates/api_doc/profile_relation/html/relation.erb +37 -0
  157. data/yard_templates/api_doc/profile_relation/setup.rb +41 -0
  158. data/yard_templates/api_doc/resource/html/attributes.erb +10 -0
  159. data/yard_templates/api_doc/resource/html/profile.erb +14 -0
  160. data/yard_templates/api_doc/resource/html/relations.erb +10 -0
  161. data/yard_templates/api_doc/resource/html/resource.erb +5 -0
  162. data/yard_templates/api_doc/resource/setup.rb +56 -0
  163. data/yard_templates/api_doc/resource_attribute/html/attribute.erb +23 -0
  164. data/yard_templates/api_doc/resource_attribute/setup.rb +20 -0
  165. data/yard_templates/api_doc/resource_relation/html/relation.erb +47 -0
  166. data/yard_templates/api_doc/resource_relation/setup.rb +80 -0
  167. data/yard_templates/api_doc/setup.rb +31 -0
  168. data/yard_templates/api_doc/sidebar/html/profile_list.erb +8 -0
  169. data/yard_templates/api_doc/sidebar/html/search.erb +7 -0
  170. data/yard_templates/api_doc/sidebar/html/serializer_list.erb +8 -0
  171. data/yard_templates/api_doc/sidebar/html/sidebar.erb +13 -0
  172. data/yard_templates/api_doc/sidebar/setup.rb +56 -0
  173. data.tar.gz.sig +2 -2
  174. metadata +143 -38
  175. metadata.gz.sig +0 -0
  176. data/lib/shaf/api_doc/comment.rb +0 -27
  177. data/lib/shaf/api_doc/document.rb +0 -137
  178. data/lib/shaf/extensions/current_user.rb +0 -48
  179. data/lib/shaf/middleware.rb +0 -1
  180. data/lib/shaf/responder/hal_serializable.rb +0 -64
  181. data/lib/shaf/tasks/api_doc_task.rb +0 -125
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shaf
4
+ class Profile
5
+ module UniqueId
6
+ def id
7
+ return @id if defined? @id
8
+ @id = __find_unique_id
9
+ end
10
+
11
+ def __pending_id?
12
+ @__pending_id ||= false
13
+ end
14
+
15
+ private
16
+
17
+ def __find_unique_id
18
+ @__pending_id = true
19
+
20
+ return name.to_s unless __id_collision? name.to_s
21
+
22
+ id = [parent.name, name].join('_')
23
+ return id unless __id_collision? id
24
+
25
+ id = "#{id}0"
26
+
27
+ loop do
28
+ id = id.next
29
+ break id unless __id_collision? id
30
+ end
31
+ ensure
32
+ @__pending_id = false
33
+ end
34
+
35
+ def __id_collision? id
36
+ descriptor = self
37
+
38
+ loop do
39
+ break false unless descriptor.respond_to?(:parent) && descriptor.parent
40
+ descriptor = descriptor.parent
41
+
42
+ __parent_descriptors(descriptor).each do |desc|
43
+ next if desc == self
44
+ next if desc.__pending_id?
45
+ return true if desc.id == id
46
+ end
47
+ end
48
+ end
49
+
50
+ def __parent_descriptors(parent)
51
+ descriptors = []
52
+ descriptors += parent.attributes if parent.respond_to? :attributes
53
+ descriptors += parent.relations if parent.respond_to? :relations
54
+ descriptors
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shaf/profile/evaluator'
4
+ require 'shaf/extensions/resource_uris'
5
+
6
+ module Shaf
7
+ class Profile
8
+ module NormalizeName
9
+ private def normalize(name)
10
+ name.to_s.downcase.tr('-', '_')
11
+ end
12
+ end
13
+
14
+ extend NormalizeName
15
+ include NormalizeName
16
+ include Shaf::UriHelper
17
+
18
+ class << self
19
+ def inherited(child)
20
+ Profiles.register child
21
+ end
22
+
23
+ def name(str = nil)
24
+ @name = str if str
25
+ @name if defined? @name
26
+ end
27
+
28
+ def doc(str = nil)
29
+ @doc = str if str
30
+ @doc if defined? @doc
31
+ end
32
+
33
+ def urn(value = nil)
34
+ @urn = value if value
35
+ @urn if defined? @urn
36
+ end
37
+
38
+ def example(str)
39
+ examples << str
40
+ end
41
+
42
+ def match?(str)
43
+ normalize(name) == normalize(str)
44
+ end
45
+
46
+ def attributes
47
+ @attributes ||= []
48
+ end
49
+
50
+ def relations
51
+ @relations ||= []
52
+ end
53
+
54
+ def examples
55
+ @examples ||= []
56
+ end
57
+
58
+ def attribute(*args, **kwargs, &block)
59
+ evaluator.attribute(*args, **kwargs, &block)
60
+ end
61
+
62
+ def relation(*args, **kwargs, &block)
63
+ evaluator.rel(*args, **kwargs, &block)
64
+ end
65
+ alias rel relation
66
+
67
+ def descriptor(id)
68
+ find_attribute(id) || find_relation(id)
69
+ end
70
+
71
+ def find_attribute(id)
72
+ attributes.find { |attr| attr.id.to_sym == id.to_sym }
73
+ end
74
+
75
+ def find_relation(id)
76
+ relations.find { |rel| rel.id.to_sym == id.to_sym }
77
+ end
78
+
79
+ def use(*descriptors, from:, doc: nil)
80
+ descriptors.each do |id|
81
+ desc = from.descriptor(id)
82
+ href = profile_path(from.name, fragment_id: id)
83
+
84
+ case desc
85
+ when Relation
86
+ kwargs = {
87
+ doc: doc || desc&.doc,
88
+ href: href,
89
+ http_methods: desc.http_methods,
90
+ payload_type: desc.payload_type,
91
+ content_type: desc.content_type,
92
+ }
93
+ relation(id, **kwargs)
94
+ when Attribute
95
+ attribute(id, href: href, doc: doc)
96
+ when NilClass
97
+ raise "#{from.name} does not have a descriptor with id #{id}"
98
+ else
99
+ raise Errors::ServerError, "Unsupported descriptor: #{desc}"
100
+ end
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def evaluator
107
+ Evaluator.new(parent: self)
108
+ end
109
+ end
110
+
111
+ def name
112
+ normalize(self.class.name)
113
+ end
114
+ end
115
+ end
@@ -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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shaf
4
+ module Profiles
5
+ class ShafError < Shaf::Profile
6
+ name 'shaf-error'
7
+ urn 'urn:shaf:error'
8
+
9
+ doc 'This profile describes a set of descriptors for generic error messages.'
10
+ example <<~EXAMPLE
11
+ {
12
+ "title": "Invalid entity",
13
+ "message": "The user could not be saved, due to unfulfilled requirements",
14
+ "code": "VALIDATION_ERROR",
15
+ "fields": {
16
+ "email": ["cannot be empty"]
17
+ }
18
+ }
19
+ EXAMPLE
20
+
21
+ example <<~EXAMPLE
22
+ {
23
+ "title": "Unpermitted action",
24
+ "message": "User is not allowed to edit this resource",
25
+ "code": "FORBIDDEN_ERROR",
26
+ }
27
+ EXAMPLE
28
+
29
+ attribute :code,
30
+ type: :string,
31
+ doc: 'An identifier that describes the type of error.'
32
+
33
+ attribute :title,
34
+ type: :string,
35
+ doc: 'A short string used for labeling the error.'
36
+
37
+ attribute :message,
38
+ type: :string,
39
+ doc: 'A description with details about the error.'
40
+
41
+ attribute :fields,
42
+ type: :string,
43
+ doc: 'An object describing validation errors. ' \
44
+ 'The keys correspond to attributes of a resource. ' \
45
+ 'The values are an array of strings describing validation errors ' \
46
+ 'for the corresponding attribute.'
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shaf
4
+ module Profiles
5
+ class ShafForm < Shaf::Profile
6
+ name 'shaf-form'
7
+ urn 'urn:shaf:form'
8
+
9
+ doc 'This profile describes how a set of descriptors should be interpreted to ' \
10
+ 'turn a generic media type into a form (similar to HTML forms). For ' \
11
+ 'example, the HAL media type (application/hal+json) does not specify any ' \
12
+ 'semantics about forms, however we can add semantics about forms to a HAL ' \
13
+ 'document using this profile.'
14
+
15
+ example <<~EXAMPLE
16
+ Given the following form:
17
+
18
+ ```json
19
+ "create-form": {
20
+ "method": "POST",
21
+ "name": "create-post",
22
+ "title": "Create Post",
23
+ "href": "/posts",
24
+ "type": "application/json",
25
+ "_links": {
26
+ "self": {
27
+ "href": "http://localhost:3000/posts/form"
28
+ }
29
+ },
30
+ "fields": [
31
+ {
32
+ "name": "title",
33
+ "type": "string",
34
+ },
35
+ {
36
+ "name": "message",
37
+ "type": "string",
38
+ }
39
+ ]
40
+ }
41
+ ```
42
+
43
+ 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.
44
+
45
+ Using Curl, the form above could be submitted using:
46
+
47
+ ```sh
48
+ curl -H "Content-Type: application/json" \
49
+ -X POST \
50
+ -d '{"title": "hello", "message": "world"}' \
51
+ localhost:3000/posts
52
+ ```
53
+ EXAMPLE
54
+
55
+ attribute :method,
56
+ type: :string,
57
+ doc: 'The HTTP method used for submitting the form'
58
+
59
+ attribute :href,
60
+ type: :string,
61
+ doc: 'The target uri that the form should be submitted to'
62
+
63
+ attribute :name,
64
+ type: :string,
65
+ doc: 'A string used to indentify the form.'
66
+
67
+ attribute :title,
68
+ type: :string,
69
+ doc: 'A string used as title/label when showing the form in a user interface'
70
+
71
+ attribute :type,
72
+ type: :string,
73
+ doc: 'The media type that must be set in the CONTENT-TYPE header when submiting the form'
74
+
75
+ attribute :submit,
76
+ type: :string,
77
+ doc: 'A string used as label for the CallToAction (e.g. the submit button text)'
78
+
79
+ attribute :fields, doc: 'A list of field descriptors' do
80
+
81
+ attribute :name,
82
+ type: :string,
83
+ doc: 'A string that should be used as key for the field.'
84
+
85
+ attribute :type,
86
+ type: :string,
87
+ doc: 'A string specifying the possible values accepted by the field.'
88
+
89
+ attribute :title,
90
+ type: :string,
91
+ doc: 'A string used to present this field in a UI. If not present name may be used instead.'
92
+
93
+ attribute :value,
94
+ doc: 'A prefilled value. Should be shown in a user interface (unless hidden) and sent back on submission unless changed.'
95
+
96
+ attribute :required,
97
+ type: :boolean,
98
+ doc: 'A boolean specifying that the field must be given a value before the form is submitted.'
99
+
100
+ attribute :hidden,
101
+ type: :boolean,
102
+ doc: 'A boolean specifying that the field should not be shown in a user interface.'
103
+
104
+ attribute :accepted_values,
105
+ type: :array,
106
+ doc: 'A list of av acceptable values. Submitting a value not present in this list SHOULD result in a client error response.'
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,46 @@
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 unregister(clazz)
18
+ profiles.delete(clazz)
19
+ end
20
+
21
+ def find(name)
22
+ name = String(name)
23
+ return if name.empty?
24
+
25
+ profiles.find { |profile| profile.match? name }
26
+ end
27
+
28
+ def find!(name)
29
+ find(name) or raise ProfileNotFoundError, name
30
+ end
31
+
32
+ def profiles
33
+ @profiles ||= []
34
+ end
35
+
36
+ def clear
37
+ @profiles.clear
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ require 'shaf/profile'
44
+ require 'shaf/profiles/shaf_form'
45
+ require 'shaf/profiles/shaf_error'
46
+ require 'shaf/profiles/shaf_basic'
@@ -1,15 +1,58 @@
1
1
  module Shaf
2
2
  module RegistrableFactory
3
-
4
3
  class NotFoundError < StandardError; end
4
+ class NoIdentifiersError < StandardError
5
+
6
+ def initialize(clazz)
7
+ super <<~ERR
8
+ #{clazz} does not have an @identifiers ivar.
9
+ Did you perhaps forget to call `#{clazz}.identifier`?
10
+ ERR
11
+ end
12
+ end
13
+
14
+ class Entry
15
+ attr_reader :clazz
16
+
17
+ def initialize(clazz)
18
+ @clazz = clazz
19
+ end
20
+
21
+ def match?(strings)
22
+ raise NoIdentifiersError, clazz unless identifiers
23
+ return false if strings.size < identifiers.size
24
+ identifiers.zip(strings).all? { |pattern, str| matching_identifier? str, pattern }
25
+ end
26
+
27
+ def identifier_count
28
+ identifiers&.size || 0
29
+ end
30
+
31
+ def usage
32
+ clazz.instance_variable_get(:@usage)
33
+ end
34
+
35
+ private
36
+
37
+ def identifiers
38
+ clazz.identified_by
39
+ end
40
+
41
+ def matching_identifier?(str, pattern)
42
+ return false if pattern.nil? || str.nil? || str.empty?
43
+ pattern = pattern.to_s if pattern.is_a? Symbol
44
+ return str == pattern if pattern.is_a? String
45
+ !!str.match(pattern)
46
+ end
47
+ end
5
48
 
6
49
  def all
7
- reg.dup
50
+ reg.map(&:clazz)
8
51
  end
9
52
 
10
- def each
53
+ def each(&block)
11
54
  return all.each unless block_given?
12
- all.each { |c| yield c }
55
+ all.each(&block)
13
56
  end
14
57
 
15
58
  def size
@@ -17,34 +60,31 @@ module Shaf
17
60
  end
18
61
 
19
62
  def register(clazz)
20
- reg << clazz
63
+ reg << Entry.new(clazz)
21
64
  end
22
65
 
23
66
  def unregister(*str)
24
67
  return if str.empty? || !str.all?
25
- reg.delete_if { |clazz| matching_class? str, clazz }
68
+ reg.delete_if { |entry| entry.match? str }
26
69
  end
27
70
 
28
71
  def lookup(*str)
29
- return if str.empty? || !str.all?
30
- reg.select { |clazz| matching_class? str, clazz }
31
- .sort_by(&method(:identifier_count))
32
- .last
72
+ lookup_entry(*str)&.clazz
33
73
  end
34
74
 
35
75
  def usage
36
76
  reg.compact.map do |entry|
37
- usage = entry.instance_variable_get(:@usage)
77
+ usage = entry.usage
38
78
  usage.respond_to?(:call) ? usage.call : usage
39
79
  end
40
80
  end
41
81
 
42
82
  def create(*params, **options)
43
- clazz = lookup(*params)
44
- raise NotFoundError.new(%Q(Command '#{ARGV}' is not supported)) unless clazz
83
+ entry = lookup_entry(*params)
84
+ raise NotFoundError.new(%Q(Command '#{ARGV}' is not supported)) unless entry
45
85
 
46
- args = init_args(clazz, params)
47
- clazz.new(*args, **options)
86
+ args = init_args(entry, params)
87
+ entry.clazz.new(*args, **options)
48
88
  end
49
89
 
50
90
  private
@@ -53,25 +93,15 @@ module Shaf
53
93
  @reg ||= []
54
94
  end
55
95
 
56
- def matching_class?(strings, clazz)
57
- identifiers = clazz.instance_variable_get(:@identifiers)
58
- return false if strings.size < identifiers.size
59
- identifiers.zip(strings).all? { |pattern, str| matching_identifier? str, pattern }
60
- end
61
-
62
- def matching_identifier?(str, pattern)
63
- return false if pattern.nil? || str.nil? || str.empty?
64
- pattern = pattern.to_s if pattern.is_a? Symbol
65
- return str == pattern if pattern.is_a? String
66
- str.match(pattern) || false
67
- end
68
-
69
- def identifier_count(clazz)
70
- clazz.instance_variable_get(:@identifiers)&.size || 0
96
+ def lookup_entry(*str)
97
+ return if str.empty? || !str.all?
98
+ reg.select { |entry| entry.match? str }
99
+ .sort_by { |entry| entry.identifier_count }
100
+ .last
71
101
  end
72
102
 
73
- def init_args(clazz, params)
74
- first_non_id = identifier_count(clazz)
103
+ def init_args(entry, params)
104
+ first_non_id = entry.identifier_count
75
105
  params[first_non_id..-1]
76
106
  end
77
107
  end
@@ -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
+
@@ -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
- preload_links = preload_links(preload, responder, response, controller)
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 preload_links(rels, responder, response, controller = nil)
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, preload_links)
64
+ add_preload_links(controller, response)
73
65
  controller.body(response.body)
74
66
  end
75
67
 
76
- def add_preload_links(controller, response, preload_links)
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