solis 0.64.0

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +6 -0
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +287 -0
  8. data/Rakefile +10 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/examples/after_hooks.rb +24 -0
  12. data/examples/config.yml.template +15 -0
  13. data/examples/read_from_shacl.rb +22 -0
  14. data/examples/read_from_shacl_abv.rb +84 -0
  15. data/examples/read_from_sheet.rb +22 -0
  16. data/lib/solis/config_file.rb +91 -0
  17. data/lib/solis/error/cursor_error.rb +6 -0
  18. data/lib/solis/error/general_error.rb +6 -0
  19. data/lib/solis/error/invalid_attribute_error.rb +6 -0
  20. data/lib/solis/error/invalid_datatype_error.rb +6 -0
  21. data/lib/solis/error/not_found_error.rb +6 -0
  22. data/lib/solis/error/query_error.rb +6 -0
  23. data/lib/solis/error.rb +3 -0
  24. data/lib/solis/graph.rb +360 -0
  25. data/lib/solis/model.rb +565 -0
  26. data/lib/solis/options.rb +19 -0
  27. data/lib/solis/query/construct.rb +93 -0
  28. data/lib/solis/query/filter.rb +133 -0
  29. data/lib/solis/query/run.rb +97 -0
  30. data/lib/solis/query.rb +347 -0
  31. data/lib/solis/resource.rb +37 -0
  32. data/lib/solis/shape/data_types.rb +280 -0
  33. data/lib/solis/shape/reader/csv.rb +12 -0
  34. data/lib/solis/shape/reader/file.rb +16 -0
  35. data/lib/solis/shape/reader/sheet.rb +777 -0
  36. data/lib/solis/shape/reader/simple_sheets/sheet.rb +59 -0
  37. data/lib/solis/shape/reader/simple_sheets/worksheet.rb +173 -0
  38. data/lib/solis/shape/reader/simple_sheets.rb +40 -0
  39. data/lib/solis/shape.rb +189 -0
  40. data/lib/solis/sparql_adaptor.rb +318 -0
  41. data/lib/solis/store/sparql/client/query.rb +35 -0
  42. data/lib/solis/store/sparql/client.rb +41 -0
  43. data/lib/solis/version.rb +3 -0
  44. data/lib/solis.rb +13 -0
  45. data/solis.gemspec +50 -0
  46. metadata +304 -0
@@ -0,0 +1,91 @@
1
+ #encoding: UTF-8
2
+
3
+ require 'yaml'
4
+
5
+ module Solis
6
+ class ConfigFile
7
+ @config = {}
8
+ @config_file_path = ''
9
+ @config_file_name = 'config.yml'
10
+
11
+ def self.version
12
+ "0.0.4"
13
+ end
14
+
15
+ def self.name
16
+ @config_file_name
17
+ end
18
+
19
+ def self.name=(config_file_name)
20
+ @config_file_name = config_file_name
21
+ end
22
+
23
+ def self.path
24
+ @config_file_path
25
+ end
26
+
27
+ def self.path=(config_file_path)
28
+ @config_file_path = File.absolute_path(config_file_path)
29
+ end
30
+
31
+ def self.[](key)
32
+ init
33
+ @config[key]
34
+ end
35
+
36
+ def self.[]=(key,value)
37
+ init
38
+ @config[key] = value
39
+ File.open("#{path}/#{name}", 'w') do |f|
40
+ f.puts @config.to_yaml
41
+ end
42
+ end
43
+
44
+ def self.include?(key)
45
+ init
46
+ @config.include?(key)
47
+ end
48
+
49
+ def self.config
50
+ init
51
+ @config
52
+ end
53
+
54
+ private
55
+
56
+ def self.init
57
+ discover_config_file_path
58
+ if @config.empty?
59
+ config = YAML::load_file("#{path}/#{name}")
60
+ @config = process(config)
61
+ end
62
+ end
63
+
64
+ def self.discover_config_file_path
65
+ @config_file_path = ENV['CONFIG_FILE_PATH'] || '' if path.nil? || path.empty?
66
+ if @config_file_path.nil? || @config_file_path.empty?
67
+ if File.exist?(@config_file_name)
68
+ @config_file_path = '.'
69
+ elsif File.exist?("config/#{@config_file_name}")
70
+ @config_file_path = 'config'
71
+ end
72
+ end
73
+
74
+ #puts "#{@config_file_name} found at #{@config_file_path}"
75
+ end
76
+
77
+ def self.process(config)
78
+ new_config = {}
79
+ config.each do |k,v|
80
+
81
+ if config[k].is_a?(Hash)
82
+ v = process(v)
83
+ end
84
+ # config.delete(k)
85
+ new_config.store(k.to_sym, v)
86
+ end
87
+
88
+ new_config
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,6 @@
1
+ module Solis
2
+ module Error
3
+ class CursorError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Solis
2
+ module Error
3
+ class GeneralError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Solis
2
+ module Error
3
+ class InvalidAttributeError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Solis
2
+ module Error
3
+ class InvalidDatatypeError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Solis
2
+ module Error
3
+ class NotFoundError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Solis
2
+ module Error
3
+ class QueryError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ Dir.glob("#{File.dirname(__FILE__)}/error/**.rb") do |dir|
2
+ require dir
3
+ end
@@ -0,0 +1,360 @@
1
+ require 'graphiti'
2
+ require 'moneta'
3
+ require 'active_support/all'
4
+ require 'uri'
5
+
6
+ require_relative 'shape'
7
+ require_relative 'model'
8
+ require_relative 'resource'
9
+ require_relative 'options'
10
+
11
+ module Solis
12
+ class Graph
13
+ attr_accessor :options, :default_before_read, :default_after_read, :default_before_create, :default_after_create, :default_before_update, :default_after_update, :default_before_delete, :default_after_delete
14
+
15
+ def initialize(graph, options = {})
16
+ raise "Please provide a graph_name, graph_prefix and sparql_endpoint option" if options.nil? || options.empty?
17
+ cloned_options = options.clone
18
+
19
+ Solis::Options.instance.set = options
20
+
21
+ @global_resource_stack = []
22
+ @graph = graph
23
+ @graph_name = cloned_options.delete(:graph_name) || '/'
24
+ @graph_prefix = cloned_options.delete(:graph_prefix) || 'pf0'
25
+ @sparql_endpoint = cloned_options.delete(:sparql_endpoint) || nil
26
+
27
+ if cloned_options&.key?(:hooks) && cloned_options[:hooks].is_a?(Hash)
28
+ hooks = cloned_options[:hooks]
29
+
30
+ if hooks.key?(:read)
31
+ if hooks[:read].key?(:before)
32
+ @default_before_read = hooks[:read][:before]
33
+ end
34
+
35
+ if hooks[:read].key?(:after)
36
+ @default_after_read = hooks[:read][:after]
37
+ end
38
+ end
39
+
40
+ if hooks.key?(:create)
41
+ if hooks[:create].key?(:before)
42
+ @default_before_create = hooks[:create][:before]
43
+ end
44
+
45
+ if hooks[:create].key?(:after)
46
+ @default_after_create = hooks[:create][:after]
47
+ end
48
+ end
49
+
50
+ if hooks.key?(:update)
51
+ if hooks[:update].key?(:before)
52
+ @default_before_update = hooks[:update][:before]
53
+ end
54
+
55
+ if hooks[:update].key?(:after)
56
+ @default_after_update = hooks[:update][:after]
57
+ end
58
+ end
59
+
60
+ if hooks.key?(:delete)
61
+ if hooks[:delete].key?(:before)
62
+ @default_before_delete = hooks[:delete][:before]
63
+ end
64
+ if hooks[:delete].key?(:after)
65
+ @default_after_delete = hooks[:delete][:after]
66
+ end
67
+ end
68
+ end
69
+
70
+ unless @sparql_endpoint.nil?
71
+ uri = URI.parse(@sparql_endpoint)
72
+ @sparql_endpoint = RDF::Repository.new(uri: RDF::URI(@graph_name), title: uri.host) if uri.scheme.eql?('repository')
73
+ end
74
+
75
+ @inflections = cloned_options.delete(:inflections) || nil
76
+ @shapes = Solis::Shape.from_graph(graph)
77
+ @language = cloned_options.delete(:language) || 'en'
78
+
79
+ unless @inflections.nil?
80
+ raise "Inflection file not found #{File.absolute_path(@inflections)}" unless File.exist?(@inflections)
81
+ JSON.parse(File.read(@inflections)).each do |s, p|
82
+ ActiveSupport::Inflector.inflections.irregular(s, p)
83
+ end
84
+ end
85
+
86
+ @shape_tree = {}
87
+ shape_keys = @shapes.map do |shape_name, _|
88
+ if shape_name.empty?
89
+ LOGGER.warn("Dangling entity found #{_[:target_class].to_s} removing")
90
+ next
91
+ end
92
+ @shapes[shape_name][:attributes].select { |_, metadata| metadata.key?(:node_kind) && !metadata[:node_kind].nil? }.values.map { |m| m[:datatype].to_s }
93
+ end
94
+ shape_keys += @shapes.keys
95
+ shape_keys = shape_keys.flatten.compact.sort.uniq
96
+
97
+ shape_keys.each do |shape_name|
98
+ d = @shape_tree.key?(shape_name) ? @shape_tree[shape_name] : 0
99
+ d += 1
100
+ @shape_tree[shape_name] = d
101
+ end
102
+
103
+ @shape_tree = @shape_tree.sort_by(&:last).reverse.to_h
104
+
105
+ shape_keys.each do |s|
106
+ shape_as_model(s)
107
+ end
108
+
109
+ @shape_tree.each do |shape_name, _|
110
+ shape_as_resource(shape_name)
111
+ end
112
+
113
+ Graphiti.configure do |config|
114
+ config.pagination_links = true
115
+ config.context_for_endpoint= ->(path, action) {
116
+ Solis::NoopEndpoint.new(path, action)
117
+ }
118
+ end
119
+
120
+ Graphiti.setup!
121
+ end
122
+
123
+ def list_shapes
124
+ @shapes.keys.sort
125
+ end
126
+
127
+ def shape?(key)
128
+ @shapes.key?(key)
129
+ end
130
+
131
+ def jsonapi_schema
132
+ Graphiti::Schema.generate.to_json
133
+ end
134
+
135
+ def shape_as_model(shape_name)
136
+ raise Solis::Error::NotFoundError, "'#{shape_name}' not found. Available classes are #{list_shapes.join(', ')}" unless shape?(shape_name)
137
+ return Object.const_get(shape_name) if Object.const_defined?(shape_name)
138
+
139
+ LOGGER.info("Creating model #{shape_name}")
140
+ attributes = @shapes[shape_name][:attributes].keys.map { |m| m.to_sym }
141
+
142
+ model = nil
143
+ parent_model = nil
144
+ if @shapes[shape_name].key?(:target_node) && @shapes[shape_name][:target_node].value =~ /^#{@graph_name}(.*)Shape$/
145
+ parent_shape_model = $1
146
+ parent_model = shape_as_model(parent_shape_model)
147
+
148
+ model = Object.const_set(shape_name, ::Class.new(parent_model) do
149
+ attr_accessor(*attributes)
150
+ end)
151
+ else
152
+ model = Object.const_set(shape_name, ::Class.new(Solis::Model) do
153
+ attr_accessor(*attributes)
154
+ end)
155
+ end
156
+
157
+ model.graph_name = @graph_name
158
+ model.graph_prefix = @graph_prefix
159
+ model.shapes = @shapes
160
+ model.metadata = @shapes[shape_name]
161
+ #model.language = Graphiti.context[:object]&.language || Solis::Options.instance.get[:language] || @language || 'en'
162
+ unless parent_model.nil?
163
+ parent_model.metadata[:attributes].each do |k, v|
164
+ unless model.metadata[:attributes].key?(k)
165
+ model.metadata[:attributes][k] = v
166
+ end
167
+ end
168
+ end
169
+ model.sparql_endpoint = @sparql_endpoint
170
+ model.graph = self
171
+
172
+ model.model_before_read do |original_class|
173
+ @default_before_read.call(original_class)
174
+ end if @default_before_read
175
+
176
+ model.model_after_read do |persisted_class|
177
+ @default_after_read.call(persisted_class)
178
+ end if @default_after_read
179
+
180
+ model.model_before_create do |original_class|
181
+ @default_before_create.call(original_class)
182
+ end if @default_before_create
183
+
184
+ model.model_after_create do |persisted_class|
185
+ @default_after_create.call(persisted_class)
186
+ end if @default_after_create
187
+
188
+ model.model_before_update do |original_class, updated_class|
189
+ @default_before_update.call(original_class, updated_class)
190
+ end if @default_before_update
191
+
192
+ model.model_after_update do |updated_class, persisted_class|
193
+ @default_after_update.call(updated_class, persisted_class)
194
+ end if @default_after_update
195
+
196
+ model.model_before_delete do |updated_class|
197
+ @default_before_delete.call(updated_class)
198
+ end if @default_before_delete
199
+
200
+ model.model_after_delete do |persisted_class|
201
+ @default_after_delete.call(persisted_class)
202
+ end if @default_after_delete
203
+
204
+ model
205
+ end
206
+
207
+ def shape_as_resource(shape_name, stack_level = [])
208
+ model = shape_as_model(shape_name)
209
+ resource_name = "#{shape_name}Resource"
210
+
211
+ raise Solis::Error::NotFoundError, "#{shape_name} not found. Available classes are #{list_shapes.join(', ')}" unless shape?(shape_name)
212
+ return Object.const_get(resource_name) if Object.const_defined?(resource_name)
213
+
214
+ LOGGER.info("Creating resource #{resource_name}")
215
+
216
+ attributes = @shapes[shape_name][:attributes].select { |_, metadata| metadata.key?(:node_kind) && metadata[:node_kind].nil? }
217
+
218
+ relations = @shapes[shape_name][:attributes].select { |_, metadata| metadata.key?(:node_kind) && !metadata[:node_kind].nil? }
219
+
220
+ @global_resource_stack << resource_name
221
+ relations.each_key do |k|
222
+ next if relations[k][:node_kind].is_a?(RDF::URI) && relations[k][:class].value.gsub(@graph_name, '').gsub('Shape', '').eql?(shape_name)
223
+ relation_shape = relations[k][:class].value.gsub(@graph_name, '').gsub('Shape', '')
224
+ shape_as_resource(relation_shape, stack_level << relation_shape) unless stack_level.include?(relation_shape)
225
+ end
226
+
227
+ description = @shapes[shape_name][:comment]
228
+ parent_resource = Resource
229
+ descendants = ObjectSpace.each_object(Class).select { |klass| klass < model }.map { |m| "#{m.to_s}Resource" }
230
+
231
+ graph = self
232
+
233
+ #Resource
234
+ if Object.const_defined?(resource_name)
235
+ resource = Object.const_get(resource_name)
236
+ else
237
+ ###################
238
+ # Define new resource
239
+ resource = Object.const_set(resource_name, ::Class.new(Resource) do
240
+ if descendants.length > 0
241
+ self.polymorphic = descendants
242
+ self.polymorphic << resource_name
243
+ self.polymorphic.uniq!
244
+ end
245
+
246
+ self.model = model
247
+ self.type = model.name.demodulize.underscore.pluralize.to_sym
248
+ self.description = description
249
+
250
+ attributes.each do |key, metadata|
251
+ next if key.nil? || key.empty?
252
+ if key.eql?('id')
253
+ attribute key.to_sym, :uuid, description: metadata[:comment]
254
+ else
255
+ if (metadata[:maxcount] && metadata[:maxcount] > 1 || metadata[:maxcount].nil?) && ![:boolean, :hash, :array].include?(metadata[:datatype])
256
+ datatype = "array_of_#{metadata[:datatype]}s".to_sym
257
+ else
258
+ datatype = metadata[:datatype]
259
+ end
260
+ LOGGER.info "\t#{resource_name}.#{key}(#{datatype})"
261
+ attribute key.to_sym, datatype, description: metadata[:comment]
262
+ end
263
+ end
264
+ end)
265
+
266
+ relations.each do |key, value|
267
+ # next if value[:datatype].to_s.classify.eql?(shape_name) #why skip self relations...
268
+ if (value[:mincount] && value[:mincount] > 1 || value[:mincount].nil?) || (value[:maxcount] && value[:maxcount] > 1 || value[:maxcount].nil?)
269
+ belongs_to_resource_name = value[:datatype].nil? ? value[:class].value.gsub(self.model.graph_name, '') : value[:datatype].to_s.tableize.classify
270
+ LOGGER.info "\t\t\t#{resource_name}(#{resource_name.gsub('Resource','').tableize.singularize}) belongs_to #{belongs_to_resource_name}(#{key})"
271
+ resource.belongs_to(key.to_sym, foreign_key: :id, resource: graph.shape_as_resource("#{belongs_to_resource_name}", stack_level << belongs_to_resource_name)) do
272
+ #resource.attribute key.to_sym, :string, only: [:filterable]
273
+
274
+ link do |resource|
275
+ remote_resources = resource.instance_variable_get("@#{key}")
276
+ if remote_resources
277
+ remote_resources = [remote_resources] unless remote_resources.is_a?(Array)
278
+ remote_resources = remote_resources.map do |remote_resource|
279
+ resource_id = remote_resource.id =~ /^http/ ? remote_resource.id.split('/').last : remote_resource.id
280
+ #"/#{key.tableize}/#{resource_id}"
281
+ "#{resource.class.graph_name.gsub(/\/$/,'')}/#{belongs_to_resource_name.tableize}/#{resource_id}"
282
+ end
283
+
284
+ # return remote_resources.length == 1 ? remote_resources.first : remote_resources
285
+ end
286
+
287
+ remote_resources.first if remote_resources #belongs_to
288
+ end
289
+ end
290
+ else
291
+ has_many_resource_name = value[:datatype].nil? ? value[:class].gsub(self.model.graph_name, '') : value[:datatype].to_s.classify
292
+ LOGGER.info "\t\t\t#{resource_name}(#{resource_name.gsub('Resource','').tableize.singularize}) has_many #{has_many_resource_name}(#{key})"
293
+ resource.has_many(key.to_sym, foreign_key: :id, primary_key: :id, resource: graph.shape_as_resource("#{has_many_resource_name}", stack_level << has_many_resource_name)) do
294
+
295
+ belongs_to_resource = graph.shape_as_resource("#{has_many_resource_name}")
296
+
297
+ belongs_to_resource.belongs_to(resource.model.name.tableize.singularize, foreign_key: :id, primary_key: :id, resource: graph.shape_as_resource(resource.model.name)) do
298
+ link do |resource|
299
+ remote_resources = resource.instance_variable_get("@#{shape_name.tableize.singularize}")
300
+ if remote_resources
301
+ remote_resources = [remote_resources] unless remote_resources.is_a?(Array)
302
+ remote_resources = remote_resources.map do |remote_resource|
303
+ resource_id = remote_resource.id =~ /^http/ ? remote_resource.id.split('/').last : remote_resource.id
304
+ #"/#{key.tableize}/#{resource_id}"
305
+ "#{resource.class.graph_name.gsub(/\/$/,'')}/#{belongs_to_resource.name.tableize}/#{resource_id}"
306
+ end
307
+
308
+ # return remote_resources.length == 1 ? remote_resources.first : remote_resources
309
+ end
310
+ remote_resources if remote_resources #has_many_belongs_to
311
+ end
312
+ end
313
+ #
314
+ link do |resource|
315
+ remote_resources = resource.instance_variable_get("@#{key}")
316
+ if remote_resources
317
+ remote_resources = [remote_resources] unless remote_resources.is_a?(Array)
318
+ remote_resources = remote_resources.map do |remote_resource|
319
+ resource_id = remote_resource.id =~ /^http/ ? remote_resource.id.split('/').last : remote_resource.id
320
+ #"/#{key.tableize}/#{resource_id}"
321
+ "#{resource.class.graph_name.gsub(/\/$/,'')}/#{remote_resource.name.tableize}/#{resource_id}"
322
+ end
323
+
324
+ #return remote_resources.length == 1 ? remote_resources.first : remote_resources
325
+ end
326
+ remote_resources.first if remote_resources
327
+ end
328
+ end
329
+ end
330
+
331
+ resource.filter :"#{key}_id", :string, single: true, only: [:eq, :not_eq] do
332
+ eq do |scope, filter_value|
333
+ scope[:filters][key.to_sym] = filter_value
334
+ scope
335
+ end
336
+
337
+ not_eq do |scope, filter_value|
338
+ scope[:filters][key.to_sym] = {value: [filter_value], operator: '=', is_not: true}
339
+ scope
340
+ end
341
+ end
342
+
343
+ end
344
+ end
345
+ resource.sparql_endpoint = @sparql_endpoint
346
+ resource.endpoint_namespace = "#{resource.model.graph_name.gsub(/\/$/,'')}#{Solis::Options.instance.get[:base_path]}"
347
+ resource
348
+ end
349
+
350
+ def flush_all(graph_name=nil, force = false)
351
+ raise Solis::Error::NotFoundError, "Supplied graph_name '#{graph_name}' does not equal graph name defined in config file '#{@graph_name}', set force to true" unless graph_name.eql?(@graph_name) && !force
352
+
353
+ @sparql_client = SPARQL::Client.new(@sparql_endpoint)
354
+ result = @sparql_client.query("with <#{graph_name}> delete {?s ?p ?o} where{?s ?p ?o}")
355
+ LOGGER.info(result.first.to_a.first.last.value)
356
+ true
357
+ end
358
+
359
+ end
360
+ end