diff --git a/lib/termium.rb b/lib/termium.rb index a6d19a2..a7fa8f0 100644 --- a/lib/termium.rb +++ b/lib/termium.rb @@ -23,6 +23,7 @@ class Error < StandardError; end require_relative "termium/language_module" require_relative "termium/parameter" require_relative "termium/source" +require_relative "termium/source_details" require_relative "termium/source_ref" require_relative "termium/subject" require_relative "termium/textual_support" diff --git a/lib/termium/source.rb b/lib/termium/source.rb index 1dfe9a0..ba3f176 100644 --- a/lib/termium/source.rb +++ b/lib/termium/source.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "source_details" + module Termium # For class Source < Lutaml::Model::Serializable @@ -7,7 +9,8 @@ class Source < Lutaml::Model::Serializable ISOIEC_BIB_REGEX = /\AISO-IEC-([\d-]+)\s+\*\s+(\d{4})\s+.*/.freeze attribute :order, :integer - attribute :details, :string + attribute :details, SourceDetails + xml do root "source" map_attribute "order", to: :order @@ -15,12 +18,13 @@ class Source < Lutaml::Model::Serializable end def content - if (matches = details.match(ISOIEC_BIB_REGEX)) + presentable_details = details.to_s + if (matches = presentable_details.match(ISOIEC_BIB_REGEX)) "ISO/IEC #{matches[1]}:#{matches[2]}" - elsif (matches = details.match(ISO_BIB_REGEX)) + elsif (matches = presentable_details.match(ISO_BIB_REGEX)) "ISO #{matches[1]}:#{matches[2]}" else - details + presentable_details end end diff --git a/lib/termium/source_details.rb b/lib/termium/source_details.rb new file mode 100644 index 0000000..315d159 --- /dev/null +++ b/lib/termium/source_details.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Termium + # Parses the asterisk-delimited source details string + class SourceDetails + attr_accessor :raw, :standard, :author_name, :year, :organization, :department, :office, :division, :role + + class << self + def cast(value) + return value if value.is_a?(SourceDetails) + return nil if value.nil? || value.to_s.strip.empty? + + new( + value + ) + end + + def serialize(value) + value&.raw + end + + alias from_xml cast + end + + def initialize(value) + return if value.nil? || value.to_s.strip.empty? + + columns = value.to_s.split("*").map(&:strip) + + @raw = value.to_s + @year = presence(columns[1]) + @organization = presence(columns[2]) + @department = presence(columns[3]) + @office = presence(columns[4]) + @division = presence(columns[5]) + @role = presence(columns[6]) + + first_column = presence(columns[0]) + if standard_identifier?(first_column) + @standard = first_column + else + @author_name = first_column + end + end + + def presence(value) + value unless value.nil? || value.empty? + end + + # Matches ISO or ISO-IEC standard identifiers with a document number + # e.g., "ISO-9000", "ISO-IEC-2382-16", "ISO-IEC-27001" + def standard_identifier?(value) + return false if value.nil? + + value.match?(/\AISO(-IEC)?-\d+/i) + end + + alias to_xml raw + alias to_s raw + end +end diff --git a/spec/termium/source_details_spec.rb b/spec/termium/source_details_spec.rb new file mode 100644 index 0000000..99b607d --- /dev/null +++ b/spec/termium/source_details_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +RSpec.describe Termium::SourceDetails do + describe ".cast" do + context "with ISO-IEC standard identifier" do + let(:input) { "ISO-IEC-2382-16 * 1996 * * * " } + subject(:details) { described_class.cast(input) } + + it "sets standard from first column" do + expect(details.standard).to eq("ISO-IEC-2382-16") + end + + it "sets year" do + expect(details.year).to eq("1996") + end + + it "does not set author_name" do + expect(details.author_name).to be_nil + end + + it "does not set organization" do + expect(details.organization).to be_nil + end + end + + context "with organization name (not a standard identifier)" do + let(:input) { "ISO/IEC JTC 1 * 2011" } + subject(:details) { described_class.cast(input) } + + it "sets author_name from first column" do + expect(details.author_name).to eq("ISO/IEC JTC 1") + end + + it "sets year" do + expect(details.year).to eq("2011") + end + + it "does not set standard" do + expect(details.standard).to be_nil + end + end + + context "with personal author (has organization)" do + let(:input) { "Ranger, Natalie * 2006 * Bureau de la traduction * Services linguistiques" } + subject(:details) { described_class.cast(input) } + + it "sets author_name from first column" do + expect(details.author_name).to eq("Ranger, Natalie") + end + + it "sets year" do + expect(details.year).to eq("2006") + end + + it "sets organization" do + expect(details.organization).to eq("Bureau de la traduction") + end + + it "sets department" do + expect(details.department).to eq("Services linguistiques") + end + + it "does not set standard" do + expect(details.standard).to be_nil + end + end + + context "when value is already SourceDetails" do + let(:original) { described_class.cast("ISO-IEC-2382-16 * 1996 * * * ") } + + it "returns the same object" do + expect(described_class.cast(original)).to be(original) + end + end + + context "with nil input" do + it "returns nil" do + expect(described_class.cast(nil)).to be_nil + end + end + + context "with empty string" do + it "returns nil" do + expect(described_class.cast("")).to be_nil + end + end + end + + describe ".serialize" do + let(:details) { described_class.cast("ISO-IEC-2382-16 * 1996 * * * ") } + + it "returns the raw string" do + expect(described_class.serialize(details)).to eq("ISO-IEC-2382-16 * 1996 * * * ") + end + + it "returns nil for nil input" do + expect(described_class.serialize(nil)).to be_nil + end + end + + describe "#to_s" do + let(:details) { described_class.cast("Ranger, Natalie * 2006 * Bureau de la traduction") } + + it "returns the raw string" do + expect(details.to_s).to eq("Ranger, Natalie * 2006 * Bureau de la traduction") + end + end +end diff --git a/spec/termium/source_spec.rb b/spec/termium/source_spec.rb new file mode 100644 index 0000000..e3da787 --- /dev/null +++ b/spec/termium/source_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +RSpec.describe Termium::Source do + describe "XML round-trip" do + let(:xml_input) { '' } + + it "preserves details attribute" do + source = described_class.from_xml(xml_input) + output = source.to_xml + + expect(output).to include('details="ISO-IEC-2382-16 * 1996 * * * "') + end + + it "parses details into SourceDetails object" do + source = described_class.from_xml(xml_input) + + expect(source.details).to be_a(Termium::SourceDetails) + expect(source.details.standard).to eq("ISO-IEC-2382-16") + end + end + + describe "#content" do + context "with ISO-IEC standard" do + let(:source) { described_class.from_xml('') } + + it "formats as ISO/IEC reference" do + expect(source.content).to eq("ISO/IEC 2382-16:1996") + end + end + + context "with ISO standard" do + let(:source) { described_class.from_xml('') } + + it "formats as ISO reference" do + expect(source.content).to eq("ISO 2382-12:1988") + end + end + + context "with non-standard source" do + let(:source) { described_class.from_xml('') } + + it "returns raw details" do + expect(source.content).to eq("Ranger, Natalie * 2006 * Bureau de la traduction") + end + end + end +end