unitsdb 0.1.1 → 2.1.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 +4 -4
- data/.github/workflows/dependent-repos.json +5 -0
- data/.github/workflows/depenedent-gems.yml +16 -0
- data/.gitmodules +3 -0
- data/.rspec +2 -1
- data/.rubocop_todo.yml +88 -12
- data/Gemfile +2 -2
- data/README.adoc +697 -1
- data/exe/unitsdb +7 -0
- data/lib/unitsdb/cli.rb +81 -0
- data/lib/unitsdb/commands/_modify.rb +22 -0
- data/lib/unitsdb/commands/base.rb +52 -0
- data/lib/unitsdb/commands/check_si.rb +124 -0
- data/lib/unitsdb/commands/get.rb +133 -0
- data/lib/unitsdb/commands/normalize.rb +81 -0
- data/lib/unitsdb/commands/release.rb +112 -0
- data/lib/unitsdb/commands/search.rb +219 -0
- data/lib/unitsdb/commands/si_formatter.rb +485 -0
- data/lib/unitsdb/commands/si_matcher.rb +470 -0
- data/lib/unitsdb/commands/si_ttl_parser.rb +100 -0
- data/lib/unitsdb/commands/si_updater.rb +212 -0
- data/lib/unitsdb/commands/validate/identifiers.rb +40 -0
- data/lib/unitsdb/commands/validate/references.rb +316 -0
- data/lib/unitsdb/commands/validate/si_references.rb +115 -0
- data/lib/unitsdb/commands/validate.rb +40 -0
- data/lib/unitsdb/config.rb +19 -0
- data/lib/unitsdb/database.rb +661 -0
- data/lib/unitsdb/dimension.rb +19 -25
- data/lib/unitsdb/dimension_details.rb +20 -0
- data/lib/unitsdb/dimension_reference.rb +8 -0
- data/lib/unitsdb/dimensions.rb +3 -6
- data/lib/unitsdb/errors.rb +13 -0
- data/lib/unitsdb/external_reference.rb +14 -0
- data/lib/unitsdb/identifier.rb +8 -0
- data/lib/unitsdb/localized_string.rb +17 -0
- data/lib/unitsdb/prefix.rb +11 -12
- data/lib/unitsdb/prefix_reference.rb +10 -0
- data/lib/unitsdb/prefixes.rb +3 -6
- data/lib/unitsdb/quantities.rb +3 -27
- data/lib/unitsdb/quantity.rb +12 -24
- data/lib/unitsdb/quantity_reference.rb +4 -7
- data/lib/unitsdb/root_unit_reference.rb +14 -0
- data/lib/unitsdb/scale.rb +17 -0
- data/lib/unitsdb/scale_properties.rb +12 -0
- data/lib/unitsdb/scale_reference.rb +10 -0
- data/lib/unitsdb/scales.rb +11 -0
- data/lib/unitsdb/si_derived_base.rb +13 -14
- data/lib/unitsdb/symbol_presentations.rb +14 -0
- data/lib/unitsdb/unit.rb +20 -26
- data/lib/unitsdb/unit_reference.rb +5 -8
- data/lib/unitsdb/unit_system.rb +8 -10
- data/lib/unitsdb/unit_system_reference.rb +10 -0
- data/lib/unitsdb/unit_systems.rb +3 -16
- data/lib/unitsdb/units.rb +3 -6
- data/lib/unitsdb/utils.rb +84 -0
- data/lib/unitsdb/version.rb +1 -1
- data/lib/unitsdb.rb +13 -10
- data/unitsdb.gemspec +6 -1
- metadata +112 -12
- data/lib/unitsdb/dimension_quantity.rb +0 -28
- data/lib/unitsdb/dimension_symbol.rb +0 -22
- data/lib/unitsdb/prefix_symbol.rb +0 -12
- data/lib/unitsdb/root_unit.rb +0 -17
- data/lib/unitsdb/root_units.rb +0 -20
- data/lib/unitsdb/symbol.rb +0 -17
- data/lib/unitsdb/unit_symbol.rb +0 -15
- data/lib/unitsdb/unitsdb.rb +0 -6
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module Unitsdb
|
7
|
+
module Commands
|
8
|
+
# Updater for SI references in YAML
|
9
|
+
module SiUpdater
|
10
|
+
SI_AUTHORITY = "si-digital-framework"
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
# Update references in YAML file (TTL → DB direction)
|
15
|
+
def update_references(entity_type, missing_matches, db_entities, output_file, include_potential = false)
|
16
|
+
# Use the database objects to access the data directly
|
17
|
+
original_yaml_file = db_entities.first.send(:yaml_file) if db_entities&.first.respond_to?(:yaml_file, true)
|
18
|
+
|
19
|
+
# If we can't get the path from the database object, use the output file path as a fallback
|
20
|
+
if original_yaml_file.nil? || !File.exist?(original_yaml_file)
|
21
|
+
puts "Warning: Could not determine original YAML file path. Using output file as template."
|
22
|
+
original_yaml_file = output_file
|
23
|
+
|
24
|
+
# Create an empty template if output file doesn't exist
|
25
|
+
unless File.exist?(original_yaml_file)
|
26
|
+
FileUtils.mkdir_p(File.dirname(original_yaml_file))
|
27
|
+
File.write(original_yaml_file, { entity_type => [] }.to_yaml)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Load the original YAML file
|
32
|
+
yaml_content = File.read(original_yaml_file)
|
33
|
+
output_data = YAML.safe_load(yaml_content)
|
34
|
+
|
35
|
+
# Group by entity ID to avoid duplicates
|
36
|
+
grouped_matches = missing_matches.group_by { |match| match[:entity_id] }
|
37
|
+
|
38
|
+
# Process each entity that needs updating
|
39
|
+
grouped_matches.each do |entity_id, matches|
|
40
|
+
# Filter matches based on include_potential parameter
|
41
|
+
filtered_matches = matches.select do |match|
|
42
|
+
# Check if it's an exact match or if we're including potential matches
|
43
|
+
match_details = match[:match_details]
|
44
|
+
if match_details&.dig(:exact) == false || %w[symbol_match
|
45
|
+
partial_match].include?(match_details&.dig(:match_desc) || "")
|
46
|
+
include_potential
|
47
|
+
else
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Skip if no matches after filtering
|
53
|
+
next if filtered_matches.empty?
|
54
|
+
|
55
|
+
# Find the entity in the array under the entity_type key
|
56
|
+
entity_index = output_data[entity_type].find_index do |e|
|
57
|
+
# Find entity with matching identifier
|
58
|
+
e["identifiers"]&.any? { |id| id["id"] == entity_id }
|
59
|
+
end
|
60
|
+
|
61
|
+
next unless entity_index
|
62
|
+
|
63
|
+
# Get the entity
|
64
|
+
entity = output_data[entity_type][entity_index]
|
65
|
+
|
66
|
+
# Initialize references array if it doesn't exist
|
67
|
+
entity["references"] ||= []
|
68
|
+
|
69
|
+
# Add new references
|
70
|
+
filtered_matches.each do |match|
|
71
|
+
# If this match has multiple SI references, add them all
|
72
|
+
if match[:multiple_si]
|
73
|
+
match[:multiple_si].each do |si_data|
|
74
|
+
# Check if reference already exists
|
75
|
+
next if entity["references"].any? do |ref|
|
76
|
+
ref["uri"] == si_data[:uri] && ref["authority"] == SI_AUTHORITY
|
77
|
+
end
|
78
|
+
|
79
|
+
# Add new reference
|
80
|
+
entity["references"] << {
|
81
|
+
"uri" => si_data[:uri],
|
82
|
+
"type" => "normative",
|
83
|
+
"authority" => SI_AUTHORITY
|
84
|
+
}
|
85
|
+
end
|
86
|
+
else
|
87
|
+
# Check if reference already exists
|
88
|
+
next if entity["references"].any? do |ref|
|
89
|
+
ref["uri"] == match[:si_uri] && ref["authority"] == SI_AUTHORITY
|
90
|
+
end
|
91
|
+
|
92
|
+
# Add new reference
|
93
|
+
entity["references"] << {
|
94
|
+
"uri" => match[:si_uri],
|
95
|
+
"type" => "normative",
|
96
|
+
"authority" => SI_AUTHORITY
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
write_yaml_file(output_file, output_data)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Update references in YAML file (DB → TTL direction)
|
106
|
+
def update_db_references(entity_type, missing_refs, output_file, include_potential = false)
|
107
|
+
# Try to get the original YAML file from the first entity
|
108
|
+
first_entity = missing_refs.first&.dig(:db_entity)
|
109
|
+
original_yaml_file = first_entity.send(:yaml_file) if first_entity.respond_to?(:yaml_file, true)
|
110
|
+
|
111
|
+
# If we can't get the path from the database object, use the output file path as a fallback
|
112
|
+
if original_yaml_file.nil? || !File.exist?(original_yaml_file)
|
113
|
+
puts "Warning: Could not determine original YAML file path. Using output file as template."
|
114
|
+
original_yaml_file = output_file
|
115
|
+
|
116
|
+
# Create an empty template if output file doesn't exist
|
117
|
+
unless File.exist?(original_yaml_file)
|
118
|
+
FileUtils.mkdir_p(File.dirname(original_yaml_file))
|
119
|
+
File.write(original_yaml_file, { entity_type => [] }.to_yaml)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Load the original YAML file
|
124
|
+
yaml_content = File.read(original_yaml_file)
|
125
|
+
output_data = YAML.safe_load(yaml_content)
|
126
|
+
|
127
|
+
# Group by entity ID to avoid duplicates
|
128
|
+
missing_refs_by_id = {}
|
129
|
+
|
130
|
+
missing_refs.each do |match|
|
131
|
+
entity_id = match[:entity_id] || match[:db_entity].short
|
132
|
+
ttl_entities = match[:ttl_entities]
|
133
|
+
match_types = match[:match_types] || {}
|
134
|
+
|
135
|
+
# Filter TTL entities based on include_potential parameter
|
136
|
+
filtered_ttl_entities = ttl_entities.select do |ttl_entity|
|
137
|
+
# Check if it's an exact match or if we're including potential matches
|
138
|
+
match_type = match_types[ttl_entity[:uri]] || "Exact match" # Default to exact match
|
139
|
+
match_pair_key = "#{entity_id}:#{ttl_entity[:uri]}"
|
140
|
+
match_details = Unitsdb::Commands::SiMatcher.instance_variable_get(:@match_details)&.dig(match_pair_key)
|
141
|
+
|
142
|
+
if match_details && %w[symbol_match partial_match].include?(match_details[:match_desc])
|
143
|
+
include_potential
|
144
|
+
else
|
145
|
+
match_type == "Exact match" || include_potential
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Skip if no entities after filtering
|
150
|
+
next if filtered_ttl_entities.empty?
|
151
|
+
|
152
|
+
missing_refs_by_id[entity_id] ||= []
|
153
|
+
|
154
|
+
# Add filtered matching TTL entities for this DB entity
|
155
|
+
filtered_ttl_entities.each do |ttl_entity|
|
156
|
+
missing_refs_by_id[entity_id] << {
|
157
|
+
uri: ttl_entity[:uri],
|
158
|
+
type: "normative",
|
159
|
+
authority: SI_AUTHORITY
|
160
|
+
}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Update the YAML content
|
165
|
+
output_data[entity_type].each do |entity_yaml|
|
166
|
+
# Find entity by ID or short
|
167
|
+
entity_id = if entity_yaml["identifiers"]
|
168
|
+
begin
|
169
|
+
entity_yaml["identifiers"].first["id"]
|
170
|
+
rescue StandardError
|
171
|
+
nil
|
172
|
+
end
|
173
|
+
elsif entity_yaml["id"]
|
174
|
+
entity_yaml["id"]
|
175
|
+
end
|
176
|
+
|
177
|
+
next unless entity_id && missing_refs_by_id.key?(entity_id)
|
178
|
+
|
179
|
+
# Add references
|
180
|
+
entity_yaml["references"] ||= []
|
181
|
+
|
182
|
+
missing_refs_by_id[entity_id].each do |ref|
|
183
|
+
# Check if this reference already exists
|
184
|
+
next if entity_yaml["references"].any? do |existing_ref|
|
185
|
+
existing_ref["uri"] == ref[:uri] &&
|
186
|
+
existing_ref["authority"] == ref[:authority]
|
187
|
+
end
|
188
|
+
|
189
|
+
# Add the reference
|
190
|
+
entity_yaml["references"] << {
|
191
|
+
"uri" => ref[:uri],
|
192
|
+
"type" => ref[:type],
|
193
|
+
"authority" => ref[:authority]
|
194
|
+
}
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
write_yaml_file(output_file, output_data)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Helper to write YAML file
|
202
|
+
def write_yaml_file(output_file, output_data)
|
203
|
+
# Ensure the output directory exists
|
204
|
+
output_dir = File.dirname(output_file)
|
205
|
+
FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
|
206
|
+
|
207
|
+
# Write to YAML file
|
208
|
+
File.write(output_file, output_data.to_yaml)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unitsdb
|
4
|
+
module Commands
|
5
|
+
module Validate
|
6
|
+
class Identifiers < Base
|
7
|
+
def run
|
8
|
+
db = load_database
|
9
|
+
all_dups = db.validate_uniqueness
|
10
|
+
|
11
|
+
display_results(all_dups)
|
12
|
+
rescue Unitsdb::Errors::DatabaseError => e
|
13
|
+
puts "Error: #{e.message}"
|
14
|
+
exit(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def display_results(all_dups)
|
20
|
+
%i[short id].each do |type|
|
21
|
+
dups = all_dups[type]
|
22
|
+
if dups.empty?
|
23
|
+
puts "No duplicate '#{type}' fields found."
|
24
|
+
next
|
25
|
+
end
|
26
|
+
|
27
|
+
puts "\nFound duplicate '#{type}' fields:"
|
28
|
+
dups.each do |file, items|
|
29
|
+
puts " #{file}:"
|
30
|
+
items.each do |val, paths|
|
31
|
+
puts " '#{val}':"
|
32
|
+
paths.each { |p| puts " - #{p}" }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unitsdb
|
4
|
+
module Commands
|
5
|
+
module Validate
|
6
|
+
class References < Base
|
7
|
+
def run
|
8
|
+
# Load the database
|
9
|
+
db = load_database(@options[:database])
|
10
|
+
|
11
|
+
# Build registry of all valid IDs
|
12
|
+
registry = build_id_registry(db)
|
13
|
+
|
14
|
+
# Check all references
|
15
|
+
invalid_refs = check_references(db, registry)
|
16
|
+
|
17
|
+
# Display results
|
18
|
+
display_reference_results(invalid_refs, registry)
|
19
|
+
rescue Unitsdb::Errors::DatabaseError => e
|
20
|
+
puts "Error: #{e.message}"
|
21
|
+
exit(1)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def build_id_registry(db)
|
27
|
+
registry = {}
|
28
|
+
|
29
|
+
# Add all unit identifiers to the registry
|
30
|
+
registry["units"] = {}
|
31
|
+
db.units.each_with_index do |unit, index|
|
32
|
+
unit.identifiers.each do |identifier|
|
33
|
+
next unless identifier.id && identifier.type
|
34
|
+
|
35
|
+
# Add the composite key (type:id)
|
36
|
+
composite_key = "#{identifier.type}:#{identifier.id}"
|
37
|
+
registry["units"][composite_key] = "index:#{index}"
|
38
|
+
|
39
|
+
# Also add just the ID for backward compatibility
|
40
|
+
registry["units"][identifier.id] = "index:#{index}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Add dimension identifiers
|
45
|
+
registry["dimensions"] = {}
|
46
|
+
db.dimensions.each_with_index do |dimension, index|
|
47
|
+
dimension.identifiers.each do |identifier|
|
48
|
+
next unless identifier.id && identifier.type
|
49
|
+
|
50
|
+
composite_key = "#{identifier.type}:#{identifier.id}"
|
51
|
+
registry["dimensions"][composite_key] = "index:#{index}"
|
52
|
+
registry["dimensions"][identifier.id] = "index:#{index}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Also track dimensions by short name
|
56
|
+
if dimension.respond_to?(:short) && dimension.short
|
57
|
+
registry["dimensions_short"] ||= {}
|
58
|
+
registry["dimensions_short"][dimension.short] = "index:#{index}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Add quantity identifiers
|
63
|
+
registry["quantities"] = {}
|
64
|
+
db.quantities.each_with_index do |quantity, index|
|
65
|
+
quantity.identifiers.each do |identifier|
|
66
|
+
next unless identifier.id && identifier.type
|
67
|
+
|
68
|
+
composite_key = "#{identifier.type}:#{identifier.id}"
|
69
|
+
registry["quantities"][composite_key] = "index:#{index}"
|
70
|
+
registry["quantities"][identifier.id] = "index:#{index}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Add prefix identifiers
|
75
|
+
registry["prefixes"] = {}
|
76
|
+
db.prefixes.each_with_index do |prefix, index|
|
77
|
+
prefix.identifiers.each do |identifier|
|
78
|
+
next unless identifier.id && identifier.type
|
79
|
+
|
80
|
+
composite_key = "#{identifier.type}:#{identifier.id}"
|
81
|
+
registry["prefixes"][composite_key] = "index:#{index}"
|
82
|
+
registry["prefixes"][identifier.id] = "index:#{index}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Add unit system identifiers
|
87
|
+
registry["unit_systems"] = {}
|
88
|
+
db.unit_systems.each_with_index do |unit_system, index|
|
89
|
+
unit_system.identifiers.each do |identifier|
|
90
|
+
next unless identifier.id && identifier.type
|
91
|
+
|
92
|
+
composite_key = "#{identifier.type}:#{identifier.id}"
|
93
|
+
registry["unit_systems"][composite_key] = "index:#{index}"
|
94
|
+
registry["unit_systems"][identifier.id] = "index:#{index}"
|
95
|
+
end
|
96
|
+
|
97
|
+
# Also track unit systems by short name
|
98
|
+
if unit_system.respond_to?(:short) && unit_system.short
|
99
|
+
registry["unit_systems_short"] ||= {}
|
100
|
+
registry["unit_systems_short"][unit_system.short] = "index:#{index}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Debug registry if requested
|
105
|
+
if @options[:debug_registry]
|
106
|
+
puts "Registry contents:"
|
107
|
+
registry.each do |type, ids|
|
108
|
+
puts " #{type}:"
|
109
|
+
ids.each do |id, location|
|
110
|
+
puts " #{id} => #{location}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
registry
|
116
|
+
end
|
117
|
+
|
118
|
+
def check_references(db, registry)
|
119
|
+
invalid_refs = {}
|
120
|
+
|
121
|
+
# Check unit references in dimensions
|
122
|
+
check_dimension_references(db, registry, invalid_refs)
|
123
|
+
|
124
|
+
# Check unit_system references
|
125
|
+
check_unit_system_references(db, registry, invalid_refs)
|
126
|
+
|
127
|
+
# Check quantity references
|
128
|
+
check_quantity_references(db, registry, invalid_refs)
|
129
|
+
|
130
|
+
# Check root unit references in units
|
131
|
+
check_root_unit_references(db, registry, invalid_refs)
|
132
|
+
|
133
|
+
invalid_refs
|
134
|
+
end
|
135
|
+
|
136
|
+
def check_dimension_references(db, registry, invalid_refs)
|
137
|
+
db.dimensions.each_with_index do |dimension, index|
|
138
|
+
next unless dimension.respond_to?(:dimension_reference) && dimension.dimension_reference
|
139
|
+
|
140
|
+
ref_id = dimension.dimension_reference
|
141
|
+
ref_type = "dimensions"
|
142
|
+
ref_path = "dimensions:index:#{index}:dimension_reference"
|
143
|
+
|
144
|
+
validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "dimensions")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def check_unit_system_references(db, registry, invalid_refs)
|
149
|
+
db.units.each_with_index do |unit, index|
|
150
|
+
next unless unit.respond_to?(:unit_system_reference) && unit.unit_system_reference
|
151
|
+
|
152
|
+
unit.unit_system_reference.each_with_index do |ref_id, idx|
|
153
|
+
ref_type = "unit_systems"
|
154
|
+
ref_path = "units:index:#{index}:unit_system_reference[#{idx}]"
|
155
|
+
|
156
|
+
validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def check_quantity_references(db, registry, invalid_refs)
|
162
|
+
db.units.each_with_index do |unit, index|
|
163
|
+
next unless unit.respond_to?(:quantity_references) && unit.quantity_references
|
164
|
+
|
165
|
+
unit.quantity_references.each_with_index do |ref_id, idx|
|
166
|
+
ref_type = "quantities"
|
167
|
+
ref_path = "units:index:#{index}:quantity_references[#{idx}]"
|
168
|
+
|
169
|
+
validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def check_root_unit_references(db, registry, invalid_refs)
|
175
|
+
db.units.each_with_index do |unit, index|
|
176
|
+
next unless unit.respond_to?(:root_units) && unit.root_units
|
177
|
+
|
178
|
+
unit.root_units.each_with_index do |root_unit, idx|
|
179
|
+
next unless root_unit.respond_to?(:unit_reference) && root_unit.unit_reference
|
180
|
+
|
181
|
+
# Check unit reference
|
182
|
+
ref_id = root_unit.unit_reference
|
183
|
+
ref_type = "units"
|
184
|
+
ref_path = "units:index:#{index}:root_units.#{idx}.unit_reference"
|
185
|
+
|
186
|
+
validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
|
187
|
+
|
188
|
+
# Check prefix reference if present
|
189
|
+
next unless root_unit.respond_to?(:prefix_reference) && root_unit.prefix_reference
|
190
|
+
|
191
|
+
ref_id = root_unit.prefix_reference
|
192
|
+
ref_type = "prefixes"
|
193
|
+
ref_path = "units:index:#{index}:root_units.#{idx}.prefix_reference"
|
194
|
+
|
195
|
+
validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, file_type)
|
201
|
+
# Handle references that are objects with id and type (could be a hash or an object)
|
202
|
+
if ref_id.respond_to?(:id) && ref_id.respond_to?(:type)
|
203
|
+
id = ref_id.id
|
204
|
+
type = ref_id.type
|
205
|
+
composite_key = "#{type}:#{id}"
|
206
|
+
|
207
|
+
# Try multiple lookup strategies
|
208
|
+
valid = false
|
209
|
+
|
210
|
+
# 1. Try exact composite key match
|
211
|
+
valid = true if registry.key?(ref_type) && registry[ref_type].key?(composite_key)
|
212
|
+
|
213
|
+
# 2. Try just ID match if composite didn't work
|
214
|
+
valid = true if !valid && registry.key?(ref_type) && registry[ref_type].key?(id)
|
215
|
+
|
216
|
+
# 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
|
217
|
+
if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
|
218
|
+
registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
|
219
|
+
registry[ref_type].keys.any? { |k| k.end_with?(":SI_#{id.sub("si-", "")}") } ||
|
220
|
+
registry[ref_type].keys.any? { |k| k.end_with?(":non-SI_#{id.sub("nonsi-", "")}") }
|
221
|
+
)
|
222
|
+
# Special handling for unit_systems between unitsml and nist types
|
223
|
+
valid = true
|
224
|
+
end
|
225
|
+
|
226
|
+
if valid
|
227
|
+
puts "Valid reference: #{id} (#{type}) at #{file_type}:#{ref_path}" if @options[:print_valid]
|
228
|
+
else
|
229
|
+
invalid_refs[file_type] ||= {}
|
230
|
+
invalid_refs[file_type][ref_path] = { id: id, type: type, ref_type: ref_type }
|
231
|
+
end
|
232
|
+
# Handle references that are objects with id and type in a hash
|
233
|
+
elsif ref_id.is_a?(Hash) && ref_id.key?("id") && ref_id.key?("type")
|
234
|
+
id = ref_id["id"]
|
235
|
+
type = ref_id["type"]
|
236
|
+
composite_key = "#{type}:#{id}"
|
237
|
+
|
238
|
+
# Try multiple lookup strategies
|
239
|
+
valid = false
|
240
|
+
|
241
|
+
# 1. Try exact composite key match
|
242
|
+
valid = true if registry.key?(ref_type) && registry[ref_type].key?(composite_key)
|
243
|
+
|
244
|
+
# 2. Try just ID match if composite didn't work
|
245
|
+
valid = true if !valid && registry.key?(ref_type) && registry[ref_type].key?(id)
|
246
|
+
|
247
|
+
# 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
|
248
|
+
if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
|
249
|
+
registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
|
250
|
+
registry[ref_type].keys.any? { |k| k.end_with?(":SI_#{id.sub("si-", "")}") } ||
|
251
|
+
registry[ref_type].keys.any? { |k| k.end_with?(":non-SI_#{id.sub("nonsi-", "")}") }
|
252
|
+
)
|
253
|
+
# Special handling for unit_systems between unitsml and nist types
|
254
|
+
valid = true
|
255
|
+
end
|
256
|
+
|
257
|
+
if valid
|
258
|
+
puts "Valid reference: #{id} (#{type}) at #{file_type}:#{ref_path}" if @options[:print_valid]
|
259
|
+
else
|
260
|
+
invalid_refs[file_type] ||= {}
|
261
|
+
invalid_refs[file_type][ref_path] = { id: id, type: type, ref_type: ref_type }
|
262
|
+
end
|
263
|
+
else
|
264
|
+
# Handle plain string references (legacy format)
|
265
|
+
valid = registry.key?(ref_type) && registry[ref_type].key?(ref_id)
|
266
|
+
|
267
|
+
if valid
|
268
|
+
puts "Valid reference: #{ref_id} (#{ref_type}) at #{file_type}:#{ref_path}" if @options[:print_valid]
|
269
|
+
else
|
270
|
+
invalid_refs[file_type] ||= {}
|
271
|
+
invalid_refs[file_type][ref_path] = { id: ref_id, type: ref_type }
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def display_reference_results(invalid_refs, registry)
|
277
|
+
if invalid_refs.empty?
|
278
|
+
puts "All references are valid!"
|
279
|
+
return
|
280
|
+
end
|
281
|
+
|
282
|
+
puts "Found invalid references:"
|
283
|
+
|
284
|
+
# Display registry contents if debug_registry is enabled
|
285
|
+
# This is needed for the failing test
|
286
|
+
if @options[:debug_registry]
|
287
|
+
puts "\nRegistry contents:"
|
288
|
+
registry.each do |type, ids|
|
289
|
+
next if ids.empty?
|
290
|
+
|
291
|
+
puts " #{type}:"
|
292
|
+
ids.each do |id, location|
|
293
|
+
puts " #{id}: {type: #{type.sub("s", "")}, source: #{location}}"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
invalid_refs.each do |file, refs|
|
298
|
+
puts " #{file}:"
|
299
|
+
refs.each do |path, ref|
|
300
|
+
puts " #{path} => '#{ref[:id]}' (#{ref[:type]})"
|
301
|
+
|
302
|
+
# Suggest corrections
|
303
|
+
next unless registry.key?(ref[:ref_type])
|
304
|
+
|
305
|
+
similar_ids = Unitsdb::Utils.find_similar_ids(ref[:id], registry[ref[:ref_type]].keys)
|
306
|
+
if similar_ids.any?
|
307
|
+
puts " Did you mean one of these?"
|
308
|
+
similar_ids.each { |id| puts " - #{id}" }
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../base"
|
4
|
+
|
5
|
+
module Unitsdb
|
6
|
+
module Commands
|
7
|
+
module Validate
|
8
|
+
class SiReferences < Unitsdb::Commands::Base
|
9
|
+
def run
|
10
|
+
# Load the database
|
11
|
+
db = load_database(@options[:database])
|
12
|
+
|
13
|
+
# Check for duplicate SI references
|
14
|
+
duplicates = check_si_references(db)
|
15
|
+
|
16
|
+
# Display results
|
17
|
+
display_duplicate_results(duplicates)
|
18
|
+
rescue Unitsdb::Errors::DatabaseError => e
|
19
|
+
puts "Error: #{e.message}"
|
20
|
+
exit(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def check_si_references(db)
|
26
|
+
duplicates = {}
|
27
|
+
|
28
|
+
# Check units
|
29
|
+
check_entity_si_references(db.units, "units", duplicates)
|
30
|
+
|
31
|
+
# Check quantities
|
32
|
+
check_entity_si_references(db.quantities, "quantities", duplicates)
|
33
|
+
|
34
|
+
# Check prefixes
|
35
|
+
check_entity_si_references(db.prefixes, "prefixes", duplicates)
|
36
|
+
|
37
|
+
duplicates
|
38
|
+
end
|
39
|
+
|
40
|
+
def check_entity_si_references(entities, entity_type, duplicates)
|
41
|
+
# Track SI references by URI
|
42
|
+
si_refs = {}
|
43
|
+
|
44
|
+
entities.each_with_index do |entity, index|
|
45
|
+
# Skip if no references
|
46
|
+
next unless entity.respond_to?(:references) && entity.references
|
47
|
+
|
48
|
+
# Check each reference
|
49
|
+
entity.references.each do |ref|
|
50
|
+
# Only interested in si-digital-framework references
|
51
|
+
next unless ref.authority == "si-digital-framework"
|
52
|
+
|
53
|
+
# Get entity info for display
|
54
|
+
entity_id = if entity.respond_to?(:identifiers) && entity.identifiers&.first.respond_to?(:id)
|
55
|
+
entity.identifiers.first.id
|
56
|
+
else
|
57
|
+
entity.short
|
58
|
+
end
|
59
|
+
|
60
|
+
# Track this reference
|
61
|
+
si_refs[ref.uri] ||= []
|
62
|
+
si_refs[ref.uri] << {
|
63
|
+
entity_id: entity_id,
|
64
|
+
entity_name: entity.respond_to?(:names) ? entity.names.first : entity.short,
|
65
|
+
index: index
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Find duplicates (URIs with more than one entity)
|
71
|
+
si_refs.each do |uri, entities|
|
72
|
+
next unless entities.size > 1
|
73
|
+
|
74
|
+
# Record this duplicate
|
75
|
+
duplicates[entity_type] ||= {}
|
76
|
+
duplicates[entity_type][uri] = entities
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def display_duplicate_results(duplicates)
|
81
|
+
if duplicates.empty?
|
82
|
+
puts "No duplicate SI references found! Each SI reference URI is used by at most one entity of each type."
|
83
|
+
return
|
84
|
+
end
|
85
|
+
|
86
|
+
puts "Found duplicate SI references:"
|
87
|
+
|
88
|
+
duplicates.each do |entity_type, uri_duplicates|
|
89
|
+
puts "\n #{entity_type.capitalize}:"
|
90
|
+
|
91
|
+
uri_duplicates.each do |uri, entities|
|
92
|
+
puts " SI URI: #{uri}"
|
93
|
+
puts " Used by #{entities.size} entities:"
|
94
|
+
|
95
|
+
entities.each do |entity|
|
96
|
+
puts " - #{entity[:entity_id]} (#{entity[:entity_name]}) at index #{entity[:index]}"
|
97
|
+
end
|
98
|
+
puts ""
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
puts "\nEach SI digital framework reference should be used by at most one entity of each type."
|
103
|
+
puts "Please fix the duplicates by either removing the reference from all but one entity,"
|
104
|
+
puts "or by updating the references to use different URIs appropriate for each entity."
|
105
|
+
end
|
106
|
+
|
107
|
+
def load_database(path)
|
108
|
+
Unitsdb::Database.from_db(path)
|
109
|
+
rescue StandardError => e
|
110
|
+
raise Unitsdb::Errors::DatabaseError, "Failed to load database: #{e.message}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|