Skip to content

Commit 6522c81

Browse files
committed
Add validator for type and whether a field is required
1 parent 3df98a3 commit 6522c81

File tree

4 files changed

+100
-13
lines changed

4 files changed

+100
-13
lines changed

lib/docx/elements/style.rb

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ def self.attributes
1212
@@attributes
1313
end
1414

15-
def self.attribute(name, *selectors, converter: Converters::DefaultValueConverter, validator: Validators::DefaultValidator)
15+
def self.required_attributes
16+
@@attributes.select { |a| a[:required] }
17+
end
18+
19+
def self.attribute(name, *selectors, required: false, converter: Converters::DefaultValueConverter, validator: Validators::DefaultValidator)
20+
@@attributes ||= []
21+
@@attributes << {name: name, selectors: selectors, required: required, converter: converter, validator: validator}
22+
1623
define_method(name) do
1724
selectors
1825
.lazy
@@ -22,15 +29,27 @@ def self.attribute(name, *selectors, converter: Converters::DefaultValueConverte
2229
end
2330

2431
define_method("#{name}=") do |value|
25-
validator.validate(value) || raise(Errors::StyleInvalidPropertyValue, "Invalid value for #{name}: #{value}")
32+
(required && value.nil?) &&
33+
raise(Errors::StyleRequiredPropertyValue, "Required value #{name}")
34+
35+
validator.validate(value) ||
36+
raise(Errors::StyleInvalidPropertyValue, "Invalid value for #{name}: '#{value.nil? ? "nil" : value}'")
37+
38+
encoded_value = converter.encode(value)
2639

2740
selectors.map do |attribute_xpath|
28-
encoded_value = converter.encode(value).to_s
2941
if (existing_attribute = node.at_xpath(attribute_xpath))
30-
existing_attribute.value = encoded_value
31-
next value
42+
if encoded_value.nil?
43+
existing_attribute.remove
44+
else
45+
existing_attribute.value = encoded_value
46+
end
47+
48+
next encoded_value
3249
end
3350

51+
next encoded_value if encoded_value.nil?
52+
3453
node_xpath, attribute = attribute_xpath.split("/@")
3554

3655
created_node =
@@ -40,7 +59,8 @@ def self.attribute(name, *selectors, converter: Converters::DefaultValueConverte
4059
# find the child node
4160
parent_node.at_xpath(child_xpath) ||
4261
# or create the child node
43-
Nokogiri::XML::Node.new(child_xpath, parent_node).tap { |created_child_node| parent_node << created_child_node }
62+
Nokogiri::XML::Node.new(child_xpath, parent_node)
63+
.tap { |created_child_node| parent_node << created_child_node }
4464
end
4565

4666
created_node.set_attribute(attribute, encoded_value)
@@ -67,9 +87,9 @@ def initialize(configuration, node, **attributes)
6787

6888
attr_accessor :node
6989

70-
attribute :id, "./@w:styleId"
71-
attribute :name, "./w:name/@w:val", "./w:next/@w:val"
72-
attribute :type, ".//@w:type"
90+
attribute :id, "./@w:styleId", required: true
91+
attribute :name, "./w:name/@w:val", "./w:next/@w:val", required: true
92+
attribute :type, ".//@w:type", required: true, validator: Validators::ValueValidator.new("paragraph", "character", "table", "numbering")
7393
attribute :keep_next, "./w:pPr/w:keepNext/@w:val", converter: Converters::BooleanConverter
7494
attribute :keep_lines, "./w:pPr/w:keepLines/@w:val", converter: Converters::BooleanConverter
7595
attribute :page_break_before, "./w:pPr/w:pageBreakBefore/@w:val", converter: Converters::BooleanConverter
@@ -112,6 +132,16 @@ def initialize(configuration, node, **attributes)
112132
attribute :vertical_alignment, "./w:rPr/w:vertAlign/@w:val"
113133
attribute :lang, "./w:rPr/w:lang/@w:val"
114134

135+
def valid?
136+
self.class.required_attributes.all? do |a|
137+
validator = a[:validator]
138+
attribute_name = a[:name]
139+
attribute_value = self.send(attribute_name)
140+
141+
validator&.validate(attribute_value)
142+
end
143+
end
144+
115145
def to_xml
116146
node.to_xml
117147
end

lib/docx/elements/style/validators.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ def self.validate(value)
1515
value =~ COLOR_REGEX
1616
end
1717
end
18+
19+
class ValueValidator
20+
def initialize(*values)
21+
@values = values
22+
end
23+
24+
def validate(value)
25+
@values.include?(value)
26+
end
27+
end
1828
end
1929
end
2030
end

lib/docx/errors.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ module Docx
22
module Errors
33
StyleNotFound = Class.new(StandardError)
44
StyleInvalidPropertyValue = Class.new(StandardError)
5+
StyleRequiredPropertyValue = Class.new(StandardError)
56
end
67
end

spec/docx/elements/style_spec.rb

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55

66
describe Docx::Elements::Style do
77
let(:fixture_path) { Dir.pwd + "/spec/fixtures/partial_styles/full.xml" }
8-
9-
let(:node) do
10-
Nokogiri::XML(File.open(fixture_path)).root.children[1]
11-
end
8+
let(:fixture_xml) { File.read(fixture_path) }
9+
let(:node) { Nokogiri::XML(fixture_xml).root.children[1] }
1210
let(:style) { described_class.new(double(:configuration), node) }
1311

1412
it "should extract attributes" do
@@ -83,6 +81,14 @@
8381
expect(node.at_xpath("./w:rPr/w:shd/@w:val").value).to eq("complex")
8482
end
8583

84+
it "should allow setting attributes to nil" do
85+
style.shading_style = nil
86+
87+
expect(style.shading_style).to eq(nil)
88+
expect(node.at_xpath("./w:pPr/w:shd/@w:val")).to eq(nil)
89+
expect { node.at_xpath("./w:pPr/w:shd/@w:val").value }.to raise_error(NoMethodError) # i.e. it's gone!
90+
end
91+
8692
describe "#to_xml" do
8793
it "should return the node as XML" do
8894
expect(style.to_xml).to eq(node.to_xml)
@@ -99,6 +105,46 @@
99105
end
100106
end
101107

108+
describe "validation" do
109+
let(:fixture_path) { Dir.pwd + "/spec/fixtures/partial_styles/basic.xml" }
110+
111+
it "validation: id" do
112+
expect { style.id = nil }.to raise_error(Docx::Errors::StyleRequiredPropertyValue)
113+
end
114+
115+
it "validation: name" do
116+
expect { style.name = nil }.to raise_error(Docx::Errors::StyleRequiredPropertyValue)
117+
end
118+
119+
it "validation: type" do
120+
expect { style.type = nil }.to raise_error(Docx::Errors::StyleRequiredPropertyValue)
121+
122+
expect { style.type = "invalid" }.to raise_error(Docx::Errors::StyleInvalidPropertyValue)
123+
end
124+
125+
it "true" do
126+
expect(style).to be_valid
127+
end
128+
129+
describe "unhappy" do
130+
let(:fixture_xml) do
131+
<<~XML
132+
<?xml version="1.0" encoding="UTF-8"?>
133+
<w:styles xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml">
134+
<w:style w:type="" w:styleId="">
135+
<w:name/>
136+
</w:style>
137+
</w:styles>
138+
XML
139+
end
140+
141+
it "false" do
142+
expect(style).to_not be_valid
143+
end
144+
end
145+
146+
end
147+
102148
describe "basic" do
103149
let(:fixture_path) { Dir.pwd + "/spec/fixtures/partial_styles/basic.xml" }
104150

0 commit comments

Comments
 (0)