linked_rails 0.0.3 → 0.0.4

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