solis 0.64.0

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