Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
137 changes: 125 additions & 12 deletions spec/termium_spec.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,130 @@
# frozen_string_literal: true

require "open3"
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