solis 0.126.0 → 0.128.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce5b23eb71b6b7189377d4981fa8e6dc076128d60aec207e58310fbd162b8a16
4
- data.tar.gz: 57c514df94596d89d76ea012cc946b2b521869f585f1d495609f8e0ec5cbdf71
3
+ metadata.gz: 8200228367f2710bad52af70fd0430ca70ec589fe284e19e22bdfe4e1a659a98
4
+ data.tar.gz: 337d425fa58afb14c95cfd3703d848a9093e563a335f1d37b08d420981389761
5
5
  SHA512:
6
- metadata.gz: 912773a18a5b44da59e1bbb6b7e3472c5d8be9a8044df0b64acbb7da501a3156ead5615363bd23fbd595d42db1b862f3d776235758d92acf8d0942f42f2fde00
7
- data.tar.gz: 90c434c26f367159078555cf9bfbc715d4179ad8f3ebffba12ba379000e7696bf25020cd9a033d90b1b1e95375574bd4600eb3492d67e703a07aaff901d6b251
6
+ metadata.gz: 64cae66429794837abf0e51b72e43847337c195a69b9f6ca5cbffa22bf6888dc472592d7c186d31ca9a7b4eca826d3ecdf1a4c1b0853d047d778cc7931f4ab47
7
+ data.tar.gz: 449810be62118c3def433b580a8e794f3b19ef84413d8b14ca1fbda72c95986ebe8787ddb5a019cb9d63c2dc73f756983b0fc6249977241585c490217a494a12
@@ -0,0 +1,22 @@
1
+ module Solis
2
+ # Normalizes language tags so the same language always ends up under the same key,
3
+ # no matter if it came from a 'Label_XX' sheet column or from a parsed rdfs:label
4
+ # (RDF.rb lowercases language tags, ex. 'nl-BE' is read back as :'nl-be')
5
+ module LanguageTag
6
+ # 'nl' -> :nl, 'nl_be' -> :'nl-BE', 'zh_hant' -> :'zh-Hant'
7
+ def self.normalize(tag)
8
+ return nil if tag.nil? || tag.to_s.strip.empty?
9
+
10
+ primary, *subtags = tag.to_s.strip.split(/[-_]/)
11
+ subtags.map! do |subtag|
12
+ case subtag.length
13
+ when 2 then subtag.upcase
14
+ when 4 then subtag.capitalize
15
+ else subtag.downcase
16
+ end
17
+ end
18
+
19
+ ([primary.downcase] + subtags).join('-').to_sym
20
+ end
21
+ end
22
+ end
data/lib/solis/model.rb CHANGED
@@ -583,13 +583,17 @@ values ?s {<#{self.graph_id}>}
583
583
  end
584
584
 
585
585
  attribute_data = { name: attribute,
586
+ label: attribute_metadata[:label] || {},
586
587
  data_type: attribute_metadata[:datatype],
587
588
  mandatory: (attribute_metadata[:mincount].to_i > 0),
588
589
  repeatable: (attribute_metadata[:maxcount].to_i > 1 || attribute_metadata[:maxcount].nil?),
589
590
  description: attribute_metadata[:comment]&.value
590
591
  }
591
592
  attribute_data[:order] = attribute_metadata[:order]&.value.to_i if attribute_metadata.key?(:order) && !attribute_metadata[:order].nil?
592
- attribute_data[:group] = attribute_metadata[:group]&.value.gsub(graph_name,'').gsub(/Group$/,'') if attribute_metadata.key?(:group) && !attribute_metadata[:group].nil?
593
+ if attribute_metadata.key?(:group) && !attribute_metadata[:group].nil?
594
+ attribute_data[:group] = { name: attribute_metadata[:group][:name],
595
+ label: attribute_metadata[:group][:label] || {} }
596
+ end
593
597
  attribute_data[:attributes] = cm[:attributes] if cm && cm[:attributes]
594
598
 
595
599
  m[:attributes] << attribute_data
@@ -43,6 +43,12 @@ module Solis
43
43
  raise "_REFERENCES tab must have ['sheeturl', 'description', 'entityrange'] as a header at row 1" unless (%w[sheeturl description entityrange] - references.header).length == 0
44
44
  end
45
45
 
46
+ if sheets.key?('_CATEGORIES')
47
+ categories = sheets['_CATEGORIES']
48
+ has_name = categories.header.include?('name') || categories.header.any? { |h| language_column('name').match?(h.to_s) }
49
+ raise "_CATEGORIES tab must have ['name'] or ['name_XX', ...] as a header at row 1" unless has_name
50
+ end
51
+
46
52
  sheets.each do |sheet_name, sheet|
47
53
  if sheet_name !~ /^_/
48
54
  entities = sheets[sheet_name]
@@ -113,6 +119,7 @@ module Solis
113
119
  category: e['categories'],
114
120
  plural: e['nameplural'],
115
121
  label: e['name'].to_s.strip,
122
+ labels: parse_labels(e),
116
123
  sub_class_of: e['subclassof'].nil? || e['subclassof'].empty? ? [] : [e['subclassof']],
117
124
  same_as: e['sameas'],
118
125
  properties: entity_data })
@@ -135,6 +142,29 @@ module Solis
135
142
  raise Solis::Error::GeneralError, e.message
136
143
  end
137
144
 
145
+ # Matches the '<prefix>_XX' columns (header is downcased by ::Sheet#header) where
146
+ # XX is a language tag, ex. 'Label_NL', 'Label_EN', 'Label_nl-BE', 'Name_nl'
147
+ def language_column(prefix)
148
+ /\A#{Regexp.escape(prefix.to_s.downcase)}_(?<language>[a-z]{2,3}([-_][a-z0-9]{2,8})*)\z/
149
+ end
150
+
151
+ # Collects all 'Label_XX' (or '<prefix>_XX') columns of a row into { nl: 'Publiceren', en: 'Publish' }
152
+ def parse_labels(row, prefix = 'label')
153
+ labels = {}
154
+ return labels unless row.respond_to?(:each_pair)
155
+
156
+ column_pattern = language_column(prefix)
157
+ row.each_pair do |column, value|
158
+ match = column_pattern.match(column.to_s)
159
+ next if match.nil?
160
+ next if value.nil? || value.to_s.strip.empty?
161
+
162
+ labels[Solis::LanguageTag.normalize(match[:language])] = value.to_s.strip
163
+ end
164
+
165
+ labels
166
+ end
167
+
138
168
  def parse_entity_data(entity_name, graph_prefix, _graph_name, e, options = {})
139
169
  properties = {}
140
170
  entity_data = e
@@ -168,31 +198,9 @@ module Solis
168
198
  same_as: p['sameas'],
169
199
  order: p['order'],
170
200
  category: p['categories'],
201
+ label: parse_labels(p),
171
202
  description: p['description']
172
203
  }
173
-
174
- # unless graph_prefix.eql?(datatype_prefix.to_sym)
175
- # prefixes = options[:prefixes]
176
- # if prefixes.key?(datatype_prefix.to_sym) && !prefixes[datatype_prefix.to_sym][:sheet_url].empty?
177
- # tmp = URI(prefixes[datatype_prefix.to_sym][:sheet_url]).path.split('/')
178
- # spreadsheet_id = tmp[tmp.index('d') + 1]
179
- #
180
- # processed_remote_sheet = {}
181
- # if prefixes[datatype_prefix.to_sym].key?(:data) && !prefixes[datatype_prefix.to_sym][:data].empty?
182
- # processed_remote_sheet = prefixes[datatype_prefix.to_sym][:data]
183
- # else
184
- # if options[:follow]
185
- # sleep 30
186
- # remote_sheet = read_sheets(options[:key], spreadsheet_id, { from_cache: true })
187
- # processed_remote_sheet = process_sheet(options[:key], remote_sheet, {follow: false})
188
- # prefixes[datatype_prefix.to_sym][:data] = processed_remote_sheet
189
- # end
190
- # end
191
- #
192
- # processed_remote_sheet
193
- # end
194
- # end
195
-
196
204
  end
197
205
  end
198
206
  end
@@ -266,12 +274,32 @@ hide empty members
266
274
  datatypes[datatype][as]
267
275
  end
268
276
 
269
- def build_shacl(datas)
277
+ # Turns { nl: 'Publiceren', en: 'Publish' } into a single rdfs:label statement with
278
+ # one language tagged literal per language, ex.
279
+ # rdfs:label "Publiceren"@nl, "Publish"@en ;
280
+ def build_labels(labels, rdfs_prefix, indent)
281
+ return '' if labels.nil? || labels.empty?
282
+
283
+ literals = labels.map do |language, value|
284
+ %("#{value.to_s.gsub('"', "'").gsub(/\n|\r/, '')}"@#{language})
285
+ end
286
+
287
+ "\n#{indent}#{rdfs_prefix}:label #{literals.join(', ')} ;"
288
+ end
289
+
290
+ def build_shacl(datas, categories = [])
270
291
  shacl_prefix = datas.first[:ontologies][:all].select { |_, v| v[:uri] =~ /shacl/ }.keys.first
271
292
  shacl_prefix = 'sh' if shacl_prefix.nil?
272
293
 
294
+ rdfs_prefix = datas.first[:ontologies][:all].select { |_, v| v[:uri] =~ /rdf-schema/ }.keys.first
295
+
273
296
  out = header(datas.first)
274
297
 
298
+ if rdfs_prefix.nil?
299
+ rdfs_prefix = 'rdfs'
300
+ out += "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n"
301
+ end
302
+
275
303
  datas.each do |data|
276
304
  data[:entities].each do |entity_name, metadata|
277
305
  graph_prefix = data[:ontologies][:base][:prefix]
@@ -300,7 +328,7 @@ hide empty members
300
328
  unless node.nil? || node.empty?
301
329
  "\n #{shacl_prefix}:node #{node} ;"
302
330
  end}
303
- #{shacl_prefix}:name "#{label}" ;
331
+ #{shacl_prefix}:name "#{label}" ;#{build_labels(metadata[:labels], rdfs_prefix, ' ' * 4)}
304
332
  )
305
333
  metadata[:properties].each do |property, property_metadata|
306
334
  attribute = property.to_s.strip
@@ -314,15 +342,16 @@ hide empty members
314
342
  order = property_metadata.key?(:order) && property_metadata[:order] ? property_metadata[:order]&.strip : nil
315
343
  category = property_metadata.key?(:category) && property_metadata[:category] ? property_metadata[:category]&.strip : nil
316
344
  category = category.nil? || category.empty? ? nil : category
345
+ labels = build_labels(property_metadata[:label], rdfs_prefix, ' ' * 17)
317
346
 
318
347
  unless category.nil?
319
- group = "#{category.classify}Group"
348
+ group = "#{category.camelize}Group"
320
349
  groups[group] = category unless groups.key?(group)
321
350
  end
322
351
 
323
352
  if datatype =~ /^#{graph_prefix}:/ || datatype =~ /^<#{graph_name}/
324
353
  out += %( #{shacl_prefix}:property [#{shacl_prefix}:path #{path} ;
325
- #{shacl_prefix}:name "#{attribute}" ;
354
+ #{shacl_prefix}:name "#{attribute}" ;#{labels}
326
355
  #{shacl_prefix}:description "#{description}" ;#{order.nil? ? '' : "\n #{shacl_prefix}:order #{order} ;"}
327
356
  #{category.nil? ? '' : "\n #{shacl_prefix}:group #{graph_prefix}:#{group} ;"}
328
357
  #{shacl_prefix}:nodeKind #{shacl_prefix}:IRI ;
@@ -332,7 +361,7 @@ hide empty members
332
361
  else
333
362
  if datatype.eql?('rdf:langString') && max_count.eql?('1')
334
363
  out += %( #{shacl_prefix}:property [#{shacl_prefix}:path #{path} ;
335
- #{shacl_prefix}:name "#{attribute}";
364
+ #{shacl_prefix}:name "#{attribute}";#{labels}
336
365
  #{shacl_prefix}:description "#{description}" ;#{order.nil? ? '' : "\n #{shacl_prefix}:order #{order} ;"}
337
366
  #{shacl_prefix}:uniqueLang true ;
338
367
  #{shacl_prefix}:datatype #{datatype} ;#{min_count =~ /\d+/ ? "\n #{shacl_prefix}:minCount #{min_count} ;" : ''}
@@ -340,7 +369,7 @@ hide empty members
340
369
  )
341
370
  elsif datatype.eql?('rdf:langString')
342
371
  out += %( #{shacl_prefix}:property [#{shacl_prefix}:path #{path} ;
343
- #{shacl_prefix}:name "#{attribute}";
372
+ #{shacl_prefix}:name "#{attribute}";#{labels}
344
373
  #{shacl_prefix}:description "#{description}" ;#{order.nil? ? '' : "\n #{shacl_prefix}:order #{order} ;"}
345
374
  #{category.nil? ? '' : "\n #{shacl_prefix}:group #{graph_prefix}:#{group} ;"}
346
375
  #{shacl_prefix}:datatype #{datatype} ;#{min_count =~ /\d+/ ? "\n #{shacl_prefix}:minCount #{min_count} ;" : ''}#{max_count =~ /\d+/ ? "\n #{shacl_prefix}:maxCount #{max_count} ;" : ''}
@@ -348,7 +377,7 @@ hide empty members
348
377
  )
349
378
  else
350
379
  out += %( #{shacl_prefix}:property [#{shacl_prefix}:path #{path} ;
351
- #{shacl_prefix}:name "#{attribute}";
380
+ #{shacl_prefix}:name "#{attribute}";#{labels}
352
381
  #{shacl_prefix}:description "#{description}" ;#{order.nil? ? '' : "\n #{shacl_prefix}:order #{order} ;"}
353
382
  #{shacl_prefix}:datatype #{datatype} ;#{min_count =~ /\d+/ ? "\n #{shacl_prefix}:minCount #{min_count} ;" : ''}#{max_count =~ /\d+/ ? "\n #{shacl_prefix}:maxCount #{max_count} ;" : ''}
354
383
  ] ;
@@ -360,10 +389,17 @@ hide empty members
360
389
  out += ".\n"
361
390
 
362
391
  groups.each do |group, category|
392
+ # use the per language names from _CATEGORIES when the category is referenced by any of them
393
+ category_labels = categories.find { |c| c[:name].eql?(category) || c[:labels].value?(category) }&.dig(:labels)
394
+ group_label = if category_labels.nil? || category_labels.empty?
395
+ %(#{rdfs_prefix}:label "#{category}")
396
+ else
397
+ build_labels(category_labels, rdfs_prefix, '').strip.chomp(' ;')
398
+ end
363
399
  out += %(
364
400
  #{graph_prefix}:#{group}
365
- a sh:PropertyGroup ;
366
- rdfs:label "#{category}" .
401
+ a #{shacl_prefix}:PropertyGroup ;
402
+ #{group_label} .
367
403
 
368
404
  )
369
405
  end
@@ -489,7 +525,7 @@ hide empty members
489
525
  puts e.message
490
526
  end
491
527
 
492
- def build_inflections(datas)
528
+ def build_inflections(datas, categories = [])
493
529
  inflections = {}
494
530
  datas.each do |data|
495
531
  data[:entities].each do |entity, metadata|
@@ -498,6 +534,16 @@ hide empty members
498
534
  end
499
535
  end
500
536
 
537
+ # categories are uninflected, singular and plural are the same word.
538
+ # an entity by the same name keeps its own plural.
539
+ # categories.each do |category|
540
+ # name = category[:name].to_s.strip
541
+ # next if name.empty?
542
+ #
543
+ # inflections[name.to_sym] = name unless inflections.key?(name.to_sym) || inclections.value?(name.to_s)
544
+ # inflections[name.underscore.to_sym] = name.underscore unless inflections.key?(name.underscore.to_sym) || inclections.value?(name.to_s)
545
+ # end
546
+
501
547
  inflections.to_json
502
548
  rescue StandardError => e
503
549
  puts e.message
@@ -743,6 +789,23 @@ hide empty members
743
789
 
744
790
  options[:prefixes] = prefixes
745
791
  options[:metadata] = metadata
792
+
793
+ # like _REFERENCES, _CATEGORIES only lives in the main sheet
794
+ # a category can have a name per language ('Name_nl', 'Name_en', ...) next to, or instead of, 'Name'.
795
+ # entity sheets refer to a category by 'Name' or, when missing, by the first 'Name_XX' column.
796
+ categories = []
797
+ if sheet_data.is_a?(Hash) && sheet_data.key?('_CATEGORIES')
798
+ categories = sheet_data['_CATEGORIES'].map do |c|
799
+ labels = parse_labels(c, 'name')
800
+ name = c['name'].to_s.strip
801
+ name = labels.values.first.to_s if name.empty?
802
+ next if name.empty?
803
+
804
+ { name: name, labels: labels }
805
+ end.compact
806
+ end
807
+ Solis::LOGGER.info("Found #{categories.length} categories") unless categories.empty?
808
+
746
809
  #TODO: cleanup
747
810
  if sheet_data.is_a?(Hash)
748
811
  raise "No _REFERENCES sheet found" unless sheet_data.key?("_REFERENCES")
@@ -780,13 +843,13 @@ hide empty members
780
843
  end
781
844
 
782
845
  Solis::LOGGER.info('Generating SHACL')
783
- shacl = build_shacl(datas)
846
+ shacl = build_shacl(datas, categories)
784
847
  Solis::LOGGER.info('Generating PLANTUML')
785
848
  plantuml = build_plantuml(datas)
786
849
  Solis::LOGGER.info('Generating SCHEMA')
787
850
  schema = build_schema(datas)
788
851
  Solis::LOGGER.info('Generating INFLECTIONS')
789
- inflections = build_inflections(datas)
852
+ inflections = build_inflections(datas, categories)
790
853
  Solis::LOGGER.info('Generating JSON SCHEMA')
791
854
  json_schema = build_json_schema(shacl)
792
855
 
data/lib/solis/shape.rb CHANGED
@@ -14,6 +14,8 @@ module Solis
14
14
  parse_solution(shapes, solution)
15
15
  end
16
16
 
17
+ add_labels(shapes, graph)
18
+
17
19
  # shapes = add_missing_attributes(shapes)
18
20
  shapes
19
21
  rescue Solis::Error::GeneralError => e
@@ -137,6 +139,60 @@ module Solis
137
139
  shapes[shape_name] = shape
138
140
  end
139
141
 
142
+ # Reads the rdfs:label statements of every node shape and property shape into
143
+ # { nl: 'Publiceren', en: 'Publish' }. This is a second pass on purpose: adding
144
+ # rdfs:label to the main query would multiply every solution by the number of
145
+ # languages.
146
+ def add_labels(shapes, graph)
147
+ graph.query([nil, RDF.type, RDF::Vocab::SHACL.NodeShape]) do |node_shape|
148
+ shape_name = name_of(graph, node_shape.subject)
149
+ shape = shapes[shape_name]
150
+ next if shape.nil?
151
+
152
+ shape[:label] = labels_of(graph, node_shape.subject)
153
+
154
+ graph.query([node_shape.subject, RDF::Vocab::SHACL.property, nil]) do |property|
155
+ attribute = shape[:attributes][name_of(graph, property.object)]
156
+ next if attribute.nil?
157
+
158
+ attribute[:label] = labels_of(graph, property.object)
159
+ attribute[:group] = group_of(graph, attribute[:group]) unless attribute[:group].nil?
160
+ end
161
+ end
162
+
163
+ shapes
164
+ end
165
+
166
+ # Turns a sh:PropertyGroup URI into
167
+ # { uri: RDF::URI, name: 'OpleidingEnLoopbaan', label: { nl: 'opleiding_en_loopbaan', en: 'education_and_career' } }
168
+ def group_of(graph, group)
169
+ return group if group.is_a?(Hash)
170
+
171
+ {
172
+ uri: group,
173
+ name: group.to_s.split(%r{[/#]}).last.to_s.gsub(/Group$/, ''),
174
+ label: labels_of(graph, group)
175
+ }
176
+ end
177
+
178
+ def name_of(graph, subject)
179
+ graph.query([subject, RDF::Vocab::SHACL.name, nil]).first&.object&.value
180
+ end
181
+
182
+ def labels_of(graph, subject)
183
+ labels = {}
184
+
185
+ graph.query([subject, RDF::RDFS.label, nil]).each do |statement|
186
+ literal = statement.object
187
+ language = Solis::LanguageTag.normalize(literal.language) if literal.is_a?(RDF::Literal)
188
+ next if language.nil?
189
+
190
+ labels[language] = literal.value
191
+ end
192
+
193
+ labels
194
+ end
195
+
140
196
  def query
141
197
  SPARQL.parse %(
142
198
  PREFIX sh: <http://www.w3.org/ns/shacl#>
data/lib/solis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Solis
2
- VERSION = "0.126.0"
2
+ VERSION = "0.128.0"
3
3
  end
data/lib/solis.rb CHANGED
@@ -7,6 +7,7 @@ require "solis/version"
7
7
  require 'solis/config_file'
8
8
  require "solis/error"
9
9
  require 'solis/rdf_edtf_literal'
10
+ require 'solis/language_tag'
10
11
  require 'solis/graph'
11
12
  require 'solis/shape'
12
13
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.126.0
4
+ version: 0.128.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mehmet Celik
@@ -307,6 +307,7 @@ files:
307
307
  - lib/solis/error/not_found_error.rb
308
308
  - lib/solis/error/query_error.rb
309
309
  - lib/solis/graph.rb
310
+ - lib/solis/language_tag.rb
310
311
  - lib/solis/model.rb
311
312
  - lib/solis/model.rb.ok
312
313
  - lib/solis/options.rb