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,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