Skip to content
Open
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
1 change: 1 addition & 0 deletions lib/termium.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 8 additions & 4 deletions lib/termium/source.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
# frozen_string_literal: true

require_relative "source_details"

module Termium
# For <source>
class Source < Lutaml::Model::Serializable
ISO_BIB_REGEX = /\AISO-([\d-]+)\s+\*\s+(\d{4})\s+.*/.freeze
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
map_attribute "details", to: :details
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

Expand Down
61 changes: 61 additions & 0 deletions lib/termium/source_details.rb
Original file line number Diff line number Diff line change
@@ -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
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SourceDetails.serialize currently returns value&.raw, which will return nil if value is a plain String. That can drop data if callers set details to a raw string (or if the serializer passes a string through). Consider handling String inputs explicitly (e.g., returning the string as-is) in addition to SourceDetails instances.

Suggested change
value&.raw
return nil if value.nil?
return value.raw if value.is_a?(SourceDetails)
return value if value.is_a?(String)
value

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value parameter is expected to be a SourceDetails instance or nil, never a plain String. Strings are converted to SourceDetails via cast before serialize is called.

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
108 changes: 108 additions & 0 deletions spec/termium/source_details_spec.rb
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions spec/termium/source_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

RSpec.describe Termium::Source do
describe "XML round-trip" do
let(:xml_input) { '<source order="1" details="ISO-IEC-2382-16 * 1996 * * * "/>' }

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('<source order="1" details="ISO-IEC-2382-16 * 1996 * * * "/>') }

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('<source order="1" details="ISO-2382-12 * 1988 * * * "/>') }

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('<source order="1" details="Ranger, Natalie * 2006 * Bureau de la traduction"/>') }

it "returns raw details" do
expect(source.content).to eq("Ranger, Natalie * 2006 * Bureau de la traduction")
end
end
end
end
Loading