glossarist 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 351ee7a983e5566dab9124def66fef23e1a8517b463c695e72388d40939c6031
4
- data.tar.gz: d7f4b103f7a60ed7bb80983d30d8f9bba281a1f6c8ec293dcd73eecdbec8e2bf
3
+ metadata.gz: 2545436241f3fc01daf4879682439d3f4650a44a45af855cc5260ad445caca0b
4
+ data.tar.gz: c62fb0db9a7d1dc9ec4ad4ea9015a53a413bfbc922be77db276e4739926615a6
5
5
  SHA512:
6
- metadata.gz: 4614c9b4457b76d5f7936f9b43a6f97c8ceeb62442aafb4d865e3bdc8aa786493af6062a9cef48e3328085cf38a876822eb0dd48b8ac1d490ad166eeaaf8713e
7
- data.tar.gz: dcdb5aca4bda8c3ebae41a7ef1c3cfcbf3c154fe2e0aa38ea8c1b76e175c08b7c3d070daddbb9e54e3079f324f7b229b5e6f60d77908e1689d05e35ac84347e5
6
+ metadata.gz: d611e1a09d99e64b7ad5e745d729fba2fe59d707f7f0469aa43fa3def04286c91128f5a65088f28943df550ef7eb6099d3354cadbafa7c68a93b08873512e2c1
7
+ data.tar.gz: c1e0ec90c6a41c9a4c26f8db587d0cfbb986fb3c2086494554fbde8dfc829abf0406d2bf4e123d85710c2e23c699ac82b0db9c5328f3498f3c378bd509617c81
data/config.yml CHANGED
@@ -71,7 +71,9 @@ grammar_info:
71
71
  concept:
72
72
  status:
73
73
  - draft
74
+ - submitted
74
75
  - not_valid
76
+ - invalid
75
77
  - valid
76
78
  - superseded
77
79
  - retired
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Collections
5
+ class Collection
6
+ include Enumerable
7
+
8
+ attr_reader :collection
9
+
10
+ alias :size :count
11
+
12
+ def initialize(klass:)
13
+ @klass = klass
14
+ @collection = []
15
+ end
16
+
17
+ def <<(object)
18
+ @collection << @klass.new(object)
19
+ end
20
+
21
+ def each(&block)
22
+ @collection.each(&block)
23
+ end
24
+
25
+ def empty?
26
+ @collection.empty?
27
+ end
28
+
29
+ def clear!
30
+ @collection = []
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Collections
5
+ class DesignationCollection < Collection
6
+ def initialize
7
+ super(klass: Designation::Base)
8
+ end
9
+
10
+ def <<(object)
11
+ @collection << @klass.from_h(object)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,2 +1,4 @@
1
1
  require_relative "collections/asset_collection"
2
2
  require_relative "collections/bibliography_collection"
3
+ require_relative "collections/collection"
4
+ require_relative "collections/designation_collection"
@@ -9,6 +9,8 @@ module Glossarist
9
9
  # @return [String]
10
10
  attr_reader :id
11
11
 
12
+ attr_writer :uuid
13
+
12
14
  # Concept designations.
13
15
  # @todo Alias +terms+ exists only for legacy reasons and will be removed.
14
16
  # @return [Array<Designations::Base>]
@@ -42,23 +44,42 @@ module Glossarist
42
44
  # Contains list of extended attributes
43
45
  attr_accessor :extension_attributes
44
46
 
45
- def initialize(*)
47
+ attr_accessor :lineage_source
48
+ attr_accessor :lineage_source_similarity
49
+
50
+ attr_accessor :release
51
+
52
+ def initialize(*args)
46
53
  @localizations = {}
47
- @sources = []
48
- @related = []
49
- @notes = []
50
- @designations = []
54
+ @sources = Glossarist::Collections::Collection.new(klass: ConceptSource)
55
+ @related = Glossarist::Collections::Collection.new(klass: RelatedConcept)
56
+ @definition = Glossarist::Collections::Collection.new(klass: DetailedDefinition)
57
+ @notes = Glossarist::Collections::Collection.new(klass: DetailedDefinition)
58
+ @examples = Glossarist::Collections::Collection.new(klass: DetailedDefinition)
59
+ @dates = Glossarist::Collections::Collection.new(klass: ConceptDate)
60
+
61
+ @designations = Glossarist::Collections::DesignationCollection.new
51
62
  @extension_attributes = {}
52
63
 
64
+ normalize_args(args)
65
+
53
66
  super
54
67
  end
55
68
 
69
+ def uuid
70
+ @uuid ||= Glossarist::Utilities::UUID.uuid_v5(
71
+ Glossarist::Utilities::UUID::OID_NAMESPACE,
72
+ to_h.to_yaml,
73
+ )
74
+ end
75
+
56
76
  def id=(id)
57
- raise(Glossarist::Error, "id must be a string") unless id.is_a?(String) || id.nil?
77
+ raise(Glossarist::Error, "Expect id to be a string, Got #{id.class} (#{id})") unless id.is_a?(String) || id.nil?
58
78
 
59
79
  @id = id
60
80
  end
61
81
  alias :termid= :id=
82
+ alias :identifier= :id=
62
83
 
63
84
  # List of authorative sources.
64
85
  # @todo Alias +authoritative_source+ exists for legacy reasons and may be
@@ -71,33 +92,40 @@ module Glossarist
71
92
  attr_reader :dates
72
93
 
73
94
  def examples=(examples)
74
- @examples = examples&.map { |e| DetailedDefinition.new(e) }
95
+ @examples.clear!
96
+ examples&.each { |example| @examples << example }
75
97
  end
76
98
 
77
99
  def notes=(notes)
78
- @notes = notes&.map { |n| DetailedDefinition.new(n) }
100
+ @notes.clear!
101
+ notes&.each { |note| @notes << note }
79
102
  end
80
103
 
81
104
  def definition=(definition)
82
- @definition = definition&.map { |d| DetailedDefinition.new(d) }
105
+ @definition.clear!
106
+ definition&.each { |definition| @definition << definition }
83
107
  end
84
108
 
85
109
  def designations=(designations)
86
- @designations = designations&.map do |designation|
87
- Designation::Base.from_h(designation)
88
- end
110
+ @designations.clear!
111
+ designations&.each { |designation| @designations << designation }
89
112
  end
90
113
 
91
114
  alias :terms= :designations=
92
115
 
116
+ def preferred_designations
117
+ @designations.select(&:preferred?)
118
+ end
119
+ alias :preferred_terms :preferred_designations
120
+
93
121
  def dates=(dates)
94
- @dates = dates&.map { |d| ConceptDate.new(d) }
122
+ @dates.clear!
123
+ dates&.each { |date| @dates << date }
95
124
  end
96
125
 
97
126
  def sources=(sources)
98
- @sources = sources&.map do |source|
99
- ConceptSource.new(source)
100
- end || []
127
+ @sources.clear!
128
+ sources&.each { |source| @sources << source }
101
129
  end
102
130
 
103
131
  def authoritative_source=(sources)
@@ -108,14 +136,19 @@ module Glossarist
108
136
 
109
137
  def to_h
110
138
  {
111
- "id" => id,
112
- "related" => related&.map(&:to_h),
113
- "terms" => (terms&.map(&:to_h) || []),
114
- "definition" => definition&.map(&:to_h),
115
- "notes" => notes&.map(&:to_h),
116
- "examples" => examples&.map(&:to_h),
117
- }
118
- .compact
139
+ "data" => {
140
+ "dates" => dates&.map(&:to_h),
141
+ "definition" => definition&.map(&:to_h),
142
+ "examples" => examples&.map(&:to_h),
143
+ "id" => id,
144
+ "lineage_source_similarity" => lineage_source_similarity,
145
+ "notes" => notes&.map(&:to_h),
146
+ "release" => release,
147
+ "sources" => sources.empty? ? nil : sources&.map(&:to_h),
148
+ "terms" => (terms&.map(&:to_h) || []),
149
+ "related" => related&.map(&:to_h),
150
+ }.compact,
151
+ }.compact
119
152
  end
120
153
 
121
154
  # @deprecated For legacy reasons only.
@@ -147,7 +180,28 @@ module Glossarist
147
180
  end
148
181
 
149
182
  def related=(related)
150
- @related = related&.map { |r| RelatedConcept.new(r) } || []
183
+ @related.clear!
184
+ related&.each { |r| @related << r }
185
+ end
186
+
187
+ Glossarist::GlossaryDefinition::CONCEPT_DATE_TYPES.each do |type|
188
+ # Sets the ConceptDate and add it to dates list of the specified type.
189
+ define_method("date_#{type}=") do |date|
190
+ date_hash = {
191
+ "type" => type,
192
+ "date" => date,
193
+ }
194
+ @dates ||= []
195
+ @dates << ConceptDate.new(date_hash)
196
+ end
197
+ end
198
+
199
+ def normalize_args(args)
200
+ args.each do |arg|
201
+ data = arg.delete("data")
202
+
203
+ arg.merge!(data) if data
204
+ end
151
205
  end
152
206
  end
153
207
  end
@@ -12,8 +12,8 @@ module Glossarist
12
12
 
13
13
  def to_h
14
14
  {
15
- "type" => type,
16
15
  "date" => date,
16
+ "type" => type,
17
17
  }.compact
18
18
  end
19
19
  end
@@ -17,7 +17,13 @@ module Glossarist
17
17
  collection ||= ManagedConceptCollection.new
18
18
 
19
19
  Dir.glob(concepts_glob) do |filename|
20
- collection.store(load_concept_from_file(filename))
20
+ concept = load_concept_from_file(filename)
21
+ concept.localized_concepts.each do |_lang, id|
22
+ localized_concept = load_localized_concept(id)
23
+ concept.add_l10n(localized_concept)
24
+ end
25
+
26
+ collection.store(concept)
21
27
  end
22
28
  end
23
29
 
@@ -27,20 +33,49 @@ module Glossarist
27
33
  end
28
34
 
29
35
  def load_concept_from_file(filename)
30
- ManagedConcept.new(Psych.safe_load(File.read(filename)))
36
+ concept_hash = Psych.safe_load(File.read(filename), permitted_classes: [Date])
37
+ concept_hash["uuid"] = concept_hash["id"] || File.basename(filename, ".*")
38
+ ManagedConcept.new(concept_hash)
39
+ rescue Psych::SyntaxError => e
40
+ raise Glossarist::ParseError.new(filename: filename, line: e.line)
41
+ end
42
+
43
+ def load_localized_concept(id)
44
+ concept_hash = Psych.safe_load(
45
+ File.read(localized_concept_path(id)),
46
+ permitted_classes: [Date],
47
+ )
48
+ concept_hash["uuid"] = id
49
+
50
+ Config.class_for(:localized_concept).new(concept_hash)
31
51
  rescue Psych::SyntaxError => e
32
52
  raise Glossarist::ParseError.new(filename: filename, line: e.line)
33
53
  end
34
54
 
35
55
  def save_concept_to_file(concept)
36
- filename = File.join(path, "concept-#{concept.id}.yaml")
56
+ concept_dir = File.join(path, "concept")
57
+ localized_concept_dir = File.join(path, "localized_concept")
58
+
59
+ Dir.mkdir(concept_dir) unless Dir.exist?(concept_dir)
60
+ Dir.mkdir(localized_concept_dir) unless Dir.exist?(localized_concept_dir)
61
+
62
+ filename = File.join(concept_dir, "#{concept.uuid}.yaml")
37
63
  File.write(filename, Psych.dump(concept.to_h))
64
+
65
+ concept.localized_concepts.each do |lang, uuid|
66
+ filename = File.join(localized_concept_dir, "#{uuid}.yaml")
67
+ File.write(filename, Psych.dump(concept.localization(lang).to_h))
68
+ end
38
69
  end
39
70
 
40
71
  private
41
72
 
42
73
  def concepts_glob
43
- File.join(path, "concept-*.{yaml,yml}")
74
+ File.join(path, "concept", "*.{yaml,yml}")
75
+ end
76
+
77
+ def localized_concept_path(id)
78
+ Dir.glob(File.join(path, "localized_concept", "#{id}.{yaml,yml}"))&.first
44
79
  end
45
80
  end
46
81
  end
@@ -37,9 +37,9 @@ module Glossarist
37
37
  origin_hash = self.origin.to_h.empty? ? nil : self.origin.to_h
38
38
 
39
39
  {
40
+ "origin" => origin_hash,
40
41
  "type" => type.to_s,
41
42
  "status" => status&.to_s,
42
- "origin" => origin_hash,
43
43
  "modification" => modification,
44
44
  }.compact
45
45
  end
@@ -24,15 +24,14 @@ module Glossarist
24
24
  # @return [String]
25
25
  attr_accessor :classification
26
26
 
27
- attr_accessor :review_date
28
- attr_accessor :review_decision_date
29
- attr_accessor :review_decision_event
30
-
31
- def initialize(*)
32
- @examples = []
33
-
34
- super
35
- end
27
+ # Temporary fields
28
+ # @todo Need to remove these once the isotc211-glossary is fixed
29
+ attr_accessor *%i[
30
+ review_date
31
+ review_decision_date
32
+ review_decision_event
33
+ review_type
34
+ ]
36
35
 
37
36
  def language_code=(language_code)
38
37
  if language_code.is_a?(String) && language_code.length == 3
@@ -42,10 +41,12 @@ module Glossarist
42
41
  end
43
42
  end
44
43
 
45
- def to_h # rubocop:disable Metrics/MethodLength
46
- super.merge({
47
- "language_code" => language_code,
44
+ def to_h # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
45
+ hash = super
46
+
47
+ hash["data"].merge!({
48
48
  "domain" => domain,
49
+ "language_code" => language_code,
49
50
  "entry_status" => entry_status,
50
51
  "sources" => sources.empty? ? nil : sources&.map(&:to_h),
51
52
  "classification" => classification,
@@ -54,11 +55,13 @@ module Glossarist
54
55
  "review_decision_date" => review_decision_date,
55
56
  "review_decision_event" => review_decision_event,
56
57
  }.compact).merge(@extension_attributes)
58
+
59
+ hash
57
60
  end
58
61
 
59
62
  def self.from_h(hash)
60
63
  terms = hash["terms"]&.map { |h| Designation::Base.from_h(h) } || []
61
- sources = hash["authoritative_source"]&.each { |source| source.merge({ "type" => "authoritative"}) }
64
+ sources = hash["authoritative_source"]&.each { |source| source.merge({ "type" => "authoritative" }) }
62
65
 
63
66
  super(hash.merge({"terms" => terms, "sources" => sources}))
64
67
  end
@@ -8,6 +8,9 @@ module Glossarist
8
8
  # @return [String]
9
9
  attr_accessor :id
10
10
  alias :termid= :id=
11
+ alias :identifier= :id=
12
+
13
+ attr_accessor :uuid
11
14
 
12
15
  # @return [Array<RelatedConcept>]
13
16
  attr_reader :related
@@ -29,24 +32,27 @@ module Glossarist
29
32
  #
30
33
  # Keys are language codes and values are instances of {LocalizedConcept}.
31
34
  # @return [Hash<String, LocalizedConcept>]
32
- attr_accessor :localizations
35
+ attr_reader :localizations
33
36
 
34
37
  def initialize(attributes = {})
35
38
  @localizations = {}
36
- self.localized_concepts = attributes.values.grep(Hash)
39
+ @localized_concepts = {}
40
+ @localized_concept_class = Config.class_for(:localized_concept)
41
+ @uuid_namespace = Glossarist::Utilities::UUID::OID_NAMESPACE
37
42
 
38
43
  attributes = symbolize_keys(attributes)
39
- super(slice_keys(attributes, managed_concept_attributes))
40
- end
44
+ @uuid = attributes[:uuid]
41
45
 
42
- def localized_concepts=(localized_concepts_hash)
43
- @localized_concepts = localized_concepts_hash.map { |l| Config.class_for(:localized_concept).new(l) }.compact
46
+ data = attributes.delete(:data) || {}
47
+ data["groups"] = attributes[:groups]
44
48
 
45
- @localized_concepts.each do |l|
46
- add_l10n(l)
47
- end
49
+ data = symbolize_keys(data.compact)
48
50
 
49
- @localized_concepts
51
+ super(slice_keys(data, managed_concept_attributes))
52
+ end
53
+
54
+ def uuid
55
+ @uuid ||= Glossarist::Utilities::UUID.uuid_v5(@uuid_namespace, to_h.to_yaml)
50
56
  end
51
57
 
52
58
  def related=(related)
@@ -63,10 +69,51 @@ module Glossarist
63
69
  @groups = groups.is_a?(Array) ? groups : [groups]
64
70
  end
65
71
 
72
+ def localized_concepts=(localized_concepts)
73
+ return unless localized_concepts
74
+
75
+ if localized_concepts.is_a?(Hash)
76
+ @localized_concepts = stringify_keys(localized_concepts)
77
+ else
78
+ localized_concepts.each do |localized_concept|
79
+ lang = localized_concept["language_code"].to_s
80
+
81
+ @localized_concepts[lang] = Glossarist::Utilities::UUID.uuid_v5(@uuid_namespace, localized_concept.to_h.to_yaml)
82
+
83
+ add_localization(
84
+ @localized_concept_class.new(localized_concept["data"] || localized_concept),
85
+ )
86
+ end
87
+ end
88
+ end
89
+
90
+ def localizations=(localizations)
91
+ return unless localizations
92
+
93
+ @localizations = {}
94
+
95
+ localizations.each do |localized_concept|
96
+ unless localized_concept.is_a?(@localized_concept_class)
97
+ localized_concept = @localized_concept_class.new(
98
+ localized_concept["data"] || localized_concept,
99
+ )
100
+ end
101
+
102
+ add_l10n(localized_concept)
103
+ end
104
+ end
105
+
106
+ def localizations_hash
107
+ @localizations.map do |key, localized_concept|
108
+ [key, localized_concept.to_h]
109
+ end.to_h
110
+ end
111
+
66
112
  # Adds concept localization.
67
113
  # @param localized_concept [LocalizedConcept]
68
114
  def add_localization(localized_concept)
69
115
  lang = localized_concept.language_code
116
+ @localized_concepts[lang] = @localized_concepts[lang] || localized_concept.uuid
70
117
  localizations.store(lang, localized_concept)
71
118
  end
72
119
 
@@ -83,17 +130,18 @@ module Glossarist
83
130
 
84
131
  def to_h
85
132
  {
86
- "termid" => id,
87
- "term" => default_designation,
88
- "related" => related&.map(&:to_h),
89
- "dates" => dates&.empty? ? nil : dates&.map(&:to_h),
90
- "groups" => groups,
91
- }.merge(localizations.transform_values(&:to_h)).compact
133
+ "data" => {
134
+ "identifier" => id,
135
+ "localized_concepts" => localized_concepts.empty? ? nil : localized_concepts,
136
+ "groups" => groups,
137
+ }.compact,
138
+ }.compact
92
139
  end
93
140
 
94
141
  def default_designation
95
142
  localized = localization("eng") || localizations.values.first
96
- localized&.terms&.first&.designation
143
+ terms = localized&.preferred_terms&.first || localized&.terms&.first
144
+ terms&.designation
97
145
  end
98
146
 
99
147
  def default_definition
@@ -105,14 +153,34 @@ module Glossarist
105
153
  localization("eng") || localizations.values.first
106
154
  end
107
155
 
156
+ def date_accepted=(date)
157
+ date_hash = {
158
+ "type" => "accepted",
159
+ "date" => date,
160
+ }
161
+
162
+ @dates ||= []
163
+ @dates << ConceptDate.new(date_hash)
164
+ end
165
+
166
+ def date_accepted
167
+ @dates.find { |date| date.accepted? }
168
+ end
169
+
108
170
  def managed_concept_attributes
109
171
  %i[
172
+ data
110
173
  id
111
- termid
174
+ identifier
175
+ uuid
112
176
  related
113
177
  status
114
178
  dates
179
+ date_accepted
180
+ dateAccepted
115
181
  localized_concepts
182
+ localizedConcepts
183
+ localizations
116
184
  groups
117
185
  ].compact
118
186
  end
@@ -6,6 +6,7 @@ module Glossarist
6
6
 
7
7
  def initialize
8
8
  @managed_concepts = {}
9
+ @managed_concepts_ids = {}
9
10
  @concept_manager = ConceptManager.new
10
11
  end
11
12
 
@@ -39,7 +40,7 @@ module Glossarist
39
40
  # ManagedConcept ID
40
41
  # @return [ManagedConcept, nil]
41
42
  def fetch(id)
42
- @managed_concepts[id]
43
+ @managed_concepts[id] || @managed_concepts[@managed_concepts_ids[id]]
43
44
  end
44
45
 
45
46
  alias :[] :fetch
@@ -52,16 +53,17 @@ module Glossarist
52
53
  # ManagedConcept ID
53
54
  # @return [ManagedConcept]
54
55
  def fetch_or_initialize(id)
55
- fetch(id) or store(ManagedConcept.new(id: id))
56
+ fetch(id) or store(ManagedConcept.new(data: { id: id }))
56
57
  end
57
58
 
58
- # Adds concept to the collection. If collection contains a concept with
59
+ # Adds concept to the collection. If collection contains a concept with
59
60
  # the same ID already, that concept is replaced.
60
61
  #
61
62
  # @param managed_concept [ManagedConcept]
62
63
  # ManagedConcept about to be added
63
64
  def store(managed_concept)
64
- @managed_concepts[managed_concept.id] = managed_concept
65
+ @managed_concepts[managed_concept.uuid] = managed_concept
66
+ @managed_concepts_ids[managed_concept.id] = managed_concept.uuid if managed_concept.id
65
67
  end
66
68
 
67
69
  alias :<< :store
@@ -18,7 +18,11 @@ module Glossarist
18
18
  def set_attribute(name, value)
19
19
  public_send("#{name}=", value)
20
20
  rescue NoMethodError
21
- if Config.extension_attributes.include?(name)
21
+ # adding support for camel case
22
+ if name.match?(/[A-Z]/)
23
+ name = snake_case(name.to_s).to_sym
24
+ retry
25
+ elsif Config.extension_attributes.include?(name)
22
26
  extension_attributes[name] = value
23
27
  else
24
28
  raise ArgumentError, "#{self.class.name} does not have " +
@@ -29,5 +33,9 @@ module Glossarist
29
33
  def self.from_h(hash)
30
34
  new(hash)
31
35
  end
36
+
37
+ def snake_case(str)
38
+ str.gsub(/([A-Z])/) { "_#{$1.downcase}" }
39
+ end
32
40
  end
33
41
  end
@@ -9,7 +9,26 @@ module Glossarist
9
9
  def symbolize_keys(hash)
10
10
  result = {}
11
11
  hash.each_pair do |key, value|
12
- result[key.to_sym] = value
12
+ result[key.to_sym] = if value.is_a?(Hash)
13
+ symbolize_keys(value)
14
+ else
15
+ value
16
+ end
17
+ end
18
+ result
19
+ end
20
+
21
+ # Hash#transform_keys is not available in Ruby 2.4
22
+ # so we have to do this ourselves :(
23
+ # symbolize hash keys
24
+ def stringify_keys(hash)
25
+ result = {}
26
+ hash.each_pair do |key, value|
27
+ result[key.to_s] = if value.is_a?(Hash)
28
+ stringify_keys(value)
29
+ else
30
+ value
31
+ end
13
32
  end
14
33
  result
15
34
  end
@@ -24,6 +43,22 @@ module Glossarist
24
43
  end
25
44
  result
26
45
  end
46
+
47
+ def convert_keys_to_snake_case(hash)
48
+ result = {}
49
+ hash.each_pair do |key, value|
50
+ result[snake_case(key)] = if value.is_a?(Hash)
51
+ convert_keys_to_snake_case(value)
52
+ else
53
+ value
54
+ end
55
+ end
56
+ result
57
+ end
58
+
59
+ def snake_case(str)
60
+ str.gsub(/([A-Z])/) { "_#{$1.downcase}" }
61
+ end
27
62
  end
28
63
  end
29
64
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # extracted from https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/digest/uuid.rb
4
+ # to generate uuid_v5 for concept files
5
+
6
+ require "securerandom"
7
+ require "openssl"
8
+
9
+ module Glossarist
10
+ module Utilities
11
+ module UUID
12
+ DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" # :nodoc:
13
+ URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" # :nodoc:
14
+ OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" # :nodoc:
15
+ X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" # :nodoc:
16
+
17
+ # Generates a v5 non-random UUID (Universally Unique IDentifier).
18
+ #
19
+ # Using OpenSSL::Digest::MD5 generates version 3 UUIDs; OpenSSL::Digest::SHA1 generates version 5 UUIDs.
20
+ # uuid_from_hash always generates the same UUID for a given name and namespace combination.
21
+ #
22
+ # See RFC 4122 for details of UUID at: https://www.ietf.org/rfc/rfc4122.txt
23
+ def self.uuid_from_hash(hash_class, namespace, name)
24
+ if hash_class == Digest::MD5 || hash_class == OpenSSL::Digest::MD5
25
+ version = 3
26
+ elsif hash_class == Digest::SHA1 || hash_class == OpenSSL::Digest::SHA1
27
+ version = 5
28
+ else
29
+ raise ArgumentError, "Expected OpenSSL::Digest::SHA1 or OpenSSL::Digest::MD5, got #{hash_class.name}."
30
+ end
31
+
32
+ uuid_namespace = pack_uuid_namespace(namespace)
33
+
34
+ hash = hash_class.new
35
+ hash.update(uuid_namespace)
36
+ hash.update(name)
37
+
38
+ ary = hash.digest.unpack("NnnnnN")
39
+ ary[2] = (ary[2] & 0x0FFF) | (version << 12)
40
+ ary[3] = (ary[3] & 0x3FFF) | 0x8000
41
+
42
+ "%08x-%04x-%04x-%04x-%04x%08x" % ary
43
+ end
44
+
45
+ # Convenience method for uuid_from_hash using OpenSSL::Digest::MD5.
46
+ def self.uuid_v3(uuid_namespace, name)
47
+ uuid_from_hash(OpenSSL::Digest::MD5, uuid_namespace, name)
48
+ end
49
+
50
+ # Convenience method for uuid_from_hash using OpenSSL::Digest::SHA1.
51
+ def self.uuid_v5(uuid_namespace, name)
52
+ uuid_from_hash(OpenSSL::Digest::SHA1, uuid_namespace, name)
53
+ end
54
+
55
+ # Convenience method for SecureRandom.uuid.
56
+ def self.uuid_v4
57
+ SecureRandom.uuid
58
+ end
59
+
60
+ def self.pack_uuid_namespace(namespace)
61
+ if [DNS_NAMESPACE, OID_NAMESPACE, URL_NAMESPACE, X500_NAMESPACE].include?(namespace)
62
+ namespace
63
+ else
64
+ match_data = namespace.match(/\A(\h{8})-(\h{4})-(\h{4})-(\h{4})-(\h{4})(\h{8})\z/)
65
+
66
+ raise ArgumentError, "Only UUIDs are valid namespace identifiers" unless match_data.present?
67
+
68
+ match_data.captures.map { |s| s.to_i(16) }.pack("NnnnnN")
69
+ end
70
+ end
71
+
72
+ private_class_method :pack_uuid_namespace
73
+ end
74
+ end
75
+ end
@@ -3,3 +3,4 @@
3
3
  require_relative "utilities/enum"
4
4
  require_relative "utilities/boolean_attributes"
5
5
  require_relative "utilities/common_functions"
6
+ require_relative "utilities/uuid"
@@ -4,5 +4,5 @@
4
4
  #
5
5
 
6
6
  module Glossarist
7
- VERSION = "1.1.0"
7
+ VERSION = "2.0.0"
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glossarist
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-11 00:00:00.000000000 Z
11
+ date: 2023-11-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: relaton
@@ -109,6 +109,8 @@ files:
109
109
  - lib/glossarist/collections.rb
110
110
  - lib/glossarist/collections/asset_collection.rb
111
111
  - lib/glossarist/collections/bibliography_collection.rb
112
+ - lib/glossarist/collections/collection.rb
113
+ - lib/glossarist/collections/designation_collection.rb
112
114
  - lib/glossarist/concept.rb
113
115
  - lib/glossarist/concept_date.rb
114
116
  - lib/glossarist/concept_manager.rb
@@ -142,6 +144,7 @@ files:
142
144
  - lib/glossarist/utilities/enum/class_methods.rb
143
145
  - lib/glossarist/utilities/enum/enum_collection.rb
144
146
  - lib/glossarist/utilities/enum/instance_methods.rb
147
+ - lib/glossarist/utilities/uuid.rb
145
148
  - lib/glossarist/version.rb
146
149
  homepage: https://github.com/glossarist/glossarist-ruby
147
150
  licenses: