linked_rails 0.0.3 → 0.0.4.pre.g14b377f91

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.
Files changed (173) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -674
  3. data/app/controllers/linked_rails/actions/objects_controller.rb +9 -0
  4. data/app/controllers/linked_rails/bulk_controller.rb +71 -21
  5. data/app/controllers/linked_rails/enum_values_controller.rb +0 -42
  6. data/app/models/linked_rails/actions/item.rb +64 -55
  7. data/app/models/linked_rails/actions/list.rb +6 -31
  8. data/app/models/linked_rails/actions/object.rb +38 -0
  9. data/app/models/linked_rails/collection/configuration.rb +55 -0
  10. data/app/models/linked_rails/collection/filter.rb +1 -1
  11. data/app/models/linked_rails/collection/filter_field.rb +19 -2
  12. data/app/models/linked_rails/collection/filter_option.rb +1 -1
  13. data/app/models/linked_rails/collection/filterable.rb +6 -9
  14. data/app/models/linked_rails/collection/infinite.rb +113 -0
  15. data/app/models/linked_rails/collection/infinite_view.rb +1 -90
  16. data/app/models/linked_rails/collection/iri.rb +47 -43
  17. data/app/models/linked_rails/collection/iri_mapping.rb +15 -7
  18. data/app/models/linked_rails/collection/paginated.rb +46 -0
  19. data/app/models/linked_rails/collection/paginated_view.rb +1 -33
  20. data/app/models/linked_rails/collection/sortable.rb +1 -9
  21. data/app/models/linked_rails/collection/sorting.rb +1 -1
  22. data/app/models/linked_rails/collection/view.rb +51 -14
  23. data/app/models/linked_rails/collection.rb +53 -85
  24. data/app/models/linked_rails/creative_work.rb +1 -1
  25. data/app/models/linked_rails/entry_point.rb +8 -5
  26. data/app/models/linked_rails/enum_value.rb +40 -2
  27. data/app/models/linked_rails/form/field/association_input.rb +7 -1
  28. data/app/models/linked_rails/form/field/file_input.rb +1 -0
  29. data/app/models/linked_rails/form/field/resource_field.rb +2 -0
  30. data/{lib/linked_rails/enhancements/indexable/model.rb → app/models/linked_rails/form/field/url_input.rb} +3 -3
  31. data/app/models/linked_rails/form/field.rb +37 -13
  32. data/app/models/linked_rails/form/field_factory.rb +48 -18
  33. data/app/models/linked_rails/form/group.rb +4 -6
  34. data/app/models/linked_rails/form/page.rb +8 -4
  35. data/app/models/linked_rails/form.rb +16 -21
  36. data/app/models/linked_rails/manifest.rb +104 -22
  37. data/app/models/linked_rails/menus/item.rb +10 -13
  38. data/app/models/linked_rails/menus/list.rb +16 -7
  39. data/app/models/linked_rails/ontology/base.rb +3 -1
  40. data/app/models/linked_rails/ontology/class.rb +3 -3
  41. data/app/models/linked_rails/ontology.rb +5 -5
  42. data/app/models/linked_rails/property_query.rb +2 -0
  43. data/app/models/linked_rails/sequence.rb +4 -13
  44. data/app/models/linked_rails/shacl/property_shape.rb +1 -1
  45. data/app/models/linked_rails/web_page.rb +0 -4
  46. data/app/models/linked_rails/web_site.rb +0 -4
  47. data/app/models/linked_rails/widget.rb +4 -11
  48. data/app/policies/linked_rails/actions/object_policy.rb +11 -0
  49. data/app/policies/linked_rails/collection_policy.rb +2 -2
  50. data/app/serializers/linked_rails/actions/item_serializer.rb +5 -5
  51. data/app/serializers/linked_rails/actions/object_serializer.rb +9 -0
  52. data/app/serializers/linked_rails/collection/filter_field_serializer.rb +3 -2
  53. data/app/serializers/linked_rails/collection/filter_option_serializer.rb +1 -1
  54. data/app/serializers/linked_rails/collection/filter_serializer.rb +1 -1
  55. data/app/serializers/linked_rails/collection/sorting_serializer.rb +1 -1
  56. data/app/serializers/linked_rails/collection/view_serializer.rb +3 -3
  57. data/app/serializers/linked_rails/collection_serializer.rb +8 -7
  58. data/app/serializers/linked_rails/condition_serializer.rb +3 -3
  59. data/app/serializers/linked_rails/entry_point_serializer.rb +3 -3
  60. data/app/serializers/linked_rails/enum_value_serializer.rb +1 -0
  61. data/app/serializers/linked_rails/form/field/association_input_serializer.rb +1 -0
  62. data/app/serializers/linked_rails/form/field/file_input_serializer.rb +11 -0
  63. data/app/serializers/linked_rails/form/field_serializer.rb +3 -1
  64. data/app/serializers/linked_rails/form/group_serializer.rb +1 -1
  65. data/app/serializers/linked_rails/form/page_serializer.rb +1 -1
  66. data/app/serializers/linked_rails/menus/item_serializer.rb +3 -3
  67. data/app/serializers/linked_rails/menus/list_serializer.rb +1 -1
  68. data/app/serializers/linked_rails/ontology_serializer.rb +2 -2
  69. data/app/serializers/linked_rails/property_query_serializer.rb +7 -0
  70. data/app/serializers/linked_rails/sequence_serializer.rb +2 -5
  71. data/app/serializers/linked_rails/shacl/node_shape_serializer.rb +1 -1
  72. data/app/serializers/linked_rails/shacl/property_shape_serializer.rb +1 -1
  73. data/app/serializers/linked_rails/shacl/shape_serializer.rb +5 -5
  74. data/app/serializers/linked_rails/web_page_serializer.rb +3 -3
  75. data/app/serializers/linked_rails/web_site_serializer.rb +1 -1
  76. data/app/serializers/linked_rails/widget_serializer.rb +3 -3
  77. data/lib/generators/linked_rails/install/install_generator.rb +5 -8
  78. data/lib/generators/linked_rails/install/templates/README +2 -0
  79. data/lib/generators/linked_rails/install/templates/app_menu_list.rb +36 -7
  80. data/lib/generators/linked_rails/install/templates/application_menu_list.rb +40 -1
  81. data/lib/generators/linked_rails/install/templates/initializer.rb +1 -2
  82. data/lib/generators/linked_rails/install/templates/locales.yml +14 -0
  83. data/lib/generators/linked_rails/install/templates/rdf_serializers_initializer.rb +1 -1
  84. data/lib/generators/linked_rails/install/templates/vocab.rb +1 -0
  85. data/lib/generators/linked_rails/install/templates/vocab.yml +2 -2
  86. data/lib/generators/linked_rails/model/model_generator.rb +0 -1
  87. data/lib/generators/linked_rails/model/templates/controller.rb.tt +5 -1
  88. data/lib/generators/linked_rails/model/templates/form.rb.tt +3 -0
  89. data/lib/generators/linked_rails/model/templates/menu_list.rb.tt +15 -0
  90. data/lib/generators/linked_rails/model/templates/policy.rb.tt +13 -0
  91. data/lib/generators/linked_rails/model/templates/serializer.rb.tt +5 -1
  92. data/lib/linked_rails/active_response/controller/collections.rb +1 -1
  93. data/lib/linked_rails/active_response/controller/crud_defaults.rb +4 -4
  94. data/lib/linked_rails/active_response/controller/params.rb +10 -10
  95. data/lib/linked_rails/active_response/controller.rb +8 -18
  96. data/lib/linked_rails/active_response/responders/rdf.rb +19 -10
  97. data/lib/linked_rails/callable_variable.rb +1 -1
  98. data/lib/linked_rails/collection_params_parser.rb +93 -0
  99. data/lib/linked_rails/controller/actionable.rb +121 -0
  100. data/lib/linked_rails/controller/authorization.rb +6 -0
  101. data/lib/linked_rails/controller/default_actions/create.rb +52 -0
  102. data/lib/linked_rails/controller/default_actions/destroy.rb +42 -0
  103. data/lib/linked_rails/controller/default_actions/update.rb +43 -0
  104. data/lib/linked_rails/controller/delta.rb +58 -0
  105. data/lib/linked_rails/controller/error_handling.rb +10 -10
  106. data/lib/linked_rails/controller/rendering.rb +48 -0
  107. data/lib/linked_rails/controller.rb +24 -4
  108. data/lib/linked_rails/enhancements/creatable/controller.rb +1 -1
  109. data/lib/linked_rails/enhancements/destroyable/controller.rb +1 -1
  110. data/lib/linked_rails/enhancements/updatable/controller.rb +1 -1
  111. data/lib/linked_rails/enhancements.rb +0 -16
  112. data/lib/linked_rails/helpers/delta_helper.rb +26 -57
  113. data/lib/linked_rails/helpers/ontola_actions_helper.rb +2 -2
  114. data/lib/linked_rails/helpers/resource_helper.rb +4 -2
  115. data/lib/linked_rails/iri_mapper.rb +17 -39
  116. data/lib/linked_rails/middleware/error_handling.rb +51 -0
  117. data/lib/linked_rails/middleware/linked_data_params.rb +30 -151
  118. data/lib/linked_rails/model/actionable.rb +68 -0
  119. data/lib/linked_rails/model/collections.rb +201 -39
  120. data/lib/linked_rails/model/dirty.rb +6 -18
  121. data/lib/linked_rails/model/enhancements.rb +1 -6
  122. data/lib/linked_rails/model/filtering.rb +4 -6
  123. data/lib/linked_rails/model/indexable.rb +6 -16
  124. data/lib/linked_rails/model/iri.rb +28 -19
  125. data/lib/linked_rails/model/iri_mapping.rb +37 -8
  126. data/lib/linked_rails/model/menuable.rb +28 -0
  127. data/lib/linked_rails/model/serialization.rb +2 -15
  128. data/lib/linked_rails/model/singularable.rb +57 -0
  129. data/lib/linked_rails/model/sorting.rb +0 -5
  130. data/lib/linked_rails/model/tables.rb +26 -0
  131. data/lib/linked_rails/model.rb +17 -7
  132. data/lib/linked_rails/params_parser.rb +131 -54
  133. data/lib/linked_rails/policy/attribute_conditions.rb +2 -2
  134. data/lib/linked_rails/policy.rb +40 -46
  135. data/lib/linked_rails/railtie.rb +11 -0
  136. data/lib/linked_rails/rdf_error.rb +2 -2
  137. data/lib/linked_rails/renderers.rb +1 -0
  138. data/lib/linked_rails/routes.rb +38 -22
  139. data/lib/linked_rails/serializer/actionable.rb +27 -0
  140. data/lib/linked_rails/serializer/menuable.rb +31 -0
  141. data/lib/linked_rails/serializer/singularable.rb +26 -0
  142. data/lib/linked_rails/serializer.rb +28 -11
  143. data/lib/linked_rails/test_methods.rb +114 -0
  144. data/lib/linked_rails/translate.rb +31 -9
  145. data/lib/linked_rails/types/iri_type.rb +37 -0
  146. data/lib/linked_rails/uri_template.rb +30 -0
  147. data/lib/linked_rails/version.rb +1 -1
  148. data/lib/linked_rails/vocab.rb +9 -0
  149. data/lib/linked_rails.rb +30 -13
  150. data/lib/rails/welcome_controller.rb +3 -3
  151. data/lib/rdf/list.rb +9 -0
  152. data/lib/rdf/query_fix.rb +15 -0
  153. metadata +71 -33
  154. data/app/models/linked_rails/actions/default_actions/create.rb +0 -60
  155. data/app/models/linked_rails/actions/default_actions/destroy.rb +0 -45
  156. data/app/models/linked_rails/actions/default_actions/update.rb +0 -50
  157. data/app/models/linked_rails/actions/default_actions.rb +0 -17
  158. data/lib/generators/linked_rails/install/templates/application_action_list.rb +0 -3
  159. data/lib/generators/linked_rails/model/templates/action_list.rb.tt +0 -6
  160. data/lib/linked_rails/enhancements/actionable/model.rb +0 -71
  161. data/lib/linked_rails/enhancements/actionable/serializer.rb +0 -25
  162. data/lib/linked_rails/enhancements/creatable/action.rb +0 -15
  163. data/lib/linked_rails/enhancements/destroyable/action.rb +0 -15
  164. data/lib/linked_rails/enhancements/destroyable/routing.rb +0 -19
  165. data/lib/linked_rails/enhancements/menuable/model.rb +0 -36
  166. data/lib/linked_rails/enhancements/menuable/serializer.rb +0 -33
  167. data/lib/linked_rails/enhancements/route_concerns.rb +0 -56
  168. data/lib/linked_rails/enhancements/singularable/controller.rb +0 -43
  169. data/lib/linked_rails/enhancements/singularable/model.rb +0 -47
  170. data/lib/linked_rails/enhancements/singularable/serializer.rb +0 -28
  171. data/lib/linked_rails/enhancements/tableable/model.rb +0 -28
  172. data/lib/linked_rails/enhancements/updatable/action.rb +0 -15
  173. data/lib/linked_rails/enhancements/updatable/routing.rb +0 -20
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # require 'empathy/emp_json'
4
+
3
5
  module LinkedRails
4
6
  module Middleware
5
- class LinkedDataParams # rubocop:disable Metrics/ClassLength
7
+ class LinkedDataParams
8
+ include ::Empathy::EmpJson::Helpers::Slices
9
+ include ::Empathy::EmpJson::Helpers::Parsing
10
+
6
11
  def initialize(app)
7
12
  @app = app
8
13
  end
@@ -10,40 +15,15 @@ module LinkedRails
10
15
  def call(env)
11
16
  req = Rack::Request.new(env)
12
17
  params_from_query(req)
13
- params_from_graph(req)
18
+ params_from_slice(req)
14
19
 
15
20
  @app.call(env)
16
21
  end
17
22
 
18
23
  private
19
24
 
20
- def add_param(hash, key, value) # rubocop:disable Metrics/MethodLength
21
- case hash[key]
22
- when nil
23
- hash[key] = value
24
- when Hash
25
- hash[key].merge!(value)
26
- when Array
27
- hash[key].append(value)
28
- else
29
- hash[key] = [hash[key], value]
30
- end
31
- hash
32
- end
33
-
34
- def associated_class_from_params(reflection, graph, subject)
35
- return reflection.klass unless reflection.polymorphic?
36
-
37
- query = graph.query(subject: subject, predicate: Vocab.rdfv[:type])
38
- if query.empty?
39
- raise("No type given for '#{subject}' referenced by polymorphic association '#{reflection.name}'")
40
- end
41
-
42
- iri_to_class(query.first.object)
43
- end
44
-
45
- def blob_attribute(base_params, value)
46
- base_params["<#{value}>"] if value.starts_with?(Vocab.ll['blobs/'])
25
+ def add_param_from_query(data, target_class, key, value)
26
+ data[target_class.predicate_mapping[RDF::URI(key)].key] = value
47
27
  end
48
28
 
49
29
  def convert_query_params(request, target_class)
@@ -56,76 +36,32 @@ module LinkedRails
56
36
  request.update_param(class_key, data) if data.present?
57
37
  end
58
38
 
59
- def add_param_from_query(data, target_class, key, value)
60
- data[target_class.predicate_mapping[RDF::URI(key)].key] = value
61
- end
62
-
63
- def enum_attribute(klass, key, value)
64
- opts = RDF::Serializers.serializer_for(klass).try(:enum_options, key)
65
- return if opts.blank?
66
-
67
- opts.detect { |_k, options| options.iri == value }&.first&.to_s
68
- end
69
-
70
- def graph_from_request(request)
71
- request_graph = request.delete_param("<#{Vocab.ll[:graph].value}>")
72
- return if request_graph.blank?
39
+ def slice_from_request(request)
40
+ return unless request.content_type == Mime::Type.lookup_by_extension(:empjson).to_s
73
41
 
74
- RDF::Graph.load(
75
- request_graph[:tempfile].path,
76
- content_type: request_graph[:type],
77
- canonicalize: true,
78
- intern: false
79
- )
80
- end
81
-
82
- def iri_to_class(iri)
83
- iri.to_s.split(LinkedRails.app_vocab.to_s).pop&.classify&.safe_constantize ||
84
- ApplicationRecord.descendants.detect { |klass| klass.iri == iri }
85
- end
86
-
87
- def logger
88
- Rails.logger
89
- end
42
+ body = request.body.read
90
43
 
91
- def nested_attributes(base_params, graph, subject, klass, association, collection) # rubocop:disable Metrics/ParameterLists
92
- nested_resources =
93
- if graph.query([subject, Vocab.rdfv[:first], nil]).present?
94
- nested_attributes_from_list(base_params, graph, subject, klass)
95
- else
96
- parsed = parse_nested_resource(base_params, graph, subject, klass)
97
- collection ? {rand(1_000_000_000).to_s => parsed} : parsed
98
- end
99
- ["#{association}_attributes", nested_resources]
44
+ JSON.parse(body) if body.present?
100
45
  end
101
46
 
102
- def nested_attributes_from_list(base_params, graph, subject, klass)
103
- Hash[
104
- RDF::List.new(subject: subject, graph: graph)
105
- .map { |nested| [rand(1_000_000_000).to_s, parse_nested_resource(base_params, graph, nested, klass)] }
106
- ]
107
- end
108
-
109
- # Converts a serialized graph from a multipart request body to a nested
110
- # attributes hash.
47
+ # Converts a emp slice from to a nested attributes hash.
111
48
  #
112
- # The graph sent to the server should be sent under the `ll:graph` form name.
113
- # The entrypoint for the graph is the `ll:targetResource` subject, which is
49
+ # The entrypoint for the slice is the `.` subject, which is
114
50
  # assumed to be the resource intended to be targeted by the request (i.e. the
115
51
  # resource to be created, updated, or deleted).
116
52
  #
117
53
  # @return [Hash] A hash of attributes, empty if no statements were given.
118
- def params_from_graph(request)
119
- graph = graph_from_request(request)
54
+ def params_from_slice(request)
55
+ slice = slice_from_request(request)
120
56
 
121
- return unless graph
57
+ return unless slice
122
58
 
123
- request.update_param(:body_graph, graph)
59
+ request.env['emp_json'] = slice
124
60
  target_class = target_class_from_path(request)
125
61
  return if target_class.blank?
126
62
 
127
- update_actor_param(request, graph)
128
- update_target_params(request, graph, target_class)
63
+ update_actor_param(request, slice)
64
+ update_target_params(request, slice, target_class)
129
65
  end
130
66
 
131
67
  def params_from_query(request)
@@ -135,87 +71,30 @@ module LinkedRails
135
71
  convert_query_params(request, target_class)
136
72
  end
137
73
 
138
- def parse_nested_resource(base_params, graph, subject, klass)
139
- resource = parse_resource(base_params, graph, subject, klass)
140
- resource[:id] ||= LinkedRails.iri_mapper.opts_from_iri(subject)[:params][:id] if subject.iri?
141
- resource
142
- end
143
-
144
- # Recursively parses a resource from graph
145
- def parse_resource(base_params, graph, subject, klass)
146
- graph
147
- .query([subject])
148
- .map { |statement| parse_statement(base_params, graph, statement, klass) }
149
- .compact
150
- .reduce({}) { |h, (k, v)| add_param(h, k, v) }
151
- end
152
-
153
- def parse_statement(base_params, graph, statement, klass)
154
- field = serializer_field(klass, statement.predicate)
155
- if field.is_a?(FastJsonapi::Attribute)
156
- parsed_attribute(base_params, klass, field.key, statement.object.value)
157
- elsif field.is_a?(FastJsonapi::Relationship)
158
- parsed_association(base_params, graph, statement.object, klass, field.association || field.key)
159
- end
160
- end
161
-
162
- def parsed_association(base_params, graph, object, klass, association)
163
- reflection = klass.reflect_on_association(association) || raise("#{association} not found for #{klass}")
164
-
165
- if graph.has_subject?(object)
166
- association_klass = associated_class_from_params(reflection, graph, object)
167
- nested_attributes(base_params, graph, object, association_klass, association, reflection.collection?)
168
- elsif object.iri?
169
- attributes_from_iri(object, association, reflection)
170
- end
171
- end
172
-
173
- def attributes_from_iri(object, association, reflection)
174
- if reflection.options[:through]
175
- key = reflection.has_one? ? "#{association}_id" : "#{association.to_s.singularize}_ids"
176
- elsif reflection.belongs_to?
177
- key = reflection.foreign_key
178
- end
179
- return unless key
180
-
181
- resource = LinkedRails.iri_mapper.resource_from_iri(object, nil)
182
- value = resource&.send(reflection.association_primary_key)
183
-
184
- [key, value] if value
185
- end
186
-
187
- def parsed_attribute(base_params, klass, key, value)
188
- [key, blob_attribute(base_params, value) || enum_attribute(klass, key, value) || value]
189
- end
190
-
191
- def serializer_field(klass, predicate)
192
- field = klass.try(:predicate_mapping).try(:[], predicate)
193
- logger.info("#{predicate} not found for #{klass || 'nil'}") if field.blank?
194
- field
195
- end
196
-
197
74
  def target_class_from_path(request) # rubocop:disable Metrics/AbcSize
198
75
  opts = LinkedRails.iri_mapper.opts_from_iri(
199
76
  request.base_url + request.env['REQUEST_URI'],
200
77
  method: request.request_method
201
78
  )
202
79
 
203
- logger.info("No class found for #{request.base_url + request.env['REQUEST_URI']}") unless opts[:class]
80
+ Rails.logger.info("No class found for #{request.base_url + request.env['REQUEST_URI']}") unless opts[:class]
204
81
 
205
82
  opts[:class]
206
83
  end
207
84
 
208
- def update_actor_param(request, graph)
209
- actor = graph.query([Vocab.ll[:targetResource], Vocab.schema.creator]).first
85
+ def update_actor_param(request, slice)
86
+ actor = values_from_slice(slice, '.', Vocab.schema.creator)
87
+
210
88
  return if actor.blank?
211
89
 
212
- request.update_param(:actor_iri, actor.object)
213
- graph.delete(actor)
90
+ request.update_param(:actor_iri, emp_to_primitive(actor))
214
91
  end
215
92
 
216
- def update_target_params(request, graph, target_class)
93
+ def update_target_params(request, slice, target_class)
217
94
  key = target_class.to_s.demodulize.underscore
218
- from_body = parse_resource(request.params, graph, Vocab.ll[:targetResource], target_class)
95
+
96
+ parser = ParamsParser.new(slice: slice, params: request.params)
97
+ from_body = parser.parse_resource('.', target_class)
219
98
 
220
99
  request.update_param(key, from_body.merge(request.params[key] || {}))
221
100
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Model
5
+ module Actionable
6
+ extend ActiveSupport::Concern
7
+
8
+ def actions(user_context = nil)
9
+ action_list(user_context).actions
10
+ end
11
+
12
+ def action(tag, user_context = nil)
13
+ actions(user_context).find { |a| a.tag == tag }
14
+ end
15
+
16
+ def action_list(user_context)
17
+ @action_list ||= {}
18
+ @action_list[user_context] ||= self.class.action_list.new(resource: self, user_context: user_context)
19
+ end
20
+
21
+ def action_triples
22
+ @action_triples ||= triples_for_actions(actions) + triples_for_actions(collection_actions)
23
+ end
24
+
25
+ def collection_actions
26
+ (try(:collections) || []).map do |opts|
27
+ collection_for(opts[:name]).actions
28
+ end.flatten
29
+ end
30
+
31
+ def favorite_actions
32
+ actions.filter(&:favorite)
33
+ end
34
+
35
+ private
36
+
37
+ def triples_for_actions(actions)
38
+ actions.flat_map do |action|
39
+ [
40
+ [iri, action.predicate, action.iri],
41
+ [iri, Vocab.schema.potentialAction, action.iri]
42
+ ]
43
+ end
44
+ end
45
+
46
+ module ClassMethods
47
+ def action_list
48
+ @action_list ||= define_action_list
49
+ end
50
+
51
+ private
52
+
53
+ def action_superclass
54
+ superclass.try(:action_list) || LinkedRails.action_list_parent_class
55
+ end
56
+
57
+ def define_action_list
58
+ klass = Class.new(action_superclass)
59
+ actionable_class = self
60
+ klass.define_singleton_method(:actionable_class) do
61
+ actionable_class
62
+ end
63
+ klass
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -5,8 +5,181 @@ module LinkedRails
5
5
  module Collections
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ COLLECTION_CUSTOMIZABLE_OPTIONS = {
9
+ # display [Sym] The default display type.
10
+ # Choose between :grid, :settingsTable, :table, :card, :default
11
+ display: :default,
12
+ # grid_max_columns [Integer] The default amount of columns to use in a grid.
13
+ grid_max_columns: 3,
14
+ # page_size [Integer] The default page size.
15
+ page_size: 20,
16
+ # table_type [Sym] The columns to use in the table.
17
+ table_type: lambda {
18
+ case display&.to_sym
19
+ when :table
20
+ :default
21
+ when :settingsTable
22
+ :settings
23
+ end
24
+ },
25
+ # title String The default title.
26
+ title: -> { title_from_translation },
27
+ # type [Sym] The default pagination type.
28
+ # Choose between :paginated, :infinite.
29
+ type: :paginated
30
+ }.freeze
31
+ COLLECTION_STATIC_OPTIONS = {
32
+ # association [Sym] The association of the collection items.
33
+ association: nil,
34
+ # association_base [Scope, Array] The items of the collection.
35
+ association_base: -> { apply_scope(sorted_association(filtered_association)) },
36
+ # association_class [Class] The class of the collection items.
37
+ association_class: nil,
38
+ # association_scope [Sym] The scope applied to the collection.
39
+ association_scope: nil,
40
+ # call_to_action [String] The label shown as call to action.
41
+ call_to_action: nil,
42
+ # collection_class [Class] The base class of the collection.
43
+ # If you want to use a class other than LinkedRails.collection_class.
44
+ collection_class: nil,
45
+ # collection_class [Hash] The default filters applied to the collection.
46
+ default_filters: {},
47
+ # collection_class [Array<Hash>] The default sortings applied to the collection.
48
+ default_sortings: [{key: Vocab.schema.dateCreated, direction: :desc}],
49
+ # include_members [Boolean] Whether to include the members of this collection.
50
+ include_members: false,
51
+ # iri_template_keys [Array<Sym>] Custom query keys for the iri template
52
+ iri_template_keys: [],
53
+ # joins [Array<Sym>, Sym] The associations to join
54
+ joins: nil,
55
+ # parent [Instance] The default parent of a collection.
56
+ parent: nil,
57
+ # parent_iri [Array<String>] The iri elements of the parent
58
+ parent_iri: -> { parent&.iri_elements },
59
+ # part_of [Instance] The record to serialize as isPartOf
60
+ part_of: -> { parent },
61
+ # policy_scope [Scope] The policy scope class to be used for scoping
62
+ # Set to false to skip scoping
63
+ policy_scope: -> { policy ? policy::Scope : Pundit::PolicyFinder.new(filtered_association).scope! },
64
+ # route_key [Symbol, String] The route key for the association
65
+ route_key: nil,
66
+ # view [IRI] The view to use for rendering the members
67
+ view: nil
68
+ }.freeze
69
+ COLLECTION_OPTIONS = COLLECTION_CUSTOMIZABLE_OPTIONS.merge(COLLECTION_STATIC_OPTIONS)
70
+
71
+ module ClassMethods
72
+ def collection_iri(**opts)
73
+ LinkedRails.iri(path: collection_root_relative_iri(**opts))
74
+ end
75
+
76
+ # Sets the defaults for all collections for this class.
77
+ # Can be overridden by #with_collection, called from associated models,
78
+ # or by passing parameters in an iri.
79
+ # @param [Hash] options
80
+ def collection_options(**options)
81
+ initialize_default_collection_opts
82
+
83
+ options.each do |key, value|
84
+ raise("Invalid key passed to collection_options: #{key}") unless valid_collection_option?(key)
85
+
86
+ _default_collection_opts[key] = value
87
+ end
88
+ _default_collection_opts[:iri_template] = _default_collection_opts[:collection_class].generate_iri_template(
89
+ _default_collection_opts[:iri_template_keys]
90
+ )
91
+ _default_collection_opts
92
+ end
93
+
94
+ def collection_root_relative_iri(**opts)
95
+ opts[:filter] = LinkedRails.collection_class.filter_iri_opts(opts[:filter]) if opts.key?(:filter)
96
+ opts[:route_key] = collection_route_key
97
+ default_collection_option(:iri_template).expand(**opts)
98
+ end
99
+
100
+ def collection_route_key
101
+ default_collection_option(:route_key) || route_key
102
+ end
103
+
104
+ def default_collection_options
105
+ initialize_default_collection_opts
106
+
107
+ _default_collection_opts
108
+ end
109
+
110
+ def default_collection_option(key)
111
+ default_collection_options[key]
112
+ end
113
+
114
+ # Defines a collection to be used in {collection_for}
115
+ # @see Ldable#collection_for
116
+ # @note Adds a instance_method <name>_collection
117
+ # @param [Hash] name as to be used in {collection_for}
118
+ # @param [Hash] options See COLLECTION_OPTIONS
119
+ # @return [Collection]
120
+ def with_collection(name, **options) # rubocop:disable Metrics/AbcSize
121
+ options[:association] ||= name.to_sym
122
+ options[:association_class] ||= name.to_s.classify.constantize
123
+ merged_options = options[:association_class].default_collection_options.merge(options)
124
+ merged_options[:iri_template] = merged_options[:collection_class].generate_iri_template(
125
+ merged_options[:iri_template_keys]
126
+ )
127
+ collections_add(name: name, options: merged_options)
128
+
129
+ define_method "#{name.to_s.singularize}_collection" do |opts = {}|
130
+ collection_for(name, **opts)
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def collections_add(opts)
137
+ initialize_collections
138
+ collections.delete_if { |c| c[:name] == opts[:name] }
139
+ opts[:options] = sanitized_collection_options(opts[:options])
140
+ collections.append(opts)
141
+ end
142
+
143
+ def initialize_collections
144
+ return if collections && method(:collections).owner == singleton_class
145
+
146
+ self.collections = superclass.try(:collections)&.dup || []
147
+ end
148
+
149
+ def initialize_default_collection_opts # rubocop:disable Metrics/AbcSize
150
+ return if _default_collection_opts && method(:_default_collection_opts).owner == singleton_class
151
+
152
+ self._default_collection_opts = (superclass.try(:_default_collection_opts) || COLLECTION_OPTIONS).dup
153
+
154
+ _default_collection_opts[:collection_class] ||= LinkedRails.collection_class
155
+ _default_collection_opts[:association_class] = self
156
+ _default_collection_opts[:iri_template] = _default_collection_opts[:collection_class].generate_iri_template(
157
+ _default_collection_opts[:iri_template_keys]
158
+ )
159
+
160
+ _default_collection_opts
161
+ end
162
+
163
+ def sanitized_collection_options(opts)
164
+ opts.each_with_object(HashWithIndifferentAccess.new) do |(key, value), hash|
165
+ raise("Invalid key passed to with_collection: #{key}") unless valid_collection_option?(key.to_sym)
166
+
167
+ hash_key = COLLECTION_CUSTOMIZABLE_OPTIONS.key?(key.to_sym) ? "default_#{key}" : key
168
+
169
+ hash[hash_key] = value
170
+ end
171
+ end
172
+
173
+ def valid_collection_option?(key)
174
+ COLLECTION_OPTIONS.key?(key) || key == :iri_template
175
+ end
176
+ end
177
+
8
178
  included do
9
179
  class_attribute :collections
180
+ class_attribute :_default_collection_opts,
181
+ instance_accessor: false,
182
+ instance_predicate: false
10
183
  end
11
184
 
12
185
  # Initialises a {Collection} for one of the collections defined by {has_collection}
@@ -18,19 +191,42 @@ module LinkedRails
18
191
  # @param [ApplicationRecord] part_of
19
192
  # @param [Hash] opts Additional options to be passed to the collection.
20
193
  # @return [Collection]
21
- def collection_for(name, instance_opts = {})
22
- collection_opts = collections.detect { |c| c[:name] == name }.try(:[], :options).dup
194
+ def collection_for(name, **instance_opts)
195
+ collection_opts = collection_options_for(name).dup
23
196
  return if collection_opts.blank?
24
197
 
25
198
  collection_opts[:name] = name
26
199
  collection_opts[:parent] = self
27
- collection_opts[:part_of] = collection_opts.key?(:part_of) ? send(collection_opts[:part_of]) : self
28
- collection_class = collection_opts.delete(:collection_class) || LinkedRails.collection_class
200
+ collection_class =
201
+ collection_opts.delete(:collection_class) ||
202
+ collection_opts[:association_class].default_collection_option(:collection_class) ||
203
+ LinkedRails.collection_class
29
204
  collection_class.collection_or_view(collection_opts, instance_opts)
30
205
  end
31
206
 
207
+ def collection_iri(collection, **opts)
208
+ LinkedRails.iri(path: collection_root_relative_iri(collection, **opts))
209
+ end
210
+
211
+ def collection_options_for(name)
212
+ opts = collections.detect { |c| c[:name] == name.to_sym }
213
+ raise("Collection #{name} not found for #{self}") unless opts
214
+
215
+ opts[:options] || {}
216
+ end
217
+
218
+ def collection_root_relative_iri(collection, **opts)
219
+ collection_opts = collection_options_for(collection).dup
220
+ template = collection_opts[:iri_template]
221
+ klass = collection_opts[:association_class]
222
+ opts[:route_key] = collection_opts[:route_key] || klass.collection_route_key
223
+ opts[:parent_iri] = iri_elements
224
+
225
+ template.expand(**opts).to_s
226
+ end
227
+
32
228
  def parent_collections(user_context)
33
- return [] if try(:parent).try(:collections).blank?
229
+ return [self.class.root_collection(user_context: user_context)] if try(:parent).try(:collections).blank?
34
230
 
35
231
  parent_collections_for(parent, user_context)
36
232
  end
@@ -43,40 +239,6 @@ module LinkedRails
43
239
  .select { |collection| is_a?(collection[:options][:association_class]) }
44
240
  .map { |collection| parent.collection_for(collection[:name], user_context: user_context) }
45
241
  end
46
-
47
- module ClassMethods
48
- def collections_add(opts)
49
- initialize_collections
50
- collections.delete_if { |c| c[:name] == opts[:name] }
51
- collections.append(opts)
52
- end
53
-
54
- def initialize_collections
55
- return if collections && method(:collections).owner == singleton_class
56
-
57
- self.collections = superclass.try(:collections)&.dup || []
58
- end
59
-
60
- # Defines a collection to be used in {collection_for}
61
- # @see Ldable#collection_for
62
- # @note Adds a instance_method <name>_collection
63
- # @param [Hash] name as to be used in {collection_for}
64
- # @param [Hash] options
65
- # @option options [Sym] association the name of the association
66
- # @option options [Class] association_class the class of the association
67
- # @option options [Sym] joins the associations to join
68
- # @return [Collection]
69
- def with_collection(name, options = {})
70
- options[:association] ||= name.to_sym
71
- options[:association_class] ||= name.to_s.classify.constantize
72
-
73
- collections_add(name: name, options: options)
74
-
75
- define_method "#{name.to_s.singularize}_collection" do |opts = {}|
76
- collection_for(name, opts)
77
- end
78
- end
79
- end
80
242
  end
81
243
  end
82
244
  end
@@ -22,40 +22,28 @@ module LinkedRails
22
22
  end
23
23
  end
24
24
 
25
- def previous_changes_by_predicate
26
- serializer_class = RDF::Serializers.serializer_for(self)
27
- return {} unless respond_to?(:previous_changes) && serializer_class
28
-
29
- Hash[
30
- previous_changes
31
- .map { |k, v| [serializer_class.attributes_to_serialize[k.to_sym]&.predicate, v] }
32
- .select { |k, _v| k.present? }
33
- ]
34
- end
35
-
36
- def previously_changed_relations
25
+ def previously_changed_relations(inverted = nil)
37
26
  serializer_class = RDF::Serializers.serializer_for(self)
38
27
  return {} unless serializer_class.try(:relationships_to_serialize)
39
28
 
40
29
  serializer_class.relationships_to_serialize.select do |key, _value|
41
30
  if respond_to?(key)
42
31
  association_key = key.to_s.ends_with?('_collection') ? send(key).association : key
43
- association_has_destructed?(association_key) || association_changed?(association_key)
32
+ association_has_destructed?(association_key) || association_changed?(association_key, inverted)
44
33
  end
45
34
  end.with_indifferent_access
46
35
  end
47
36
 
48
37
  private
49
38
 
50
- def association_changed?(association) # rubocop:disable Metrics/AbcSize
39
+ def association_changed?(association, inverted) # rubocop:disable Metrics/AbcSize
51
40
  ids_method = "#{association.to_s.singularize}_ids"
52
41
  return true if previous_changes.include?("#{association}_id") || previous_changes.include?(ids_method)
53
42
  return false unless try(:association_cached?, association)
43
+ records = self.class.reflect_on_association(association).collection? ? send(association) : [send(association)]
54
44
 
55
- if self.class.reflect_on_association(association).collection?
56
- send(association).any? { |a| a.previous_changes.present? }
57
- else
58
- send(association)&.previous_changes&.present?
45
+ records.reject { |a| a == inverted }.any? do |a|
46
+ a&.previous_changes.present? || a&.previously_changed_relations(self).present?
59
47
  end
60
48
  end
61
49
 
@@ -16,14 +16,13 @@ module LinkedRails
16
16
 
17
17
  module ClassMethods
18
18
  # Adds an enhancement to a model and includes the Model module.
19
- def enhance(enhancement, opts = {})
19
+ def enhance(enhancement, **opts)
20
20
  initialize_enhancements
21
21
  already_included = enhanced_with?(enhancement)
22
22
 
23
23
  self.enhancements[enhancement] = opts
24
24
  return if already_included
25
25
 
26
- enhance_routing(enhancement) if enhancement.const_defined?(:Routing) && enhanced_with?(enhancement, :Routing)
27
26
  include enhancement::Model if enhancement.const_defined?(:Model) && enhanced_with?(enhancement, :Model)
28
27
  end
29
28
 
@@ -46,10 +45,6 @@ module LinkedRails
46
45
 
47
46
  private
48
47
 
49
- def enhance_routing(enhancement)
50
- LinkedRails::Enhancements::RouteConcerns.add_concern(enhancement)
51
- end
52
-
53
48
  def initialize_enhancements
54
49
  return if enhancements && method(:enhancements).owner == singleton_class
55
50