linked_rails 0.0.1

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 (207) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +65 -0
  4. data/Rakefile +34 -0
  5. data/app/controllers/linked_rails/actions/items_controller.rb +9 -0
  6. data/app/controllers/linked_rails/bulk_controller.rb +195 -0
  7. data/app/controllers/linked_rails/current_user_controller.rb +13 -0
  8. data/app/controllers/linked_rails/enum_values_controller.rb +49 -0
  9. data/app/controllers/linked_rails/forms_controller.rb +13 -0
  10. data/app/controllers/linked_rails/manifests_controller.rb +21 -0
  11. data/app/controllers/linked_rails/menus/items_controller.rb +9 -0
  12. data/app/controllers/linked_rails/menus/lists_controller.rb +9 -0
  13. data/app/controllers/linked_rails/not_found_controller.rb +15 -0
  14. data/app/controllers/linked_rails/ontologies_controller.rb +7 -0
  15. data/app/models/linked_rails/actions/default_actions/create.rb +60 -0
  16. data/app/models/linked_rails/actions/default_actions/destroy.rb +45 -0
  17. data/app/models/linked_rails/actions/default_actions/update.rb +50 -0
  18. data/app/models/linked_rails/actions/default_actions.rb +17 -0
  19. data/app/models/linked_rails/actions/item.rb +234 -0
  20. data/app/models/linked_rails/actions/list.rb +113 -0
  21. data/app/models/linked_rails/collection/filter.rb +16 -0
  22. data/app/models/linked_rails/collection/filter_field.rb +30 -0
  23. data/app/models/linked_rails/collection/filter_option.rb +17 -0
  24. data/app/models/linked_rails/collection/filterable.rb +92 -0
  25. data/app/models/linked_rails/collection/infinite_view.rb +98 -0
  26. data/app/models/linked_rails/collection/iri.rb +74 -0
  27. data/app/models/linked_rails/collection/iri_mapping.rb +33 -0
  28. data/app/models/linked_rails/collection/paginated_view.rb +41 -0
  29. data/app/models/linked_rails/collection/sortable.rb +60 -0
  30. data/app/models/linked_rails/collection/sorting.rb +72 -0
  31. data/app/models/linked_rails/collection/view.rb +101 -0
  32. data/app/models/linked_rails/collection.rb +220 -0
  33. data/app/models/linked_rails/condition.rb +7 -0
  34. data/app/models/linked_rails/creative_work.rb +21 -0
  35. data/app/models/linked_rails/current_user.rb +28 -0
  36. data/app/models/linked_rails/entry_point.rb +53 -0
  37. data/app/models/linked_rails/enum_value.rb +33 -0
  38. data/app/models/linked_rails/form/field/association_input.rb +23 -0
  39. data/app/models/linked_rails/form/field/checkbox_group.rb +10 -0
  40. data/app/models/linked_rails/form/field/checkbox_input.rb +10 -0
  41. data/app/models/linked_rails/form/field/color_input.rb +17 -0
  42. data/app/models/linked_rails/form/field/date_input.rb +10 -0
  43. data/app/models/linked_rails/form/field/date_time_input.rb +10 -0
  44. data/app/models/linked_rails/form/field/email_input.rb +10 -0
  45. data/app/models/linked_rails/form/field/file_input.rb +10 -0
  46. data/app/models/linked_rails/form/field/location_input.rb +11 -0
  47. data/app/models/linked_rails/form/field/markdown_input.rb +10 -0
  48. data/app/models/linked_rails/form/field/number_input.rb +10 -0
  49. data/app/models/linked_rails/form/field/password_input.rb +10 -0
  50. data/app/models/linked_rails/form/field/postal_range_input.rb +10 -0
  51. data/app/models/linked_rails/form/field/radio_group.rb +10 -0
  52. data/app/models/linked_rails/form/field/resource_field.rb +23 -0
  53. data/app/models/linked_rails/form/field/select_input.rb +11 -0
  54. data/app/models/linked_rails/form/field/slider_input.rb +10 -0
  55. data/app/models/linked_rails/form/field/text_area_input.rb +10 -0
  56. data/app/models/linked_rails/form/field/text_input.rb +10 -0
  57. data/app/models/linked_rails/form/field/toggle_button_group.rb +10 -0
  58. data/app/models/linked_rails/form/field.rb +117 -0
  59. data/app/models/linked_rails/form/field_factory.rb +219 -0
  60. data/app/models/linked_rails/form/group.rb +39 -0
  61. data/app/models/linked_rails/form/page.rb +31 -0
  62. data/app/models/linked_rails/form.rb +156 -0
  63. data/app/models/linked_rails/manifest.rb +102 -0
  64. data/app/models/linked_rails/media_object.rb +31 -0
  65. data/app/models/linked_rails/menus/item.rb +92 -0
  66. data/app/models/linked_rails/menus/list.rb +138 -0
  67. data/app/models/linked_rails/ontology/base.rb +50 -0
  68. data/app/models/linked_rails/ontology/class.rb +43 -0
  69. data/app/models/linked_rails/ontology/property.rb +19 -0
  70. data/app/models/linked_rails/ontology.rb +34 -0
  71. data/app/models/linked_rails/property_query.rb +11 -0
  72. data/app/models/linked_rails/resource.rb +17 -0
  73. data/app/models/linked_rails/sequence.rb +64 -0
  74. data/app/models/linked_rails/shacl/node_shape.rb +21 -0
  75. data/app/models/linked_rails/shacl/property_shape.rb +53 -0
  76. data/app/models/linked_rails/shacl/shape.rb +33 -0
  77. data/app/models/linked_rails/web_page.rb +22 -0
  78. data/app/models/linked_rails/web_site.rb +17 -0
  79. data/app/models/linked_rails/widget.rb +55 -0
  80. data/app/policies/linked_rails/actions/item_policy.rb +11 -0
  81. data/app/policies/linked_rails/actions/list_policy.rb +11 -0
  82. data/app/policies/linked_rails/collection/view_policy.rb +13 -0
  83. data/app/policies/linked_rails/collection_policy.rb +41 -0
  84. data/app/policies/linked_rails/enum_value_policy.rb +32 -0
  85. data/app/policies/linked_rails/menus/item_policy.rb +11 -0
  86. data/app/policies/linked_rails/menus/list_policy.rb +11 -0
  87. data/app/policies/linked_rails/sequence_policy.rb +9 -0
  88. data/app/serializers/linked_rails/actions/item_serializer.rb +28 -0
  89. data/app/serializers/linked_rails/collection/filter_field_serializer.rb +12 -0
  90. data/app/serializers/linked_rails/collection/filter_option_serializer.rb +12 -0
  91. data/app/serializers/linked_rails/collection/filter_serializer.rb +13 -0
  92. data/app/serializers/linked_rails/collection/sorting_serializer.rb +13 -0
  93. data/app/serializers/linked_rails/collection/view_serializer.rb +22 -0
  94. data/app/serializers/linked_rails/collection_serializer.rb +44 -0
  95. data/app/serializers/linked_rails/condition_serializer.rb +9 -0
  96. data/app/serializers/linked_rails/creative_work_serializer.rb +10 -0
  97. data/app/serializers/linked_rails/current_user_serializer.rb +7 -0
  98. data/app/serializers/linked_rails/entry_point_serializer.rb +20 -0
  99. data/app/serializers/linked_rails/enum_value_serializer.rb +12 -0
  100. data/app/serializers/linked_rails/form/field/association_input_serializer.rb +13 -0
  101. data/app/serializers/linked_rails/form/field/resource_field_serializer.rb +11 -0
  102. data/app/serializers/linked_rails/form/field/select_input_serializer.rb +11 -0
  103. data/app/serializers/linked_rails/form/field_serializer.rb +39 -0
  104. data/app/serializers/linked_rails/form/group_serializer.rb +14 -0
  105. data/app/serializers/linked_rails/form/page_serializer.rb +13 -0
  106. data/app/serializers/linked_rails/form_serializer.rb +9 -0
  107. data/app/serializers/linked_rails/media_object_serializer.rb +17 -0
  108. data/app/serializers/linked_rails/menus/item_serializer.rb +35 -0
  109. data/app/serializers/linked_rails/menus/list_serializer.rb +13 -0
  110. data/app/serializers/linked_rails/ontology/class_serializer.rb +19 -0
  111. data/app/serializers/linked_rails/ontology/property_serializer.rb +16 -0
  112. data/app/serializers/linked_rails/ontology_serializer.rb +8 -0
  113. data/app/serializers/linked_rails/rdf_error_serializer.rb +8 -0
  114. data/app/serializers/linked_rails/sequence_serializer.rb +14 -0
  115. data/app/serializers/linked_rails/shacl/node_shape_serializer.rb +12 -0
  116. data/app/serializers/linked_rails/shacl/property_shape_serializer.rb +38 -0
  117. data/app/serializers/linked_rails/shacl/shape_serializer.rb +25 -0
  118. data/app/serializers/linked_rails/web_page_serializer.rb +10 -0
  119. data/app/serializers/linked_rails/web_site_serializer.rb +11 -0
  120. data/app/serializers/linked_rails/widget_serializer.rb +15 -0
  121. data/config/initializers/inflections.rb +5 -0
  122. data/lib/generators/linked_rails/install/install_generator.rb +65 -0
  123. data/lib/generators/linked_rails/install/templates/app_menu_list.rb +41 -0
  124. data/lib/generators/linked_rails/install/templates/application_action_list.rb +3 -0
  125. data/lib/generators/linked_rails/install/templates/application_form.rb +3 -0
  126. data/lib/generators/linked_rails/install/templates/application_menu_list.rb +3 -0
  127. data/lib/generators/linked_rails/install/templates/application_policy.rb +18 -0
  128. data/lib/generators/linked_rails/install/templates/application_serializer.rb +5 -0
  129. data/lib/generators/linked_rails/install/templates/initializer.rb +9 -0
  130. data/lib/generators/linked_rails/install/templates/locales.yml +12 -0
  131. data/lib/generators/linked_rails/install/templates/rdf_responder.rb +5 -0
  132. data/lib/generators/linked_rails/install/templates/rdf_serializers_initializer.rb +5 -0
  133. data/lib/generators/linked_rails/install/templates/vocab.rb +5 -0
  134. data/lib/generators/linked_rails/install/templates/vocab.yml +26 -0
  135. data/lib/generators/linked_rails/model/model_generator.rb +58 -0
  136. data/lib/generators/linked_rails/model/templates/action_list.rb.tt +6 -0
  137. data/lib/generators/linked_rails/model/templates/controller.rb.tt +7 -0
  138. data/lib/generators/linked_rails/model/templates/create_table_migration.rb.tt +24 -0
  139. data/lib/generators/linked_rails/model/templates/form.rb.tt +6 -0
  140. data/lib/generators/linked_rails/model/templates/menu_list.rb.tt +6 -0
  141. data/lib/generators/linked_rails/model/templates/model.rb.tt +15 -0
  142. data/lib/generators/linked_rails/model/templates/module.rb.tt +7 -0
  143. data/lib/generators/linked_rails/model/templates/policy.rb.tt +6 -0
  144. data/lib/generators/linked_rails/model/templates/serializer.rb.tt +9 -0
  145. data/lib/linked_rails/active_response/controller/collections.rb +43 -0
  146. data/lib/linked_rails/active_response/controller/crud_defaults.rb +92 -0
  147. data/lib/linked_rails/active_response/controller/params.rb +51 -0
  148. data/lib/linked_rails/active_response/controller.rb +37 -0
  149. data/lib/linked_rails/active_response/responders/rdf.rb +158 -0
  150. data/lib/linked_rails/cache.rb +35 -0
  151. data/lib/linked_rails/callable_variable.rb +25 -0
  152. data/lib/linked_rails/constraints/whitelist.rb +36 -0
  153. data/lib/linked_rails/controller/authorization.rb +15 -0
  154. data/lib/linked_rails/controller/error_handling.rb +76 -0
  155. data/lib/linked_rails/controller.rb +43 -0
  156. data/lib/linked_rails/engine.rb +7 -0
  157. data/lib/linked_rails/enhanceable.rb +21 -0
  158. data/lib/linked_rails/enhancements/actionable/model.rb +71 -0
  159. data/lib/linked_rails/enhancements/actionable/serializer.rb +25 -0
  160. data/lib/linked_rails/enhancements/creatable/action.rb +15 -0
  161. data/lib/linked_rails/enhancements/creatable/controller.rb +15 -0
  162. data/lib/linked_rails/enhancements/destroyable/action.rb +15 -0
  163. data/lib/linked_rails/enhancements/destroyable/controller.rb +15 -0
  164. data/lib/linked_rails/enhancements/destroyable/routing.rb +19 -0
  165. data/lib/linked_rails/enhancements/indexable/model.rb +10 -0
  166. data/lib/linked_rails/enhancements/menuable/model.rb +36 -0
  167. data/lib/linked_rails/enhancements/menuable/serializer.rb +33 -0
  168. data/lib/linked_rails/enhancements/route_concerns.rb +56 -0
  169. data/lib/linked_rails/enhancements/singularable/controller.rb +43 -0
  170. data/lib/linked_rails/enhancements/singularable/model.rb +47 -0
  171. data/lib/linked_rails/enhancements/singularable/serializer.rb +28 -0
  172. data/lib/linked_rails/enhancements/tableable/model.rb +28 -0
  173. data/lib/linked_rails/enhancements/updatable/action.rb +15 -0
  174. data/lib/linked_rails/enhancements/updatable/controller.rb +15 -0
  175. data/lib/linked_rails/enhancements/updatable/routing.rb +20 -0
  176. data/lib/linked_rails/enhancements/updatable/serializer.rb +17 -0
  177. data/lib/linked_rails/enhancements.rb +22 -0
  178. data/lib/linked_rails/helpers/delta_helper.rb +86 -0
  179. data/lib/linked_rails/helpers/ontola_actions_helper.rb +32 -0
  180. data/lib/linked_rails/helpers/resource_helper.rb +70 -0
  181. data/lib/linked_rails/iri_mapper.rb +125 -0
  182. data/lib/linked_rails/middleware/linked_data_params.rb +224 -0
  183. data/lib/linked_rails/model/collections.rb +82 -0
  184. data/lib/linked_rails/model/dirty.rb +82 -0
  185. data/lib/linked_rails/model/enhancements.rb +61 -0
  186. data/lib/linked_rails/model/filtering.rb +89 -0
  187. data/lib/linked_rails/model/indexable.rb +51 -0
  188. data/lib/linked_rails/model/iri.rb +121 -0
  189. data/lib/linked_rails/model/iri_mapping.rb +69 -0
  190. data/lib/linked_rails/model/serialization.rb +88 -0
  191. data/lib/linked_rails/model/sorting.rb +20 -0
  192. data/lib/linked_rails/model.rb +68 -0
  193. data/lib/linked_rails/params_parser.rb +93 -0
  194. data/lib/linked_rails/policy/attribute_conditions.rb +53 -0
  195. data/lib/linked_rails/policy.rb +189 -0
  196. data/lib/linked_rails/rdf_error.rb +36 -0
  197. data/lib/linked_rails/renderers.rb +46 -0
  198. data/lib/linked_rails/routes.rb +108 -0
  199. data/lib/linked_rails/serializer.rb +137 -0
  200. data/lib/linked_rails/translate.rb +176 -0
  201. data/lib/linked_rails/version.rb +5 -0
  202. data/lib/linked_rails/vocab.rb +81 -0
  203. data/lib/linked_rails.rb +86 -0
  204. data/lib/nill_class_renderer.rb +3 -0
  205. data/lib/rails/welcome_controller.rb +45 -0
  206. data/lib/tasks/linked_rails_tasks.rake +6 -0
  207. metadata +416 -0
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Helpers
5
+ module OntolaActionsHelper
6
+ def add_exec_action_header(headers, action)
7
+ headers['Exec-Action'] ||= ''
8
+ headers['Exec-Action'] += "#{action}\n"
9
+ end
10
+
11
+ def ontola_copy_action(value)
12
+ Vocab.libro["actions/copyToClipboard?#{{value: value}.to_param}"]
13
+ end
14
+
15
+ def ontola_dialog_action(resource, opener: nil)
16
+ Vocab.libro["actions/dialog/alert?#{{resource: resource, opener: opener}.compact.to_param}"]
17
+ end
18
+
19
+ def ontola_dialog_close_action
20
+ Vocab.libro['actions/dialog/close']
21
+ end
22
+
23
+ def ontola_redirect_action(location, reload: nil)
24
+ Vocab.libro["actions/redirect?#{{location: location, reload: reload}.compact.to_param}"]
25
+ end
26
+
27
+ def ontola_snackbar_action(text)
28
+ Vocab.libro["actions/snackbar?#{{text: text}.to_param}"]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Helpers
5
+ module ResourceHelper
6
+ def current_resource
7
+ return @current_resource if instance_variable_defined?(:@current_resource)
8
+
9
+ @current_resource ||= resolve_current_resource
10
+ end
11
+
12
+ def new_resource
13
+ @new_resource ||=
14
+ if requested_resource.try(:singular_resource?)
15
+ requested_resource
16
+ elsif parent_resource
17
+ new_resource_from_parent
18
+ else
19
+ build_new_resource
20
+ end
21
+ end
22
+
23
+ def params_for_parent
24
+ params.dup
25
+ end
26
+
27
+ def parent_from_params
28
+ @parent_from_params ||= LinkedRails.iri_mapper.parent_from_params(params, user_context)
29
+ end
30
+
31
+ def parent_from_params!
32
+ parent_from_params || raise(ActiveRecord::RecordNotFound)
33
+ end
34
+
35
+ def parent_resource
36
+ @parent_resource ||= requested_resource_parent || parent_from_params
37
+ end
38
+
39
+ def parent_resource!
40
+ parent_resource || raise(ActiveRecord::RecordNotFound)
41
+ end
42
+
43
+ def requested_resource_parent
44
+ requested_resource&.parent
45
+ end
46
+
47
+ private
48
+
49
+ def build_new_resource
50
+ controller_class.build_new(user_context: user_context)
51
+ end
52
+
53
+ def new_resource_from_parent
54
+ parent_resource.build_child(
55
+ controller_class,
56
+ user_context: user_context
57
+ )
58
+ end
59
+
60
+ def resolve_current_resource
61
+ case action_name
62
+ when 'create', 'new'
63
+ new_resource
64
+ else
65
+ requested_resource
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ class IRIMapper
5
+ class << self
6
+ EMPTY_IRI_OPTS = {params: {}}.freeze
7
+
8
+ # Converts an IRI into a hash containing the action, class, iri and the params of the iri
9
+ # @return [Hash]
10
+ # @example Valid resource IRI
11
+ # opts_from_iri('https://example.com/resource/1?foo=bar')
12
+ # => {
13
+ # action: 'show',
14
+ # class: Resource,
15
+ # params: {
16
+ # id: '1',
17
+ # foo: 'bar'
18
+ # }
19
+ # }
20
+ # @example Valid collection IRI
21
+ # opts_from_iri('https://example.com/resources?foo=bar')
22
+ # => {
23
+ # action: 'index',
24
+ # class: Resource,
25
+ # params: {
26
+ # foo: 'bar'
27
+ # }
28
+ # }
29
+ # @example Invalid IRI
30
+ # opts_from_iri('https://example.com/invalid/1')
31
+ # => {
32
+ # params: {}
33
+ # }
34
+ def opts_from_iri(iri, method: 'GET')
35
+ query = Rack::Utils.parse_nested_query(URI(iri.to_s).query)
36
+ params = Rails.application.routes.recognize_path(iri.to_s, method: method)
37
+
38
+ route_params_to_opts(params.merge(query))
39
+ rescue ActionController::RoutingError
40
+ EMPTY_IRI_OPTS.dup
41
+ end
42
+
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
+ def parent_from_params(params, user_context)
59
+ return unless params.key?(:parent_iri)
60
+
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])
64
+
65
+ LinkedRails.iri_mapper.resource_from_opts(opts, user_context)
66
+ end
67
+
68
+ def resource_from_iri(iri, user_context)
69
+ ensure_absolute_iri!(iri)
70
+
71
+ opts = opts_from_iri(iri)
72
+ resource_from_opts(opts, user_context)
73
+ end
74
+
75
+ def resource_from_iri!(iri, user_context)
76
+ resource_from_iri(iri, user_context) || raise(ActiveRecord::RecordNotFound)
77
+ end
78
+
79
+ def resource_from_opts(opts, user_context)
80
+ opts[:class]&.requested_resource(opts, user_context)
81
+ end
82
+
83
+ def route_params_to_opts(params)
84
+ controller_class = class_for_controller(params[:controller])
85
+
86
+ return EMPTY_IRI_OPTS.dup if controller_class.blank?
87
+
88
+ {
89
+ action: params[:action],
90
+ class: controller_class,
91
+ params: params.except(:action, :controller)
92
+ }.with_indifferent_access
93
+ end
94
+
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
105
+
106
+ def single_resource_from_opts(opts, user_context)
107
+ opts[:class]&.requested_single_resource(opts[:params], user_context) unless opts[:params] == 'index'
108
+ end
109
+
110
+ private
111
+
112
+ def class_for_controller(controller)
113
+ return if controller.blank?
114
+
115
+ "::#{controller.camelize}Controller"
116
+ .safe_constantize
117
+ .try(:controller_class)
118
+ end
119
+
120
+ def ensure_absolute_iri!(iri)
121
+ raise("An absolute url is expected. #{iri} is given.") if iri.blank? || URI(iri).relative?
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Middleware
5
+ class LinkedDataParams # rubocop:disable Metrics/ClassLength
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ req = Rack::Request.new(env)
12
+ params_from_query(req)
13
+ params_from_graph(req)
14
+
15
+ @app.call(env)
16
+ end
17
+
18
+ private
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/'])
47
+ end
48
+
49
+ def convert_query_params(request, target_class)
50
+ keys = target_class.predicate_mapping.keys.map(&:to_s)
51
+ class_key = target_class.to_s.underscore
52
+ data = request.params[class_key] || {}
53
+ request.params.slice(*keys.map(&:to_s)).each do |key, value|
54
+ add_param_from_query(data, target_class, key, value)
55
+ end
56
+ request.update_param(class_key, data) if data.present?
57
+ end
58
+
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?
73
+
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
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
+ # Converts a serialized graph from a multipart request body to a nested
110
+ # attributes hash.
111
+ #
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
114
+ # assumed to be the resource intended to be targeted by the request (i.e. the
115
+ # resource to be created, updated, or deleted).
116
+ #
117
+ # @return [Hash] A hash of attributes, empty if no statements were given.
118
+ def params_from_graph(request)
119
+ graph = graph_from_request(request)
120
+
121
+ return unless graph
122
+
123
+ request.update_param(:body_graph, graph)
124
+ target_class = target_class_from_path(request)
125
+ return if target_class.blank?
126
+
127
+ update_actor_param(request, graph)
128
+ update_target_params(request, graph, target_class)
129
+ end
130
+
131
+ def params_from_query(request)
132
+ target_class = target_class_from_path(request) if request.params.present?
133
+ return unless target_class.try(:predicate_mapping)
134
+
135
+ convert_query_params(request, target_class)
136
+ end
137
+
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
+ def target_class_from_path(request) # rubocop:disable Metrics/AbcSize
198
+ opts = LinkedRails.iri_mapper.opts_from_iri(
199
+ request.base_url + request.env['REQUEST_URI'],
200
+ method: request.request_method
201
+ )
202
+
203
+ logger.info("No class found for #{request.base_url + request.env['REQUEST_URI']}") unless opts[:class]
204
+
205
+ opts[:class]
206
+ end
207
+
208
+ def update_actor_param(request, graph)
209
+ actor = graph.query([Vocab.ll[:targetResource], Vocab.schema.creator]).first
210
+ return if actor.blank?
211
+
212
+ request.update_param(:actor_iri, actor.object)
213
+ graph.delete(actor)
214
+ end
215
+
216
+ def update_target_params(request, graph, target_class)
217
+ key = target_class.to_s.demodulize.underscore
218
+ from_body = parse_resource(request.params, graph, Vocab.ll[:targetResource], target_class)
219
+
220
+ request.update_param(key, from_body.merge(request.params[key] || {}))
221
+ end
222
+ end
223
+ end
224
+ end