From fde673d8425411760db56c6837aa7dbfdf0b14a5 Mon Sep 17 00:00:00 2001 From: HassanAkbar Date: Tue, 21 Nov 2023 18:14:37 +0500 Subject: [PATCH] Added changes to work with termium --- lib/glossarist/concept.rb | 9 +++ lib/glossarist/concept_manager.rb | 11 +-- lib/glossarist/managed_concept.rb | 11 ++- lib/glossarist/managed_concept_collection.rb | 8 ++- lib/glossarist/utilities.rb | 1 + lib/glossarist/utilities/uuid.rb | 75 ++++++++++++++++++++ spec/unit/managed_concept_collection_spec.rb | 13 +++- 7 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 lib/glossarist/utilities/uuid.rb diff --git a/lib/glossarist/concept.rb b/lib/glossarist/concept.rb index d9ac528..526a95a 100644 --- a/lib/glossarist/concept.rb +++ b/lib/glossarist/concept.rb @@ -9,6 +9,8 @@ class Concept < Model # @return [String] attr_reader :id + attr_writer :uuid + # Concept designations. # @todo Alias +terms+ exists only for legacy reasons and will be removed. # @return [Array] @@ -64,6 +66,13 @@ def initialize(*args) super end + def uuid + @uuid ||= Glossarist::Utilities::UUID.uuid_v5( + Glossarist::Utilities::UUID::OID_NAMESPACE, + to_h.to_s, + ) + end + def id=(id) raise(Glossarist::Error, "Expect id to be a string, Got #{id.class} (#{id})") unless id.is_a?(String) || id.nil? diff --git a/lib/glossarist/concept_manager.rb b/lib/glossarist/concept_manager.rb index d3a035c..e490250 100644 --- a/lib/glossarist/concept_manager.rb +++ b/lib/glossarist/concept_manager.rb @@ -41,12 +41,13 @@ def load_concept_from_file(filename) end def load_localized_concept(id) - Config.class_for(:localized_concept).new( - Psych.safe_load( - File.read(localized_concept_path(id)), - permitted_classes: [Date], - ), + concept_hash = Psych.safe_load( + File.read(localized_concept_path(id)), + permitted_classes: [Date], ) + concept_hash["uuid"] = id + + Config.class_for(:localized_concept).new(concept_hash) rescue Psych::SyntaxError => e raise Glossarist::ParseError.new(filename: filename, line: e.line) end diff --git a/lib/glossarist/managed_concept.rb b/lib/glossarist/managed_concept.rb index 7a5c89e..f1de653 100644 --- a/lib/glossarist/managed_concept.rb +++ b/lib/glossarist/managed_concept.rb @@ -36,7 +36,9 @@ class ManagedConcept < Model def initialize(attributes = {}) @localizations = {} + @localized_concepts = {} @localized_concept_class = Config.class_for(:localized_concept) + @uuid_namespace = Glossarist::Utilities::UUID::OID_NAMESPACE attributes = symbolize_keys(attributes) @uuid = attributes[:uuid] @@ -49,6 +51,10 @@ def initialize(attributes = {}) super(slice_keys(data, managed_concept_attributes)) end + def uuid + @uuid ||= Glossarist::Utilities::UUID.uuid_v5(@uuid_namespace, to_h.to_s) + end + def related=(related) @related = related&.map { |r| RelatedConcept.new(r) } end @@ -72,7 +78,7 @@ def localized_concepts=(localized_concepts) localized_concepts.each do |localized_concept| lang = localized_concept["language_code"].to_s - @localized_concepts[lang] = SecureRandom.uuid + @localized_concepts[lang] = Glossarist::Utilities::UUID.uuid_v5(@uuid_namespace, localized_concept.to_h.to_s) add_localization( @localized_concept_class.new(localized_concept["data"] || localized_concept), @@ -107,6 +113,7 @@ def localizations_hash # @param localized_concept [LocalizedConcept] def add_localization(localized_concept) lang = localized_concept.language_code + @localized_concepts[lang] = @localized_concepts[lang] || localized_concept.uuid localizations.store(lang, localized_concept) end @@ -125,7 +132,7 @@ def to_h { "data" => { "identifier" => id, - "localized_concepts" => localized_concepts, + "localized_concepts" => localized_concepts.empty? ? nil : localized_concepts, "groups" => groups, }.compact, }.compact diff --git a/lib/glossarist/managed_concept_collection.rb b/lib/glossarist/managed_concept_collection.rb index 7461672..12e9621 100644 --- a/lib/glossarist/managed_concept_collection.rb +++ b/lib/glossarist/managed_concept_collection.rb @@ -6,6 +6,7 @@ class ManagedConceptCollection def initialize @managed_concepts = {} + @managed_concepts_ids = {} @concept_manager = ConceptManager.new end @@ -39,7 +40,7 @@ def each(&block) # ManagedConcept ID # @return [ManagedConcept, nil] def fetch(id) - @managed_concepts[id] + @managed_concepts[id] || @managed_concepts[@managed_concepts_ids[id]] end alias :[] :fetch @@ -55,13 +56,14 @@ def fetch_or_initialize(id) fetch(id) or store(ManagedConcept.new(data: { id: id })) end - # Adds concept to the collection. If collection contains a concept with + # Adds concept to the collection. If collection contains a concept with # the same ID already, that concept is replaced. # # @param managed_concept [ManagedConcept] # ManagedConcept about to be added def store(managed_concept) - @managed_concepts[managed_concept.id] = managed_concept + @managed_concepts[managed_concept.uuid] = managed_concept + @managed_concepts_ids[managed_concept.id] = managed_concept.uuid if managed_concept.id end alias :<< :store diff --git a/lib/glossarist/utilities.rb b/lib/glossarist/utilities.rb index abd6634..bc02da7 100644 --- a/lib/glossarist/utilities.rb +++ b/lib/glossarist/utilities.rb @@ -3,3 +3,4 @@ require_relative "utilities/enum" require_relative "utilities/boolean_attributes" require_relative "utilities/common_functions" +require_relative "utilities/uuid" diff --git a/lib/glossarist/utilities/uuid.rb b/lib/glossarist/utilities/uuid.rb new file mode 100644 index 0000000..c64fe29 --- /dev/null +++ b/lib/glossarist/utilities/uuid.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# extracted from https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/digest/uuid.rb +# to generate uuid_v5 for concept files + +require "securerandom" +require "openssl" + +module Glossarist + module Utilities + module UUID + DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" # :nodoc: + URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" # :nodoc: + OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" # :nodoc: + X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" # :nodoc: + + # Generates a v5 non-random UUID (Universally Unique IDentifier). + # + # Using OpenSSL::Digest::MD5 generates version 3 UUIDs; OpenSSL::Digest::SHA1 generates version 5 UUIDs. + # uuid_from_hash always generates the same UUID for a given name and namespace combination. + # + # See RFC 4122 for details of UUID at: https://www.ietf.org/rfc/rfc4122.txt + def self.uuid_from_hash(hash_class, namespace, name) + if hash_class == Digest::MD5 || hash_class == OpenSSL::Digest::MD5 + version = 3 + elsif hash_class == Digest::SHA1 || hash_class == OpenSSL::Digest::SHA1 + version = 5 + else + raise ArgumentError, "Expected OpenSSL::Digest::SHA1 or OpenSSL::Digest::MD5, got #{hash_class.name}." + end + + uuid_namespace = pack_uuid_namespace(namespace) + + hash = hash_class.new + hash.update(uuid_namespace) + hash.update(name) + + ary = hash.digest.unpack("NnnnnN") + ary[2] = (ary[2] & 0x0FFF) | (version << 12) + ary[3] = (ary[3] & 0x3FFF) | 0x8000 + + "%08x-%04x-%04x-%04x-%04x%08x" % ary + end + + # Convenience method for uuid_from_hash using OpenSSL::Digest::MD5. + def self.uuid_v3(uuid_namespace, name) + uuid_from_hash(OpenSSL::Digest::MD5, uuid_namespace, name) + end + + # Convenience method for uuid_from_hash using OpenSSL::Digest::SHA1. + def self.uuid_v5(uuid_namespace, name) + uuid_from_hash(OpenSSL::Digest::SHA1, uuid_namespace, name) + end + + # Convenience method for SecureRandom.uuid. + def self.uuid_v4 + SecureRandom.uuid + end + + def self.pack_uuid_namespace(namespace) + if [DNS_NAMESPACE, OID_NAMESPACE, URL_NAMESPACE, X500_NAMESPACE].include?(namespace) + namespace + else + match_data = namespace.match(/\A(\h{8})-(\h{4})-(\h{4})-(\h{4})-(\h{4})(\h{8})\z/) + + raise ArgumentError, "Only UUIDs are valid namespace identifiers" unless match_data.present? + + match_data.captures.map { |s| s.to_i(16) }.pack("NnnnnN") + end + end + + private_class_method :pack_uuid_namespace + end + end +end diff --git a/spec/unit/managed_concept_collection_spec.rb b/spec/unit/managed_concept_collection_spec.rb index 1efb6b9..14bcc3a 100644 --- a/spec/unit/managed_concept_collection_spec.rb +++ b/spec/unit/managed_concept_collection_spec.rb @@ -41,12 +41,19 @@ end describe "#fetch" do - it "returns a managed concept" do - managed_concept = Glossarist::ManagedConcept.new("data" => { id: "id" }) - managed_concept_collection.store(managed_concept) + let(:managed_concept) { Glossarist::ManagedConcept.new("data" => { id: "id" }) } + it "fetches a managed concept by id" do + managed_concept_collection.store(managed_concept) expect(managed_concept_collection.fetch("id")).to eq(managed_concept) end + + it "fetches a managed concept by uuid" do + managed_concept_collection.store(managed_concept) + uuid = Glossarist::Utilities::UUID.uuid_v5(Glossarist::Utilities::UUID::OID_NAMESPACE, managed_concept.to_h.to_s) + + expect(managed_concept_collection.fetch(uuid)).to eq(managed_concept) + end end describe "#[]" do