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,777 @@
1
+ require_relative 'simple_sheets'
2
+ require 'active_support/all'
3
+ require 'active_support/hash_with_indifferent_access'
4
+ require 'rdf/vocab'
5
+ require 'solis/error'
6
+
7
+ module Solis
8
+ module Shape
9
+ module Reader
10
+ class Sheet
11
+ def self.read(key, spreadsheet_id, options = {})
12
+ class << self
13
+ def validate(sheets)
14
+ raise "Please make sure the sheet contains '_PREFIXES', '_METADATA', '_ENTITIES' tabs" unless (%w[
15
+ _PREFIXES _METADATA _ENTITIES
16
+ ] - sheets.keys).length == 0
17
+
18
+ prefixes = sheets['_PREFIXES']
19
+ metadata = sheets['_METADATA']
20
+ entities = sheets['_ENTITIES']
21
+
22
+ raise "_PREFIXES tab must have ['base', 'prefix', 'uri'] as a header at row 1" unless (%w[base prefix
23
+ uri] - prefixes.header).length == 0
24
+ raise "_METADATA tab must have ['key', 'value'] as a header at row 1" unless (%w[key
25
+ value] - metadata.header).length == 0
26
+ raise "_ENTITIES tab must have ['name', 'nameplural', 'description', 'subclassof', 'sameas'] as a header at row 1" unless (%w[
27
+ name nameplural description subclassof sameas
28
+ ] - entities.header).length == 0
29
+
30
+ raise '_PREFIXES.base can only have one base URI' if prefixes.map { |m| m['base'] }.grep(/\*/).count != 1
31
+ end
32
+
33
+ def read_sheets(key, spreadsheet_id, options)
34
+ data = nil
35
+
36
+ cache_dir = ConfigFile.include?(:solis) && ConfigFile[:solis].include?(:cache) ? ConfigFile[:solis][:cache] : '/tmp'
37
+
38
+ if ::File.exist?("#{cache_dir}/#{spreadsheet_id}.json") && (options.include?(:from_cache) && options[:from_cache])
39
+ Solis::LOGGER.info("from cache #{cache_dir}/#{spreadsheet_id}.json")
40
+ data = JSON.parse(::File.read("#{cache_dir}/#{spreadsheet_id}.json"), { symbolize_names: true })
41
+ else
42
+ Solis::LOGGER.info("from source #{spreadsheet_id}")
43
+ session = SimpleSheets.new(key, spreadsheet_id)
44
+ session.key = key
45
+ sheets = {}
46
+ session.worksheets.each do |worksheet|
47
+ sheet = ::Sheet.new(worksheet)
48
+ sheets[sheet.title] = sheet
49
+ end
50
+
51
+ validate(sheets)
52
+
53
+ entities = {}
54
+ prefixes = {}
55
+ ontology_metadata = {}
56
+
57
+ sheets['_PREFIXES'].each do |e|
58
+ prefixes.store(e['prefix'].to_sym, { uri: e['uri'], base: e['base'].eql?('*') })
59
+ end
60
+ sheets['_METADATA'].each { |e| ontology_metadata.store(e['key'].to_sym, e['value']) }
61
+
62
+ base_uri = prefixes.select { |_k, v| v[:base] }.select { |s| !s.empty? }
63
+
64
+ graph_prefix = base_uri.keys.first
65
+ graph_name = base_uri.values.first[:uri]
66
+
67
+ sheets['_ENTITIES'].each do |e|
68
+
69
+ top_class = e['name'].to_s
70
+ # subclassof = e['subclassof'].empty? ? nil : e['subclassof'].split(':').last
71
+ # while subclassof
72
+ # candidate_sco = sheets['_ENTITIES'].select{|t| t['name'].eql?(subclassof)}.first
73
+ # subclassof = candidate_sco['subclassof'].empty? ? nil : candidate_sco['subclassof'].split(':').last
74
+ # top_class = candidate_sco['name'].to_s if candidate_sco['subclassof'].empty?
75
+ # end
76
+
77
+ entity_data = parse_entity_data(e['name'].to_s, graph_prefix, graph_name, sheets[top_class])
78
+
79
+ if entity_data.empty?
80
+ entity_data[:id] = {
81
+ datatype: 'xsd:string',
82
+ path: "#{graph_prefix}:id",
83
+ cardinality: { min: '1', max: '1' },
84
+ same_as: '',
85
+ description: 'systeem UUID'
86
+ }
87
+ end
88
+
89
+ entities.store(e['name'].to_sym, { description: e['description'],
90
+ plural: e['nameplural'],
91
+ label: e['name'].to_s.strip,
92
+ sub_class_of: e['subclassof'].nil? || e['subclassof'].empty? ? [] : [e['subclassof']],
93
+ same_as: e['sameas'],
94
+ properties: entity_data })
95
+ end
96
+
97
+ data = {
98
+ entities: entities,
99
+ ontologies: {
100
+ all: prefixes,
101
+ base: {
102
+ prefix: graph_prefix,
103
+ uri: graph_name
104
+ }
105
+ },
106
+ metadata: ontology_metadata
107
+ }
108
+
109
+ ::File.open("#{::File.absolute_path(cache_dir)}/#{spreadsheet_id}.json", 'wb') do |f|
110
+ f.puts data.to_json
111
+ end
112
+ end
113
+
114
+ data
115
+ rescue StandardError => e
116
+ raise Solis::Error::GeneralError, e.message
117
+ end
118
+
119
+ def parse_entity_data(name, graph_prefix, _graph_name, e)
120
+ properties = {}
121
+ entity_data = e
122
+ if entity_data && !entity_data.nil? && (entity_data.count > 0)
123
+ entity_data.each do |p|
124
+ min_max = {}
125
+
126
+ %w[min max].each do |n|
127
+ min_max[n] = if p.key?(n) && p[n] =~ /\d+/
128
+ p[n].to_i.to_s
129
+ else
130
+ ''
131
+ end
132
+ end
133
+ puts "#{name}.#{p['name']}"
134
+ unless p.key?('name')
135
+ pp p
136
+ end
137
+ properties[p['name'].strip] = {
138
+ datatype: p['datatype'],
139
+ path: "#{graph_prefix}:#{p['name'].to_s.classify}",
140
+ cardinality: { min: min_max['min'], max: min_max['max'] },
141
+ same_as: p['sameAs'],
142
+ description: p['description']
143
+ }
144
+ end
145
+ end
146
+
147
+ properties
148
+ end
149
+
150
+ def build_plantuml(data)
151
+ out = %(@startuml
152
+ !pragma layout elk
153
+ skinparam classFontSize 14
154
+ !define LIGHTORANGE
155
+ skinparam groupInheritance 1
156
+ skinparam componentStyle uml2
157
+ skinparam wrapMessageWidth 100
158
+ skinparam ArrowColor #Maroon
159
+
160
+ title #{data[:metadata][:title]} - #{data[:metadata][:version]} - #{Time.now}
161
+ )
162
+
163
+ out += "\npackage #{data[:ontologies][:base][:prefix]} {\n"
164
+ data[:entities].each do |entity_name, metadata|
165
+ out += "\nclass #{entity_name}"
166
+
167
+ properties = metadata[:properties]
168
+ relations = []
169
+ unless properties.nil? || properties.empty?
170
+ out += "{\n"
171
+ properties.each do |property, property_metadata|
172
+ out += "\t{field} #{property_metadata[:datatype]} : #{property} \n"
173
+
174
+ if property_metadata[:datatype].split(':').first.eql?(data[:ontologies][:base][:prefix].to_s)
175
+ relations << "#{property_metadata[:datatype].split(':').last} - #{property_metadata[:cardinality].key?(:max) && !property_metadata[:cardinality][:max].empty? ? "\"#{property_metadata[:cardinality][:max]}\"" : ''} #{entity_name} : #{property} >"
176
+ end
177
+ end
178
+ out += "}\n"
179
+ out += relations.join("\n")
180
+ end
181
+
182
+ out += "\n"
183
+ sub_classes = metadata[:sub_class_of]
184
+ sub_classes = [sub_classes] unless sub_classes.is_a?(Array)
185
+ sub_classes.each do |sub_class|
186
+ out += "#{entity_name} --|> #{sub_class.split(':').last}\n" unless sub_class.empty?
187
+ end
188
+ end
189
+
190
+ out += %(
191
+ hide circle
192
+ hide methods
193
+ hide empty members
194
+ @enduml
195
+ )
196
+
197
+ out
198
+ end
199
+
200
+ def datatype_lookup(datatype, as = :sql)
201
+ datatypes = {
202
+ 'xsd:string' => { sql: 'text' },
203
+ 'xsd:date' => { sql: 'date' },
204
+ 'xsd:boolean' => { sql: 'bool' },
205
+ 'xsd:integer' => { sql: 'integer' }
206
+ }
207
+
208
+ datatypes.default = { sql: 'text' }
209
+
210
+ datatypes[datatype][as]
211
+ end
212
+
213
+ def build_plantuml_erd(data)
214
+ cardinality_min = { '0' => '|o', '' => '}o', '1' => '||' }
215
+ cardinality_max = { '0' => 'o|', '' => 'o{', '1' => '||' }
216
+
217
+ out = %(@startuml
218
+ skinparam classFontSize 14
219
+ !define LIGHTORANGE
220
+ skinparam groupInheritance 1
221
+ skinparam componentStyle uml2
222
+ skinparam wrapMessageWidth 100
223
+ skinparam ArrowColor #Maroon
224
+ skinparam linetype ortho
225
+
226
+ title #{data[:metadata][:title]} - #{data[:metadata][:version]} - #{Time.now}
227
+ )
228
+
229
+ out += "\npackage #{data[:ontologies][:base][:prefix]} {\n"
230
+ relations = []
231
+ data[:entities].each do |_entity_name, metadata|
232
+ table_name = metadata[:plural].to_s.underscore
233
+ # out += "\nentity \"#{entity_name}\" as #{table_name}"
234
+ out += "\nentity \"#{table_name}\" as #{table_name}"
235
+
236
+ properties = metadata[:properties]
237
+ # relations = []
238
+ unless properties.nil? || properties.empty?
239
+ out += "{\n"
240
+ properties.each do |property, property_metadata|
241
+ if property.to_s.eql?('id')
242
+ out += "\t *#{property} : #{datatype_lookup(property_metadata[:datatype], :sql)} <<generated>>\n"
243
+ out += "--\n"
244
+ else
245
+ mandatory = property_metadata[:cardinality][:min].to_i > 0
246
+ is_fk = property_metadata[:datatype].split(':').first.eql?(data[:ontologies][:base][:prefix].to_s) ? true : false
247
+ out += "\t #{mandatory ? '*' : ''}#{property}#{is_fk ? '_id' : ''} : #{datatype_lookup(
248
+ property_metadata[:datatype], :sql
249
+ )} #{is_fk ? '<<FK>>' : ''} \n"
250
+ end
251
+
252
+ unless property_metadata[:datatype].split(':').first.eql?(data[:ontologies][:base][:prefix].to_s)
253
+ next
254
+ end
255
+
256
+ cmin = cardinality_min[(property_metadata[:cardinality][:min]).to_s]
257
+ cmax = cardinality_max[(property_metadata[:cardinality][:max]).to_s]
258
+
259
+ # relations << " #{entity_name.to_s.underscore} #{cmin}--o{ #{property_metadata[:datatype].split(':').last.to_s.underscore} "
260
+ # ref_table_name = property_metadata[:datatype].split(':').last.to_s.underscore
261
+ ref_table_name = [property_metadata[:datatype].split(':').last.to_sym,
262
+ property_metadata[:path].split(':').last.classify.to_sym].map do |m|
263
+ data[:entities][m].nil? ? nil : data[:entities][m][:plural].underscore
264
+ end.compact.first
265
+
266
+ relations << " #{table_name} #{cmin}--#{cmax} #{ref_table_name} "
267
+ end
268
+ out += "}\n"
269
+ end
270
+
271
+ out += "\n"
272
+ # out += "#{entity_name} }o-- #{metadata[:sub_class_of].split(':').last}\n" unless metadata[:sub_class_of].empty?
273
+ end
274
+ out += relations.join("\n")
275
+
276
+ out += %(
277
+ hide circle
278
+ hide methods
279
+ hide empty members
280
+ @enduml
281
+ )
282
+
283
+ out
284
+ end
285
+
286
+ def build_shacl(data)
287
+ shacl_prefix = data[:ontologies][:all].select { |_, v| v[:uri] =~ /shacl/ }.keys.first
288
+ shacl_prefix = 'sh' if shacl_prefix.nil?
289
+
290
+ out = header(data)
291
+
292
+ data[:entities].each do |entity_name, metadata|
293
+ graph_prefix = data[:ontologies][:base][:prefix]
294
+ graph_name = data[:ontologies][:base][:uri]
295
+
296
+ description = metadata[:comment]
297
+ label = metadata[:label]
298
+ target_class = "#{graph_prefix}:#{entity_name}"
299
+ node = metadata[:sub_class_of]
300
+
301
+ if node && !node.empty?
302
+ node = node.first if node.is_a?(Array)
303
+ node = node.strip
304
+ node += 'Shape' if node != /Shape$/ && node =~ /^#{graph_prefix}:/
305
+ else
306
+ node = target_class
307
+ end
308
+
309
+ out += %(
310
+ #{graph_prefix}:#{entity_name}Shape
311
+ a #{shacl_prefix}:NodeShape ;
312
+ #{shacl_prefix}:description "#{description&.gsub('"', "'")&.gsub(/\n|\r/, '')}" ;
313
+ #{shacl_prefix}:targetClass #{target_class} ;#{
314
+ unless node.nil? || node.empty?
315
+ "\n #{shacl_prefix}:node #{node} ;"
316
+ end}
317
+ #{shacl_prefix}:name "#{label}" ;
318
+ )
319
+ metadata[:properties].each do |property, property_metadata|
320
+ attribute = property.to_s.strip
321
+ next if attribute.empty?
322
+
323
+ description = property_metadata[:description]&.gsub('"', "'")&.gsub(/\n|\r/, '').strip
324
+ path = "#{graph_prefix}:#{attribute}"
325
+ datatype = property_metadata[:datatype].strip
326
+ min_count = property_metadata[:cardinality][:min].strip
327
+ max_count = property_metadata[:cardinality][:max].strip
328
+
329
+ if datatype =~ /^#{graph_prefix}:/ || datatype =~ /^<#{graph_name}/
330
+ out += %( #{shacl_prefix}:property [#{shacl_prefix}:path #{path} ;
331
+ #{shacl_prefix}:name "#{attribute}" ;
332
+ #{shacl_prefix}:description "#{description}" ;
333
+ #{shacl_prefix}:nodeKind #{shacl_prefix}:IRI ;
334
+ #{shacl_prefix}:class #{datatype} ;#{min_count =~ /\d+/ ? "\n #{shacl_prefix}:minCount #{min_count} ;" : ''}#{max_count =~ /\d+/ ? "\n #{shacl_prefix}:maxCount #{max_count} ;" : ''}
335
+ ] ;
336
+ )
337
+ else
338
+ out += %( #{shacl_prefix}:property [#{shacl_prefix}:path #{path} ;
339
+ #{shacl_prefix}:name "#{attribute}";
340
+ #{shacl_prefix}:description "#{description}" ;
341
+ #{shacl_prefix}:datatype #{datatype} ;#{min_count =~ /\d+/ ? "\n #{shacl_prefix}:minCount #{min_count} ;" : ''}#{max_count =~ /\d+/ ? "\n #{shacl_prefix}:maxCount #{max_count} ;" : ''}
342
+ ] ;
343
+ )
344
+ end
345
+ end
346
+ out += ".\n"
347
+ end
348
+
349
+ out
350
+ end
351
+
352
+ def build_schema(data)
353
+ classes = {}
354
+ datatype_properties = {}
355
+ object_properties = {}
356
+
357
+ format = :ttl
358
+ graph_prefix = data[:ontologies][:base][:prefix]
359
+ graph_name = data[:ontologies][:base][:uri]
360
+
361
+ all_prefixes = {}
362
+ data[:ontologies][:all].each { |k, v| all_prefixes[k] = v[:uri] }
363
+
364
+ data[:entities].each do |entity_name, metadata|
365
+ classes[entity_name] = {
366
+ comment: metadata[:description],
367
+ label: entity_name.to_s,
368
+ type: 'owl:Class',
369
+ subClassOf: metadata[:sub_class_of]
370
+ }
371
+
372
+ metadata[:properties].each do |property, property_metadata|
373
+ attribute = property.to_s.strip
374
+ description = property_metadata[:description]
375
+ path = "#{graph_name}#{attribute}"
376
+ datatype = property_metadata[:datatype]
377
+
378
+ schema_data = datatype_properties[attribute] || {}
379
+ domain = schema_data[:domain] || []
380
+ domain << "#{graph_name}#{entity_name}"
381
+ datatype_properties[attribute] = {
382
+ domain: domain,
383
+ comment: description,
384
+ label: attribute.to_s,
385
+ range: datatype,
386
+ type: 'rdf:Property'
387
+ }
388
+ unless property_metadata[:same_as].nil? || property_metadata[:same_as].empty?
389
+ datatype_properties[attribute]['owl:sameAs'] =
390
+ property_metadata[:same_as]
391
+ end
392
+
393
+ subclass_data = data[:entities][entity_name][:sub_class_of] || []
394
+ unless property_metadata[:cardinality][:min].empty?
395
+ subclass_data << RDF::Vocabulary.term(type: 'owl:Restriction',
396
+ onProperty: path,
397
+ minCardinality: property_metadata[:cardinality][:min])
398
+ end
399
+ unless property_metadata[:cardinality][:max].empty?
400
+ subclass_data << RDF::Vocabulary.term(type: 'owl:Restriction',
401
+ onProperty: path,
402
+ maxCardinality: property_metadata[:cardinality][:max])
403
+ end
404
+ data[:entities][entity_name][:sub_class_of] = subclass_data
405
+ end
406
+ end
407
+
408
+ lp = RDF::StrictVocabulary(graph_name)
409
+ o = ::Class.new(lp) do
410
+ ontology(graph_name.to_sym, {
411
+ "dc11:title": data[:metadata][:title].freeze,
412
+ "dc11:description": data[:metadata][:description].freeze,
413
+ "dc11:date": Time.now.to_s.freeze,
414
+ "dc11:creator": data[:metadata][:author].freeze,
415
+ "owl:versionInfo": data[:metadata][:version].freeze,
416
+ type: 'owl:Ontology'.freeze
417
+ })
418
+
419
+ classes.each do |k, v|
420
+ term k.to_sym, v
421
+ end
422
+ object_properties.each do |k, v|
423
+ property k.is_a?(RDF::URI) ? k.value.to_sym : k.to_sym, v
424
+ end
425
+
426
+ datatype_properties.each do |k, v|
427
+ property k.is_a?(RDF::URI) ? k.value.to_sym : k.to_sym, v
428
+ end
429
+ end
430
+
431
+ RDF::Vocabulary.register(graph_prefix.to_sym, o, uri: graph_name)
432
+
433
+ graph = RDF::Graph.new
434
+ graph.graph_name = RDF::URI(graph_name)
435
+
436
+ data[:entities].select { |_k, v| !v[:same_as].empty? }.each do |k, v|
437
+ prefix, verb = v[:same_as].split(':')
438
+ rdf_vocabulary = RDF::Vocabulary.from_sym(prefix.upcase)
439
+ rdf_verb = rdf_vocabulary[verb.to_sym]
440
+ graph << RDF::Statement.new(rdf_verb, RDF::RDFV.type, RDF::OWL.Class)
441
+ graph << RDF::Statement.new(rdf_verb, RDF::Vocab::OWL.sameAs, o[k.to_sym])
442
+ rescue StandardError => e
443
+ puts e.message
444
+ end
445
+
446
+ graph << o.to_enum
447
+
448
+ graph.dump(format, prefixes: all_prefixes)
449
+ end
450
+
451
+ def build_inflections(data)
452
+ inflections = {}
453
+ data[:entities].each do |entity, metadata|
454
+ inflections[entity] = metadata[:plural]
455
+ inflections[entity.to_s.underscore.to_sym] = metadata[:plural].underscore
456
+ end
457
+
458
+ inflections.to_json
459
+ end
460
+
461
+ def build_sql(data)
462
+ graph_prefix = data[:ontologies][:base][:prefix]
463
+ out = "--\n-- #{data[:metadata][:title]} - #{data[:metadata][:version]} - #{Time.now}\n"
464
+ out += "-- description: #{data[:metadata][:description]}\n"
465
+ out += "-- author: #{data[:metadata][:author]}\n--\n\n"
466
+
467
+ out += %(CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
468
+ DROP SCHEMA IF EXISTS #{graph_prefix} CASCADE;
469
+ CREATE SCHEMA #{graph_prefix};
470
+
471
+ )
472
+ data[:entities].each do |entity_name, metadata|
473
+ table_name = metadata[:plural].to_s.underscore
474
+ out += "CREATE TABLE #{graph_prefix}.#{table_name}(\n"
475
+
476
+ properties = metadata[:properties]
477
+ properties.each_with_index do |(property, property_metadata), i|
478
+ mandatory = property_metadata[:cardinality][:min].to_i > 0
479
+ is_fk = property_metadata[:datatype].split(':').first.eql?(data[:ontologies][:base][:prefix].to_s) ? true : false
480
+ if data[:entities][property_metadata[:datatype].split(':').last.to_sym].nil? && is_fk
481
+ raise Solis::Error::NotFoundError,
482
+ "#{entity_name}.#{property} Not found in _ENTITIES tab"
483
+ end
484
+
485
+ if is_fk
486
+ references = data[:entities][property_metadata[:datatype].split(':').last.to_sym][:plural].to_s.underscore
487
+ end
488
+
489
+ out += ", \n" if i > 0
490
+ if property.to_s.eql?('id')
491
+ # out += "\t#{property} #{datatype_lookup(property_metadata[:datatype], :sql)}#{mandatory ? ' NOT NULL' : ''} PRIMARY KEY"
492
+ out += "\t#{property} SERIAL#{mandatory ? ' NOT NULL' : ''} PRIMARY KEY"
493
+ else
494
+ out += "\t#{property}#{is_fk ? '_id' : ''} #{datatype_lookup(property_metadata[:datatype],
495
+ :sql)}#{mandatory ? ' NOT NULL' : ''}#{is_fk ? " REFERENCES #{graph_prefix}.#{references}(id)" : ''}"
496
+ end
497
+ end
498
+
499
+ out += ");\n\n"
500
+ end
501
+
502
+ out
503
+ end
504
+
505
+ def build_erd(data, type = :uml)
506
+ out = erd_header(data, type)
507
+ all_tables = {}
508
+ tables = {}
509
+ relations = []
510
+ references = {}
511
+ every_entity(data).each do |table|
512
+ case type
513
+ when :uml
514
+ d = table[:table].call(type)
515
+ out += d[:out]
516
+ relations << d[:relations] unless d[:relations].empty?
517
+ out += "\n\n"
518
+ #references << d[:references]
519
+ when :sql
520
+ all_tables[table[:name]] = table
521
+ d = table[:table].call(type)
522
+ tables[table[:name]] = d[:out]
523
+
524
+ d[:references].each do |k, v|
525
+ references[k] = (references.include?(k) ? references[k] : 0) + v
526
+ end
527
+ end
528
+ end
529
+
530
+ references = references.sort_by { |k, v| -v }.to_h #each{|m| r[m[0]] = m[1]}
531
+
532
+ r = references.sort_by { |k, v|
533
+ k = k[0]
534
+ relation = all_tables.key?(k) ? all_tables[k][:properties].map { |s| s[:references] }.compact.first : nil
535
+
536
+ a = references.keys.index(k)
537
+ b = references.keys.index(relation)
538
+ b = 0 if b.nil?
539
+ a = 0 if a.nil?
540
+
541
+ b = a + b if b < a
542
+
543
+ b
544
+ }
545
+
546
+ references = r.to_h
547
+
548
+ if type.eql?(:sql)
549
+ all_keys = references.keys
550
+ t = tables.sort_by { |k, v| all_keys.include?(k) ? all_keys.index(k) : 0 }
551
+ out += t.map { |m| m[1] }.join("\n")
552
+ end
553
+
554
+ # ::File.open("#{ConfigFile[:cache]}/test.json", 'wb') {|f| f.puts references.to_json}
555
+
556
+ out += relations.sort.uniq.join("\n")
557
+ out += erd_footer(data, type)
558
+
559
+ out
560
+ end
561
+
562
+ def erd_header(data, type)
563
+ header = ''
564
+
565
+ case type
566
+ when :uml
567
+ header = %(@startuml
568
+ skinparam classFontSize 14
569
+ !define LIGHTORANGE
570
+ skinparam groupInheritance 1
571
+ skinparam componentStyle uml2
572
+ skinparam wrapMessageWidth 100
573
+ skinparam ArrowColor #Maroon
574
+ skinparam linetype ortho
575
+
576
+ title #{data[:metadata][:title]} - #{data[:metadata][:version]} - #{Time.now}
577
+
578
+ package #{data[:ontologies][:base][:prefix]} {
579
+ )
580
+ when :sql
581
+ graph_prefix = data[:ontologies][:base][:prefix]
582
+ header = %(--
583
+ -- #{data[:metadata][:title]} - #{data[:metadata][:version]} - #{Time.now}
584
+ -- description: #{data[:metadata][:description]}
585
+ -- author: #{data[:metadata][:author]}
586
+ --
587
+
588
+
589
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
590
+ DROP SCHEMA IF EXISTS #{graph_prefix} CASCADE;
591
+ CREATE SCHEMA #{graph_prefix};
592
+
593
+
594
+ )
595
+ end
596
+
597
+ header
598
+ end
599
+
600
+ def erd_footer(_data, type = :uml)
601
+ footer = ''
602
+
603
+ case type
604
+ when :uml
605
+ footer = %(
606
+
607
+ hide circle
608
+ hide methods
609
+ hide empty members
610
+ @enduml
611
+ )
612
+ when :sql
613
+ footer = ''
614
+ end
615
+
616
+ footer
617
+ end
618
+
619
+ def every_entity(data)
620
+ all_references = []
621
+ graph_prefix = data[:ontologies][:base][:prefix]
622
+ data[:entities].map do |entity_name, metadata|
623
+ table_name = metadata[:plural].to_s.underscore
624
+ table_comment = metadata[:description]&.gsub('\'', '')&.gsub(/\n|\r/, ' ')
625
+
626
+ properties = metadata[:properties].map do |name, property_metadata|
627
+ raise Solis::Error::NotFoundError, "A property in the #{entity_name} tab is empty #{metadata[:properties].to_json}" if name.empty?
628
+
629
+ is_fk = property_metadata[:datatype].split(':').first.eql?(data[:ontologies][:base][:prefix].to_s) ? true : false
630
+
631
+ if property_metadata.key?(:datatype) && property_metadata[:datatype].empty?
632
+ raise Solis::Error::NotFoundError,
633
+ "#{entity_name}.#{name} Has no datatype"
634
+ end
635
+
636
+ if data[:entities][property_metadata[:datatype].split(':').last.to_sym].nil? && is_fk
637
+ raise Solis::Error::NotFoundError,
638
+ "#{entity_name}.#{name} Not found in _ENTITIES tab"
639
+ end
640
+
641
+ if is_fk
642
+ references = data[:entities][property_metadata[:datatype].split(':').last.to_sym][:plural].to_s.underscore
643
+ end
644
+ mandatory = property_metadata[:cardinality][:min].to_i > 0
645
+ datatype = datatype_lookup(property_metadata[:datatype], :sql)
646
+
647
+ column_name = "#{name}#{is_fk ? '_id' : ''}"
648
+
649
+ {
650
+ schema: graph_prefix,
651
+ name: name,
652
+ column_name: column_name,
653
+ column: lambda { |type = :uml|
654
+ out = ''
655
+ case type
656
+ when :sql
657
+ if name.to_s.eql?('id')
658
+ out += "\t#{column_name} SERIAL#{mandatory ? ' NOT NULL' : ''} PRIMARY KEY"
659
+ else
660
+ out += "\t#{column_name} #{is_fk ? 'int' : datatype}#{mandatory ? ' NOT NULL' : ''}#{is_fk ? " REFERENCES #{graph_prefix}.#{references}(id)" : ''}"
661
+ end
662
+
663
+ else
664
+ cardinality_min = { '0' => '|o', '' => '}o', '1' => '||' }
665
+ cardinality_max = { '0' => 'o|', '' => 'o{', '1' => '||' }
666
+
667
+ if name.to_s.eql?('id')
668
+ out += "\t *#{name} : #{datatype} <<generated>>\n"
669
+ out += "--\n"
670
+ else
671
+ out += "\t #{mandatory ? '*' : ''}#{column_name} : #{datatype} #{is_fk ? '<<FK>>' : ''}"
672
+ end
673
+
674
+ relations = []
675
+ if property_metadata[:datatype].split(':').first.eql?(graph_prefix.to_s)
676
+ cmin = cardinality_min[(property_metadata[:cardinality][:min]).to_s]
677
+ cmax = cardinality_max[(property_metadata[:cardinality][:max]).to_s]
678
+
679
+ ref_table_name = [property_metadata[:datatype].split(':').last.to_sym,
680
+ property_metadata[:path].split(':').last.classify.to_sym].map do |m|
681
+ data[:entities][m].nil? ? nil : data[:entities][m][:plural].underscore
682
+ end.compact.first
683
+
684
+ relations << "#{table_name} #{cmin}--#{cmax} #{ref_table_name} "
685
+ end
686
+ out = { out: out, relations: relations, references: references }
687
+ end
688
+
689
+ out
690
+ },
691
+ type: datatype,
692
+ foreign_key: is_fk,
693
+ mandatory: mandatory,
694
+ references: references,
695
+ cardinality: property_metadata[:cardinality],
696
+ comment: property_metadata[:description]&.gsub('\'', '')&.gsub(/\n|\r/, ' ')
697
+ }
698
+ end
699
+
700
+ {
701
+ table: lambda { |type = :uml|
702
+ out = ''
703
+ case type
704
+ when :sql
705
+ out = "CREATE TABLE #{graph_prefix}.#{table_name}(\n"
706
+ properties.each_with_index do |property, i|
707
+ out += ", \n" if i > 0
708
+ out += property[:column].call(type)
709
+ all_references << property[:references]
710
+ end
711
+ out += "\n);\n"
712
+
713
+ unless table_comment.nil? || table_comment.empty?
714
+ out += "COMMENT ON TABLE #{graph_prefix}.#{table_name} '#{table_comment}';\n"
715
+ end
716
+
717
+ properties.each_with_index do |property, i|
718
+ if property.key?(:comment) && !property[:comment].empty?
719
+ out += "COMMENT ON COLUMN #{graph_prefix}.#{table_name}.#{property[:column_name]} IS '#{property[:comment]}';\n"
720
+ end
721
+ end
722
+
723
+ out = { out: out, references: all_references.compact.sort.each_with_object(Hash.new(0)) { |o, h| h[o] += 1 } }
724
+ else
725
+ out += "entity \"#{table_name}\" as #{table_name}"
726
+ unless properties.nil? || properties.empty?
727
+ out += "{\n"
728
+ relations = []
729
+ properties.each_with_index do |property, i|
730
+ d = property[:column].call(:uml)
731
+ out += "\n" if i > 1
732
+ out += d[:out]
733
+ relations += d[:relations]
734
+ all_references << property[:references]
735
+ end
736
+ out += "\n}\n"
737
+ out = { out: out, relations: relations }
738
+ end
739
+ end
740
+
741
+ out
742
+ },
743
+ schema: graph_prefix,
744
+ entity_name: entity_name,
745
+ name: table_name,
746
+ comment: table_comment,
747
+ properties: properties
748
+ }
749
+ end
750
+ end
751
+
752
+ def header(data)
753
+ out = data[:ontologies][:all].map do |k, v|
754
+ "@prefix #{k}: <#{v[:uri]}> ."
755
+ end.join("\n")
756
+
757
+ "#{out}\n"
758
+ end
759
+ end
760
+
761
+ data = read_sheets(key, spreadsheet_id, options)
762
+
763
+ shacl = build_shacl(data)
764
+ plantuml = build_plantuml(data)
765
+ #plantuml_erd = build_plantuml_erd(data)
766
+ plantuml_erd = build_erd(data, :uml)
767
+ schema = build_schema(data)
768
+ inflections = build_inflections(data)
769
+ sql = build_erd(data, :sql)
770
+ #erd = build_erd(data, :uml)
771
+ { inflections: inflections, shacl: shacl, schema: schema, plantuml: plantuml,
772
+ plantuml_erd: plantuml_erd, sql: sql }
773
+ end
774
+ end
775
+ end
776
+ end
777
+ end