linked_rails 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/linked_rails/bulk_controller.rb +26 -6
  3. data/app/controllers/linked_rails/enum_values_controller.rb +0 -42
  4. data/app/models/linked_rails/actions/item.rb +46 -30
  5. data/app/models/linked_rails/actions/list.rb +6 -25
  6. data/app/models/linked_rails/collection/configuration.rb +55 -0
  7. data/app/models/linked_rails/collection/filter.rb +1 -1
  8. data/app/models/linked_rails/collection/filter_field.rb +5 -1
  9. data/app/models/linked_rails/collection/filter_option.rb +1 -1
  10. data/app/models/linked_rails/collection/filterable.rb +3 -8
  11. data/app/models/linked_rails/collection/infinite.rb +102 -0
  12. data/app/models/linked_rails/collection/infinite_view.rb +1 -90
  13. data/app/models/linked_rails/collection/iri.rb +33 -52
  14. data/app/models/linked_rails/collection/iri_mapping.rb +14 -6
  15. data/app/models/linked_rails/collection/paginated.rb +45 -0
  16. data/app/models/linked_rails/collection/paginated_view.rb +1 -33
  17. data/app/models/linked_rails/collection/sortable.rb +1 -9
  18. data/app/models/linked_rails/collection/sorting.rb +1 -1
  19. data/app/models/linked_rails/collection/view.rb +15 -5
  20. data/app/models/linked_rails/collection.rb +33 -67
  21. data/app/models/linked_rails/creative_work.rb +1 -1
  22. data/app/models/linked_rails/entry_point.rb +8 -5
  23. data/app/models/linked_rails/enum_value.rb +39 -1
  24. data/app/models/linked_rails/form/field/resource_field.rb +2 -0
  25. data/app/models/linked_rails/form/field.rb +6 -8
  26. data/app/models/linked_rails/form/field_factory.rb +21 -10
  27. data/app/models/linked_rails/form/group.rb +2 -2
  28. data/app/models/linked_rails/form/page.rb +4 -4
  29. data/app/models/linked_rails/form.rb +13 -15
  30. data/app/models/linked_rails/manifest.rb +8 -2
  31. data/app/models/linked_rails/menus/item.rb +1 -1
  32. data/app/models/linked_rails/menus/list.rb +3 -2
  33. data/app/models/linked_rails/ontology/base.rb +1 -1
  34. data/app/models/linked_rails/ontology/class.rb +3 -3
  35. data/app/models/linked_rails/ontology.rb +5 -5
  36. data/app/models/linked_rails/sequence.rb +2 -2
  37. data/app/models/linked_rails/shacl/property_shape.rb +1 -1
  38. data/app/models/linked_rails/widget.rb +1 -1
  39. data/app/serializers/linked_rails/actions/item_serializer.rb +2 -1
  40. data/app/serializers/linked_rails/collection_serializer.rb +7 -2
  41. data/app/serializers/linked_rails/entry_point_serializer.rb +1 -1
  42. data/app/serializers/linked_rails/ontology_serializer.rb +2 -2
  43. data/lib/generators/linked_rails/install/install_generator.rb +5 -8
  44. data/lib/generators/linked_rails/install/templates/README +2 -0
  45. data/lib/generators/linked_rails/install/templates/app_menu_list.rb +36 -7
  46. data/lib/generators/linked_rails/install/templates/application_menu_list.rb +40 -1
  47. data/lib/generators/linked_rails/install/templates/initializer.rb +1 -2
  48. data/lib/generators/linked_rails/install/templates/locales.yml +12 -0
  49. data/lib/generators/linked_rails/install/templates/vocab.rb +1 -0
  50. data/lib/generators/linked_rails/install/templates/vocab.yml +2 -2
  51. data/lib/generators/linked_rails/model/model_generator.rb +0 -1
  52. data/lib/generators/linked_rails/model/templates/controller.rb.tt +5 -1
  53. data/lib/generators/linked_rails/model/templates/form.rb.tt +3 -0
  54. data/lib/generators/linked_rails/model/templates/menu_list.rb.tt +15 -0
  55. data/lib/generators/linked_rails/model/templates/policy.rb.tt +13 -0
  56. data/lib/generators/linked_rails/model/templates/serializer.rb.tt +1 -1
  57. data/lib/linked_rails/active_response/controller/collections.rb +1 -1
  58. data/lib/linked_rails/active_response/controller/crud_defaults.rb +3 -3
  59. data/lib/linked_rails/active_response/controller/params.rb +5 -5
  60. data/lib/linked_rails/active_response/controller.rb +11 -0
  61. data/lib/linked_rails/active_response/responders/rdf.rb +19 -10
  62. data/lib/linked_rails/callable_variable.rb +1 -1
  63. data/lib/linked_rails/collection_params_parser.rb +93 -0
  64. data/lib/linked_rails/controller/actionable.rb +118 -0
  65. data/lib/linked_rails/controller/authorization.rb +6 -0
  66. data/lib/linked_rails/controller/default_actions/create.rb +52 -0
  67. data/lib/linked_rails/controller/default_actions/destroy.rb +42 -0
  68. data/lib/linked_rails/controller/default_actions/update.rb +43 -0
  69. data/lib/linked_rails/controller.rb +20 -3
  70. data/lib/linked_rails/enhancements/creatable/controller.rb +1 -1
  71. data/lib/linked_rails/enhancements/destroyable/controller.rb +1 -1
  72. data/lib/linked_rails/enhancements/updatable/controller.rb +1 -1
  73. data/lib/linked_rails/enhancements.rb +0 -16
  74. data/lib/linked_rails/helpers/delta_helper.rb +26 -3
  75. data/lib/linked_rails/helpers/ontola_actions_helper.rb +2 -2
  76. data/lib/linked_rails/helpers/resource_helper.rb +1 -1
  77. data/lib/linked_rails/iri_mapper.rb +17 -39
  78. data/lib/linked_rails/middleware/linked_data_params.rb +7 -127
  79. data/lib/linked_rails/model/actionable.rb +69 -0
  80. data/lib/linked_rails/model/collections.rb +195 -39
  81. data/lib/linked_rails/model/dirty.rb +1 -1
  82. data/lib/linked_rails/model/enhancements.rb +1 -6
  83. data/lib/linked_rails/model/filtering.rb +2 -4
  84. data/lib/linked_rails/model/indexable.rb +5 -15
  85. data/lib/linked_rails/model/iri.rb +15 -17
  86. data/lib/linked_rails/model/iri_mapping.rb +35 -6
  87. data/lib/linked_rails/model/menuable.rb +34 -0
  88. data/lib/linked_rails/model/serialization.rb +0 -9
  89. data/lib/linked_rails/model/singularable.rb +55 -0
  90. data/lib/linked_rails/model/sorting.rb +0 -5
  91. data/lib/linked_rails/model/tables.rb +26 -0
  92. data/lib/linked_rails/model.rb +13 -5
  93. data/lib/linked_rails/params_parser.rb +131 -55
  94. data/lib/linked_rails/policy/attribute_conditions.rb +2 -2
  95. data/lib/linked_rails/policy.rb +24 -17
  96. data/lib/linked_rails/rdf_error.rb +2 -2
  97. data/lib/linked_rails/routes.rb +37 -22
  98. data/lib/linked_rails/serializer/actionable.rb +27 -0
  99. data/lib/linked_rails/serializer/menuable.rb +31 -0
  100. data/lib/linked_rails/serializer/singularable.rb +26 -0
  101. data/lib/linked_rails/serializer.rb +23 -10
  102. data/lib/linked_rails/test_methods.rb +114 -0
  103. data/lib/linked_rails/translate.rb +19 -9
  104. data/lib/linked_rails/uri_template.rb +30 -0
  105. data/lib/linked_rails/version.rb +1 -1
  106. data/lib/linked_rails/vocab.rb +8 -0
  107. data/lib/linked_rails.rb +25 -12
  108. data/lib/rails/welcome_controller.rb +3 -3
  109. data/lib/rdf/query_fix.rb +15 -0
  110. metadata +22 -25
  111. data/app/models/linked_rails/actions/default_actions/create.rb +0 -60
  112. data/app/models/linked_rails/actions/default_actions/destroy.rb +0 -45
  113. data/app/models/linked_rails/actions/default_actions/update.rb +0 -50
  114. data/app/models/linked_rails/actions/default_actions.rb +0 -17
  115. data/lib/generators/linked_rails/install/templates/application_action_list.rb +0 -3
  116. data/lib/generators/linked_rails/model/templates/action_list.rb.tt +0 -6
  117. data/lib/linked_rails/enhancements/actionable/model.rb +0 -71
  118. data/lib/linked_rails/enhancements/actionable/serializer.rb +0 -25
  119. data/lib/linked_rails/enhancements/creatable/action.rb +0 -15
  120. data/lib/linked_rails/enhancements/destroyable/action.rb +0 -15
  121. data/lib/linked_rails/enhancements/destroyable/routing.rb +0 -19
  122. data/lib/linked_rails/enhancements/indexable/model.rb +0 -10
  123. data/lib/linked_rails/enhancements/menuable/model.rb +0 -36
  124. data/lib/linked_rails/enhancements/menuable/serializer.rb +0 -33
  125. data/lib/linked_rails/enhancements/route_concerns.rb +0 -56
  126. data/lib/linked_rails/enhancements/singularable/controller.rb +0 -43
  127. data/lib/linked_rails/enhancements/singularable/model.rb +0 -47
  128. data/lib/linked_rails/enhancements/singularable/serializer.rb +0 -28
  129. data/lib/linked_rails/enhancements/tableable/model.rb +0 -28
  130. data/lib/linked_rails/enhancements/updatable/action.rb +0 -15
  131. data/lib/linked_rails/enhancements/updatable/routing.rb +0 -20
@@ -41,7 +41,10 @@ module LinkedRails
41
41
  end
42
42
 
43
43
  def changes_triples # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
44
- serializer = RDF::Serializers.serializer_for(current_resource).new(current_resource)
44
+ serializer_class = RDF::Serializers.serializer_for(current_resource)
45
+ return [] if serializer_class.blank?
46
+
47
+ serializer = serializer_class.new(current_resource)
45
48
 
46
49
  current_resource.previous_changes_by_predicate.map do |predicate, (_old_value, _new_value)|
47
50
  serializer_attributes = current_resource.class.predicate_mapping[predicate]
@@ -75,11 +78,31 @@ module LinkedRails
75
78
  end
76
79
 
77
80
  def resource_added_delta(resource)
78
- invalidate_parent_collections_delta(resource)
81
+ invalidate_parent_collections_delta(resource) + singular_added_delta(resource)
79
82
  end
80
83
 
81
84
  def resource_removed_delta(resource)
82
- invalidate_parent_collections_delta(resource)
85
+ invalidate_parent_collections_delta(resource) + singular_removed_delta(resource)
86
+ end
87
+
88
+ def same_as_statement(from, to)
89
+ [
90
+ from,
91
+ Vocab.owl.sameAs,
92
+ to
93
+ ]
94
+ end
95
+
96
+ def singular_added_delta(resource)
97
+ return [] unless resource.try(:singular_resource?)
98
+
99
+ [same_as_statement(resource.singular_iri, resource.iri)]
100
+ end
101
+
102
+ def singular_removed_delta(resource)
103
+ return [] unless resource.try(:singular_resource?)
104
+
105
+ [invalidate_resource_delta(resource.singular_iri)]
83
106
  end
84
107
  end
85
108
  end
@@ -12,8 +12,8 @@ module LinkedRails
12
12
  Vocab.libro["actions/copyToClipboard?#{{value: value}.to_param}"]
13
13
  end
14
14
 
15
- def ontola_dialog_action(resource, opener: nil)
16
- Vocab.libro["actions/dialog/alert?#{{resource: resource, opener: opener}.compact.to_param}"]
15
+ def ontola_dialog_action(resource, opener: nil, size: nil)
16
+ Vocab.libro["actions/dialog/alert?#{{resource: resource, opener: opener, size: size}.compact.to_param}"]
17
17
  end
18
18
 
19
19
  def ontola_dialog_close_action
@@ -59,7 +59,7 @@ module LinkedRails
59
59
 
60
60
  def resolve_current_resource
61
61
  case action_name
62
- when 'create', 'new'
62
+ when 'create'
63
63
  new_resource
64
64
  else
65
65
  requested_resource
@@ -12,6 +12,7 @@ module LinkedRails
12
12
  # => {
13
13
  # action: 'show',
14
14
  # class: Resource,
15
+ # iri: 'https://example.com/resource/1?foo=bar',
15
16
  # params: {
16
17
  # id: '1',
17
18
  # foo: 'bar'
@@ -22,6 +23,7 @@ module LinkedRails
22
23
  # => {
23
24
  # action: 'index',
24
25
  # class: Resource,
26
+ # iri: 'https://example.com/resources?foo=bar',
25
27
  # params: {
26
28
  # foo: 'bar'
27
29
  # }
@@ -35,38 +37,21 @@ module LinkedRails
35
37
  query = Rack::Utils.parse_nested_query(URI(iri.to_s).query)
36
38
  params = Rails.application.routes.recognize_path(iri.to_s, method: method)
37
39
 
38
- route_params_to_opts(params.merge(query))
40
+ route_params_to_opts(params.merge(query), iri.to_s)
39
41
  rescue ActionController::RoutingError
40
42
  EMPTY_IRI_OPTS.dup
41
43
  end
42
44
 
43
- def index_resource_from_iri(iri, user_context)
44
- ensure_absolute_iri!(iri)
45
-
46
- opts = opts_from_iri(iri)
47
- index_resource_from_opts(opts, user_context)
48
- end
49
-
50
- def index_resource_from_iri!(iri, user_context)
51
- index_resource_from_iri(iri, user_context) || raise(ActiveRecord::RecordNotFound)
52
- end
53
-
54
- def index_resource_from_opts(opts, user_context)
55
- opts[:class]&.requested_index_resource(opts[:params], user_context) if opts[:action] == 'index'
56
- end
57
-
58
45
  def parent_from_params(params, user_context)
59
46
  return unless params.key?(:parent_iri)
60
47
 
61
- parent_iri = "/#{params[:parent_iri]}"
62
- opts = LinkedRails.iri_mapper.opts_from_iri(parent_iri)
63
- opts[:params] = params.except(:parent_iri, :singular_route).merge(opts[:params])
48
+ parent_iri = LinkedRails.iri(path: "/#{params[:parent_iri]}")
64
49
 
65
- LinkedRails.iri_mapper.resource_from_opts(opts, user_context)
50
+ LinkedRails.iri_mapper.resource_from_iri(parent_iri, user_context)
66
51
  end
67
52
 
68
53
  def resource_from_iri(iri, user_context)
69
- ensure_absolute_iri!(iri)
54
+ return nil unless absolute_iri?(iri)
70
55
 
71
56
  opts = opts_from_iri(iri)
72
57
  resource_from_opts(opts, user_context)
@@ -80,7 +65,7 @@ module LinkedRails
80
65
  opts[:class]&.requested_resource(opts, user_context)
81
66
  end
82
67
 
83
- def route_params_to_opts(params)
68
+ def route_params_to_opts(params, iri)
84
69
  controller_class = class_for_controller(params[:controller])
85
70
 
86
71
  return EMPTY_IRI_OPTS.dup if controller_class.blank?
@@ -88,27 +73,17 @@ module LinkedRails
88
73
  {
89
74
  action: params[:action],
90
75
  class: controller_class,
91
- params: params.except(:action, :controller)
76
+ iri: iri,
77
+ params: sanitized_route_params(controller_class, params)
92
78
  }.with_indifferent_access
93
79
  end
94
80
 
95
- def single_resource_from_iri(iri, user_context)
96
- ensure_absolute_iri!(iri)
97
-
98
- opts = opts_from_iri(iri)
99
- single_resource_from_opts(opts, user_context)
100
- end
101
-
102
- def single_resource_from_iri!(iri, user_context)
103
- single_resource_from_iri(iri, user_context) || raise(ActiveRecord::RecordNotFound)
104
- end
81
+ private
105
82
 
106
- def single_resource_from_opts(opts, user_context)
107
- opts[:class]&.requested_single_resource(opts[:params], user_context) unless opts[:params] == 'index'
83
+ def absolute_iri?(iri)
84
+ iri.present? && !URI(iri).relative?
108
85
  end
109
86
 
110
- private
111
-
112
87
  def class_for_controller(controller)
113
88
  return if controller.blank?
114
89
 
@@ -117,8 +92,11 @@ module LinkedRails
117
92
  .try(:controller_class)
118
93
  end
119
94
 
120
- def ensure_absolute_iri!(iri)
121
- raise("An absolute url is expected. #{iri} is given.") if iri.blank? || URI(iri).relative?
95
+ def sanitized_route_params(controller_class, params)
96
+ new_params = params.except(:action, :controller)
97
+ nested_key = :"#{controller_class.name.tableize.singularize}_id"
98
+ new_params[:id] ||= new_params.delete(nested_key) if new_params.key?(nested_key)
99
+ new_params
122
100
  end
123
101
  end
124
102
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module LinkedRails
4
4
  module Middleware
5
- class LinkedDataParams # rubocop:disable Metrics/ClassLength
5
+ class LinkedDataParams
6
6
  def initialize(app)
7
7
  @app = app
8
8
  end
@@ -17,33 +17,8 @@ module LinkedRails
17
17
 
18
18
  private
19
19
 
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/'])
20
+ def add_param_from_query(data, target_class, key, value)
21
+ data[target_class.predicate_mapping[RDF::URI(key)].key] = value
47
22
  end
48
23
 
49
24
  def convert_query_params(request, target_class)
@@ -56,17 +31,6 @@ module LinkedRails
56
31
  request.update_param(class_key, data) if data.present?
57
32
  end
58
33
 
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
34
  def graph_from_request(request)
71
35
  request_graph = request.delete_param("<#{Vocab.ll[:graph].value}>")
72
36
  return if request_graph.blank?
@@ -79,33 +43,6 @@ module LinkedRails
79
43
  )
80
44
  end
81
45
 
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
90
-
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]
100
- end
101
-
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
46
  # Converts a serialized graph from a multipart request body to a nested
110
47
  # attributes hash.
111
48
  #
@@ -135,72 +72,13 @@ module LinkedRails
135
72
  convert_query_params(request, target_class)
136
73
  end
137
74
 
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
75
  def target_class_from_path(request) # rubocop:disable Metrics/AbcSize
198
76
  opts = LinkedRails.iri_mapper.opts_from_iri(
199
77
  request.base_url + request.env['REQUEST_URI'],
200
78
  method: request.request_method
201
79
  )
202
80
 
203
- logger.info("No class found for #{request.base_url + request.env['REQUEST_URI']}") unless opts[:class]
81
+ Rails.logger.info("No class found for #{request.base_url + request.env['REQUEST_URI']}") unless opts[:class]
204
82
 
205
83
  opts[:class]
206
84
  end
@@ -215,7 +93,9 @@ module LinkedRails
215
93
 
216
94
  def update_target_params(request, graph, target_class)
217
95
  key = target_class.to_s.demodulize.underscore
218
- from_body = parse_resource(request.params, graph, Vocab.ll[:targetResource], target_class)
96
+
97
+ parser = ParamsParser.new(graph: graph, params: request.params)
98
+ from_body = parser.parse_resource(Vocab.ll[:targetResource], target_class)
219
99
 
220
100
  request.update_param(key, from_body.merge(request.params[key] || {}))
221
101
  end
@@ -0,0 +1,69 @@
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
+ return @action_list if @action_list.try(:actionable_class) == self
49
+
50
+ @action_list = defined_action_list || define_action_list
51
+ end
52
+
53
+ private
54
+
55
+ def action_superclass
56
+ superclass.try(:action_list) || LinkedRails.action_list_parent_class
57
+ end
58
+
59
+ def defined_action_list
60
+ 'ActionList'.safe_constantize
61
+ end
62
+
63
+ def define_action_list
64
+ const_set('ActionList', Class.new(action_superclass))
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -5,8 +5,175 @@ 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
+ # collection_class [Class] The base class of the collection.
41
+ # If you want to use a class other than LinkedRails.collection_class.
42
+ collection_class: nil,
43
+ # collection_class [Hash] The default filters applied to the collection.
44
+ default_filters: {},
45
+ # collection_class [Array<Hash>] The default sortings applied to the collection.
46
+ default_sortings: [{key: Vocab.schema.dateCreated, direction: :desc}],
47
+ # iri_template_keys [Array<Sym>] Custom query keys for the iri template
48
+ iri_template_keys: [],
49
+ # joins [Array<Sym>, Sym] The associations to join
50
+ joins: nil,
51
+ # parent [Instance] The default parent of a collection.
52
+ parent: nil,
53
+ # parent_iri [Array<String>] The iri elements of the parent
54
+ parent_iri: -> { parent&.iri_elements },
55
+ # part_of [Instance] The record to serialize as isPartOf
56
+ part_of: -> { parent },
57
+ # policy_scope [Scope] The policy scope class to be used for scoping
58
+ # Set to false to skip scoping
59
+ policy_scope: -> { policy ? policy::Scope : Pundit::PolicyFinder.new(filtered_association).scope! },
60
+ # route_key [Symbol, String] The route key for the association
61
+ route_key: nil
62
+ }.freeze
63
+ COLLECTION_OPTIONS = COLLECTION_CUSTOMIZABLE_OPTIONS.merge(COLLECTION_STATIC_OPTIONS)
64
+
65
+ module ClassMethods
66
+ def collection_iri(**opts)
67
+ LinkedRails.iri(path: collection_root_relative_iri(**opts))
68
+ end
69
+
70
+ # Sets the defaults for all collections for this class.
71
+ # Can be overridden by #with_collection, called from associated models,
72
+ # or by passing parameters in an iri.
73
+ # @param [Hash] options
74
+ def collection_options(**options)
75
+ initialize_default_collection_opts
76
+
77
+ options.each do |key, value|
78
+ raise("Invalid key passed to collection_options: #{key}") unless valid_collection_option?(key)
79
+
80
+ _default_collection_opts[key] = value
81
+ end
82
+ _default_collection_opts[:iri_template] = LinkedRails.collection_class.generate_iri_template(
83
+ _default_collection_opts[:iri_template_keys]
84
+ )
85
+ _default_collection_opts
86
+ end
87
+
88
+ def collection_root_relative_iri(**opts)
89
+ opts[:filter] = LinkedRails.collection_class.filter_iri_opts(opts[:filter]) if opts.key?(:filter)
90
+ opts[:route_key] = collection_route_key
91
+ default_collection_option(:iri_template).expand(**opts)
92
+ end
93
+
94
+ def collection_route_key
95
+ default_collection_option(:route_key) || route_key
96
+ end
97
+
98
+ def default_collection_options
99
+ initialize_default_collection_opts
100
+
101
+ _default_collection_opts
102
+ end
103
+
104
+ def default_collection_option(key)
105
+ default_collection_options[key]
106
+ end
107
+
108
+ # Defines a collection to be used in {collection_for}
109
+ # @see Ldable#collection_for
110
+ # @note Adds a instance_method <name>_collection
111
+ # @param [Hash] name as to be used in {collection_for}
112
+ # @param [Hash] options See COLLECTION_OPTIONS
113
+ # @return [Collection]
114
+ def with_collection(name, **options) # rubocop:disable Metrics/AbcSize
115
+ options[:association] ||= name.to_sym
116
+ options[:association_class] ||= name.to_s.classify.constantize
117
+ merged_options = options[:association_class].default_collection_options.merge(options)
118
+ merged_options[:iri_template] = LinkedRails.collection_class.generate_iri_template(
119
+ merged_options[:iri_template_keys]
120
+ )
121
+ collections_add(name: name, options: merged_options)
122
+
123
+ define_method "#{name.to_s.singularize}_collection" do |opts = {}|
124
+ collection_for(name, **opts)
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def collections_add(opts)
131
+ initialize_collections
132
+ collections.delete_if { |c| c[:name] == opts[:name] }
133
+ opts[:options] = sanitized_collection_options(opts[:options])
134
+ collections.append(opts)
135
+ end
136
+
137
+ def initialize_collections
138
+ return if collections && method(:collections).owner == singleton_class
139
+
140
+ self.collections = superclass.try(:collections)&.dup || []
141
+ end
142
+
143
+ def initialize_default_collection_opts # rubocop:disable Metrics/AbcSize
144
+ return if _default_collection_opts && method(:_default_collection_opts).owner == singleton_class
145
+
146
+ self._default_collection_opts = (superclass.try(:_default_collection_opts) || COLLECTION_OPTIONS).dup
147
+
148
+ _default_collection_opts[:collection_class] ||= LinkedRails.collection_class
149
+ _default_collection_opts[:association_class] = self
150
+ _default_collection_opts[:iri_template] = LinkedRails.collection_class.generate_iri_template(
151
+ _default_collection_opts[:iri_template_keys]
152
+ )
153
+
154
+ _default_collection_opts
155
+ end
156
+
157
+ def sanitized_collection_options(opts)
158
+ opts.each_with_object(HashWithIndifferentAccess.new) do |(key, value), hash|
159
+ raise("Invalid key passed to with_collection: #{key}") unless valid_collection_option?(key.to_sym)
160
+
161
+ hash_key = COLLECTION_CUSTOMIZABLE_OPTIONS.key?(key.to_sym) ? "default_#{key}" : key
162
+
163
+ hash[hash_key] = value
164
+ end
165
+ end
166
+
167
+ def valid_collection_option?(key)
168
+ COLLECTION_OPTIONS.key?(key) || key == :iri_template
169
+ end
170
+ end
171
+
8
172
  included do
9
173
  class_attribute :collections
174
+ class_attribute :_default_collection_opts,
175
+ instance_accessor: false,
176
+ instance_predicate: false
10
177
  end
11
178
 
12
179
  # Initialises a {Collection} for one of the collections defined by {has_collection}
@@ -18,19 +185,42 @@ module LinkedRails
18
185
  # @param [ApplicationRecord] part_of
19
186
  # @param [Hash] opts Additional options to be passed to the collection.
20
187
  # @return [Collection]
21
- def collection_for(name, instance_opts = {})
22
- collection_opts = collections.detect { |c| c[:name] == name }.try(:[], :options).dup
188
+ def collection_for(name, **instance_opts)
189
+ collection_opts = collection_options_for(name).dup
23
190
  return if collection_opts.blank?
24
191
 
25
192
  collection_opts[:name] = name
26
193
  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
194
+ collection_class =
195
+ collection_opts.delete(:collection_class) ||
196
+ collection_opts[:association_class].default_collection_option(:collection_class) ||
197
+ LinkedRails.collection_class
29
198
  collection_class.collection_or_view(collection_opts, instance_opts)
30
199
  end
31
200
 
201
+ def collection_iri(collection, **opts)
202
+ LinkedRails.iri(path: collection_root_relative_iri(collection, **opts))
203
+ end
204
+
205
+ def collection_options_for(name)
206
+ opts = collections.detect { |c| c[:name] == name.to_sym }
207
+ raise("Collection #{name} not found for #{self}") unless opts
208
+
209
+ opts[:options] || {}
210
+ end
211
+
212
+ def collection_root_relative_iri(collection, **opts)
213
+ collection_opts = collection_options_for(collection).dup
214
+ template = collection_opts[:iri_template]
215
+ klass = collection_opts[:association_class]
216
+ opts[:route_key] = collection_opts[:route_key] || klass.collection_route_key
217
+ opts[:parent_iri] = iri_elements
218
+
219
+ template.expand(**opts).to_s
220
+ end
221
+
32
222
  def parent_collections(user_context)
33
- return [] if try(:parent).try(:collections).blank?
223
+ return [self.class.root_collection(user_context: user_context)] if try(:parent).try(:collections).blank?
34
224
 
35
225
  parent_collections_for(parent, user_context)
36
226
  end
@@ -43,40 +233,6 @@ module LinkedRails
43
233
  .select { |collection| is_a?(collection[:options][:association_class]) }
44
234
  .map { |collection| parent.collection_for(collection[:name], user_context: user_context) }
45
235
  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
236
  end
81
237
  end
82
238
  end