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
32 changes: 29 additions & 3 deletions lib/relaton/bib/model/ics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,37 @@ class ICS < Lutaml::Model::Serializable
map_element "text", to: :text
end

# Returns the explicit text if set, else the Isoics description for
# `code`. Kept for consumers that read `.text` directly.
def text
val = @text
return val if val && !val.empty?
return @text if @text.is_a?(String) && !@text.empty?

Isoics.fetch(code)&.description if code
Isoics.fetch(code)&.description if code.is_a?(String) && !code.empty?
end

# When code is assigned, eagerly populate text from Isoics if no
# explicit text has been set. Going through the public writer
# registers the value with the lutaml-model `value_set_for` tracker
# so the attribute is emitted on serialization.
def code=(val)
super
return unless val.is_a?(String) && !val.empty?
return if @text.is_a?(String) && !@text.empty?

description = Isoics.fetch(val)&.description
self.text = description if description
end

# When the deserializer reaches the end of the XML element and
# records that <text> was absent, it calls `using_default_for(:text)`
# to mark the attribute as default-valued (suppressing serialization).
# Refuse that mark if we've already populated text from Isoics so the
# value survives round-trip. See #112.
def using_default_for(attribute_name)
return if attribute_name == :text &&
@text.is_a?(String) && !@text.empty?

super
end
end
end
Expand Down
66 changes: 66 additions & 0 deletions spec/relaton/bib/model/ics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,70 @@
end
end
end

describe "XML serialization with Isoics fallback" do
# These tests guard the round-trip behaviour that lets <ics><code>X</code></ics>
# (with <text> absent from the source) re-serialize with the Isoics
# description filled in. This relies on the Isoics fallback being
# written through the public setter so lutaml-model marks the value as
# "set" and emits it.

context "ICS.new with a known code and no text" do
it "emits the Isoics description as <text>" do
ics = Relaton::Bib::ICS.new(code: "67.060")
expect(ics.to_xml).to include(
"<text>Cereals, pulses and derived products</text>",
)
end
end

context "ICS.from_xml standalone with no <text>" do
it "fills in <text> from Isoics on re-serialization" do
ics = Relaton::Bib::ICS.from_xml("<ics><code>67.060</code></ics>")
expect(ics.to_xml).to include("<code>67.060</code>")
expect(ics.to_xml).to include(
"<text>Cereals, pulses and derived products</text>",
)
end
end

context "Ext.from_xml nesting an ICS with no <text>" do
# This is the canonical metanorma collection round-trip path: the
# ICS is reached as a nested-collection child of <ext>, not via
# ICS.from_xml directly.
it "fills in <text> from Isoics on re-serialization of the parent" do
ext = Relaton::Bib::Ext.from_xml(
"<ext><ics><code>67.060</code></ics></ext>",
)
expect(ext.to_xml).to include(
"<text>Cereals, pulses and derived products</text>",
)
end
end

context "explicit text wins over Isoics" do
it "emits the explicit text in standalone to_xml" do
ics = Relaton::Bib::ICS.new(
code: "67.060", text: "Custom override text",
)
expect(ics.to_xml).to include("<text>Custom override text</text>")
expect(ics.to_xml).not_to include("Cereals")
end

it "preserves explicit text through Ext round-trip" do
ext = Relaton::Bib::Ext.from_xml(
"<ext><ics><code>67.060</code><text>Custom</text></ics></ext>",
)
expect(ext.to_xml).to include("<text>Custom</text>")
expect(ext.to_xml).not_to include("Cereals")
end
end

context "invalid ICS code" do
it "does not emit <text>" do
ics = Relaton::Bib::ICS.new(code: "not-a-real-code")
expect(ics.to_xml).not_to include("<text>")
end
end
end
end
Loading