Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/termium.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# frozen_string_literal: true

# glossarist's ManagedConceptCollection#save_to_files calls FileUtils without
# requiring it, so loading it here keeps `termium convert` from raising
# NameError.
require "fileutils"
require "glossarist"

module Termium
Expand Down
41 changes: 27 additions & 14 deletions lib/termium/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ def uuid(str = identification_number)
# details="Compartment - ISO/IEC JTC 1 Information Technology Vocabulary" />
def to_concept(options = {})
Glossarist::ManagedConcept.new.tap do |concept|
# The way to set the universal concept's identifier: data.identifier
concept.id = identification_number
# `data` must be assigned, not mutated: lutaml-model skips serializing
# attributes still flagged as using their default, so mutating the
# default `ManagedConceptData` in place drops the whole `data` block.
concept.data = Glossarist::ManagedConceptData.new(
id: identification_number,
)

concept.uuid = uuid

Expand All @@ -55,24 +59,33 @@ def to_concept(options = {})
concept.status = "valid"

if options[:date_accepted]
concept.date_accepted = options[:date_accepted]
concept.date_accepted = Glossarist::ConceptDate.new(
date: options[:date_accepted],
type: "accepted",
)
end

language_module.map do |lang_mod|
localized_concept = lang_mod.to_concept(options)
add_localizations(concept, options)
end
end

private

def add_localizations(concept, options)
language_module.each do |lang_mod|
localized_concept = lang_mod.to_concept(options)

# TODO: This is needed to skip the empty french entries of 10031781 and 10031778
next if localized_concept.nil?
# TODO: This is needed to skip the empty french entries of 10031781 and 10031778
next if localized_concept.nil?

localized_concept.id = identification_number
localized_concept.uuid = uuid("#{identification_number}-#{lang_mod.language}")
localized_concept.id = identification_number
localized_concept.uuid = uuid("#{identification_number}-#{lang_mod.language}")

universal_entry.each do |entry|
localized_concept.notes << Glossarist::DetailedDefinition.new(content: entry.value)
end
localized_concept.sources = concept_sources
concept.add_localization(localized_concept)
universal_entry.each do |entry|
localized_concept.notes << Glossarist::DetailedDefinition.new(content: entry.value)
end
localized_concept.sources = concept_sources
concept.add_localization(localized_concept)
end
end
end
Expand Down
32 changes: 23 additions & 9 deletions lib/termium/language_module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ def designations

def to_h
# TODO: This is needed to skip the empty french entries of 10031781 and 10031778
return nil unless definition
value = definition
return nil unless value

src = {
"language_code" => LANGUAGE_CODE_MAPPING[language.downcase],
"terms" => designations.map(&:to_h),
"definition" => [{ content: definition }],
"notes" => notes,
"examples" => examples,
"definition" => detailed_definitions([value]),
"notes" => detailed_definitions(notes),
"examples" => detailed_definitions(examples),
"entry_status" => "valid",
}

Expand All @@ -70,15 +71,28 @@ def to_concept(options = {})
x = to_h
return nil unless x

Glossarist::LocalizedConcept.new(x).tap do |concept|
# The flat hash belongs under "data": LocalizedConcept is data-backed, and
# `.new` would silently discard every key that is not one of its own
# attributes. `of_yaml` also routes "terms" through ConceptData.
Glossarist::LocalizedConcept.of_yaml({ "data" => x }).tap do |concept|
# Fill in register parameters
if options[:date_accepted]
# puts options[:date_accepted].inspect
concept.date_accepted = options[:date_accepted]
# `date_accepted` is a read-only derived accessor on Concept; the
# accepted date is set by way of `data.dates`.
concept.data.dates = [
Glossarist::ConceptDate.new(
date: options[:date_accepted],
type: "accepted",
),
]
end

# puts concept.inspect
end
end

private

def detailed_definitions(values)
values.map { |value| { "content" => value } }
end
end
end
138 changes: 126 additions & 12 deletions spec/termium_spec.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,131 @@
# frozen_string_literal: true

require "open3"
require "rbconfig"
require "tmpdir"
require "yaml"

Comment thread
HassanAkbar marked this conversation as resolved.
RSpec.describe Termium do
# let(:concept_folder) { "concept_collection_v2" }
# let(:concept_files) { Dir.glob(File.join(fixtures_path(concept_folder), "concept", "*.{yaml,yml}")) }
# let(:localized_concepts_folder) { File.join(fixtures_path(concept_folder), "localized_concept") }

let(:termium_extract_file) { fixtures_path("Characters.xml") }
let(:glossarist_output_file) { fixtures_path("Characters-Glossarist") }

it "does something useful" do
termium_extract = Termium::Extract.from_xml(File.read(termium_extract_file))
glossarist_col = termium_extract.to_concept
FileUtils.mkdir_p(glossarist_output_file)
glossarist_col.save_to_files(glossarist_output_file)
let(:extract) do
Termium::Extract.from_xml(File.read(fixtures_path("Characters.xml")))
end

# The TERMIUM entry used throughout: identificationNumber 2123225, which has
# both an EN and a FR languageModule.
let(:identification_number) { "2123225" }
let(:core) do
extract.core.find { |c| c.identification_number == identification_number }
end

def save_to_tmp(options = {})
Dir.mktmpdir do |dir|
extract.to_concept(options).save_to_files(dir)
yield(
Dir.glob("#{dir}/concept/*.yaml").map { |f| YAML.load_file(f) },
Dir.glob("#{dir}/localized_concept/*.yaml").map { |f| YAML.load_file(f) }
)
Comment thread
HassanAkbar marked this conversation as resolved.
end
end

describe "#to_concept" do
it "carries the TERMIUM identification number as the concept identifier" do
doc = core.to_concept.to_yaml_hash

expect(doc.dig("data", "identifier")).to eq(identification_number)
end

it "registers one localization per language, keyed by language code" do
expect(core.to_concept.localizations.keys).to contain_exactly("eng", "fre")
end

it "cross-references every localization from the concept" do
doc = core.to_concept.to_yaml_hash

expect(doc.dig("data", "localized_concepts").keys)
.to contain_exactly("eng", "fre")
end

it "gives each localization a distinct uuid" do
map = core.to_concept.to_yaml_hash.dig("data", "localized_concepts")

expect(map["eng"]).not_to eq(map["fre"])
end

it "populates the localized concept from the TERMIUM entry" do
data = core.to_concept.localization("eng").to_yaml_hash["data"]

aggregate_failures do
expect(data["language_code"]).to eq("eng")
expect(data["terms"].map { |t| t["designation"] })
.to include("average information rate")
expect(data.dig("definition", 0, "content"))
.to start_with("quotient of the character mean entropy")
end
end

it "wraps notes as detailed definitions" do
data = core.to_concept.localization("eng").to_yaml_hash["data"]

expect(data["notes"].map { |n| n["content"] })
.to include(a_string_including("average information rate may be expressed"))
end
end

describe "#to_concept with date_accepted" do
let(:date_accepted) { "2024-01-15" }

it "records the accepted date on the concept" do
save_to_tmp(date_accepted: date_accepted) do |concepts, _localized|
expect(concepts)
.to all(include("date_accepted" => a_string_starting_with(date_accepted)))
end
end

it "records the accepted date on every localized concept" do
save_to_tmp(date_accepted: date_accepted) do |_concepts, localized|
expect(localized)
.to all(include("date_accepted" => a_string_starting_with(date_accepted)))
end
end
end

# glossarist's save_to_files calls FileUtils without requiring it, so requiring
# "termium" must be sufficient on its own. This has to run in a clean subprocess:
# in-process the spec's own `require "tmpdir"` loads fileutils and masks the bug,
# which is why it reaches production while the suite stays green.
describe "loading termium standalone" do
# Passes argv directly rather than through a shell: POSIX quoting would
# reach cmd.exe verbatim on Windows and mangle the script.
def save_in_clean_process(dir)
root = File.expand_path("..", __dir__)
script = <<~RUBY
require "termium"
xml = File.read(#{File.join(root, 'spec/fixtures/Characters.xml').inspect})
Termium::Extract.from_xml(xml).to_concept.save_to_files(#{dir.inspect})
RUBY
Open3.capture2e(RbConfig.ruby, "-I#{File.join(root, 'lib')}", "-e", script,
chdir: root)
end

it "can save a dataset without the caller requiring fileutils" do
output, status = Dir.mktmpdir { |dir| save_in_clean_process(dir) }

aggregate_failures do
expect(output).not_to include("NameError")
expect(status).to be_success
end
end
end

describe "#save_to_files" do
it "writes one concept per entry and one localized concept per language" do
save_to_tmp do |concepts, localized|
aggregate_failures do
expect(concepts.size).to eq(extract.core.size)
expect(localized.size)
.to eq(extract.core.sum { |c| c.language_module.size })
end
end
end
end
end
2 changes: 1 addition & 1 deletion termium.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_dependency "glossarist", "~> 2.3.5"
spec.add_dependency "glossarist", "~> 2.11.3"
spec.add_dependency "lutaml-model", "~> 0.8.0"
spec.add_dependency "thor"
spec.add_dependency "uuidtools"
Expand Down
Loading