linked_rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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