glossarist 1.1.0 → 2.0.1

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: 351ee7a983e5566dab9124def66fef23e1a8517b463c695e72388d40939c6031
4
- data.tar.gz: d7f4b103f7a60ed7bb80983d30d8f9bba281a1f6c8ec293dcd73eecdbec8e2bf
3
+ metadata.gz: c0462402c995f5b7e9fb01a4ee0d8e1f9ee3c5633c18fa071d9db6aa9f21f99f
4
+ data.tar.gz: 444e2d92467c4101edc81bc1af65fb6aeecaa0e138f63a49076f2283802baf17
5
5
  SHA512:
6
- metadata.gz: 4614c9b4457b76d5f7936f9b43a6f97c8ceeb62442aafb4d865e3bdc8aa786493af6062a9cef48e3328085cf38a876822eb0dd48b8ac1d490ad166eeaaf8713e
7
- data.tar.gz: dcdb5aca4bda8c3ebae41a7ef1c3cfcbf3c154fe2e0aa38ea8c1b76e175c08b7c3d070daddbb9e54e3079f324f7b229b5e6f60d77908e1689d05e35ac84347e5
6
+ metadata.gz: ab7f0f87234df6fcd73286969c53e1186fcd247b4d8972a8d305b74cc995a4200785db43215a0d5aae719bc164123035f87a1756c104aa3d36b38775585f2019
7
+ data.tar.gz: d5f4a8834eb1d8bbb19d8352c13159e4626c8ff65233b5269658b77c4ee753bbcd8dcaf4502981b1ae2556a13e8af8deb45661e86f485cf7df25cf0f9d1cbb57
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
data/glossarist.gemspec CHANGED
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
31
  spec.require_paths = ["lib"]
32
32
 
33
- spec.add_dependency "relaton", "~> 1.16.0"
33
+ spec.add_dependency "relaton", "~> 1.16"
34
34
  spec.add_dependency "thor"
35
35
 
36
36
  spec.add_development_dependency "pry", "~> 0.14.0"
@@ -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 = if v1_collection?
21
+ Glossarist::V1Reader.load_concept_from_file(filename)
22
+ else
23
+ load_concept_from_file(filename)
24
+ end
25
+
26
+ collection.store(concept)
21
27
  end
22
28
  end
23
29
 
@@ -27,20 +33,64 @@ 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
+
39
+ concept = ManagedConcept.new(concept_hash)
40
+ concept.localized_concepts.each do |_lang, id|
41
+ localized_concept = load_localized_concept(id)
42
+ concept.add_l10n(localized_concept)
43
+ end
44
+
45
+ concept
46
+ rescue Psych::SyntaxError => e
47
+ raise Glossarist::ParseError.new(filename: filename, line: e.line)
48
+ end
49
+
50
+ def load_localized_concept(id)
51
+ concept_hash = Psych.safe_load(
52
+ File.read(localized_concept_path(id)),
53
+ permitted_classes: [Date],
54
+ )
55
+ concept_hash["uuid"] = id
56
+
57
+ Config.class_for(:localized_concept).new(concept_hash)
31
58
  rescue Psych::SyntaxError => e
32
59
  raise Glossarist::ParseError.new(filename: filename, line: e.line)
33
60
  end
34
61
 
35
62
  def save_concept_to_file(concept)
36
- filename = File.join(path, "concept-#{concept.id}.yaml")
63
+ concept_dir = File.join(path, "concept")
64
+ localized_concept_dir = File.join(path, "localized_concept")
65
+
66
+ Dir.mkdir(concept_dir) unless Dir.exist?(concept_dir)
67
+ Dir.mkdir(localized_concept_dir) unless Dir.exist?(localized_concept_dir)
68
+
69
+ filename = File.join(concept_dir, "#{concept.uuid}.yaml")
37
70
  File.write(filename, Psych.dump(concept.to_h))
71
+
72
+ concept.localized_concepts.each do |lang, uuid|
73
+ filename = File.join(localized_concept_dir, "#{uuid}.yaml")
74
+ File.write(filename, Psych.dump(concept.localization(lang).to_h))
75
+ end
38
76
  end
39
77
 
40
78
  private
41
79
 
42
80
  def concepts_glob
43
- File.join(path, "concept-*.{yaml,yml}")
81
+ if v1_collection?
82
+ File.join(path, "concept-*.{yaml,yml}")
83
+ else
84
+ File.join(path, "concept", "*.{yaml,yml}")
85
+ end
86
+ end
87
+
88
+ def localized_concept_path(id)
89
+ Dir.glob(File.join(path, "localized_concept", "#{id}.{yaml,yml}"))&.first
90
+ end
91
+
92
+ def v1_collection?
93
+ @v1_collection ||= !Dir.glob(File.join(path, "concept-*.{yaml,yml}")).empty?
44
94
  end
45
95
  end
46
96
  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"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ # An adapter to read concepts in V1 format, converts them to v2 format and
5
+ # load into glossarist concept model.
6
+ class V1Reader
7
+ def self.load_concept_from_file(filename)
8
+ new.load_concept_from_file(filename)
9
+ end
10
+
11
+ def load_concept_from_file(filename)
12
+ concept_hash = Psych.safe_load(File.read(filename), permitted_classes: [Date])
13
+ ManagedConcept.new(generate_v2_concept_hash(concept_hash))
14
+ end
15
+
16
+ private
17
+
18
+ def generate_v2_concept_hash(concept_hash)
19
+ v2_concept = { "groups" => concept_hash["groups"] }
20
+ v2_concept["data"] = {
21
+ "identifier" => concept_hash["termid"],
22
+ "localized_concepts" => concept_hash.values.grep(Hash),
23
+ }
24
+
25
+ v2_concept
26
+ end
27
+ end
28
+ end
@@ -4,5 +4,5 @@
4
4
  #
5
5
 
6
6
  module Glossarist
7
- VERSION = "1.1.0"
7
+ VERSION = "2.0.1"
8
8
  end
data/lib/glossarist.rb CHANGED
@@ -26,6 +26,7 @@ require_relative "glossarist/managed_concept_collection"
26
26
  require_relative "glossarist/concept_manager"
27
27
  require_relative "glossarist/managed_concept"
28
28
  require_relative "glossarist/non_verb_rep"
29
+ require_relative "glossarist/v1_reader"
29
30
 
30
31
  require_relative "glossarist/collections"
31
32
 
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.1
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-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: relaton
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.16.0
19
+ version: '1.16'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.16.0
26
+ version: '1.16'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: thor
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -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,8 @@ 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
148
+ - lib/glossarist/v1_reader.rb
145
149
  - lib/glossarist/version.rb
146
150
  homepage: https://github.com/glossarist/glossarist-ruby
147
151
  licenses: