Skip to content

Commit

Permalink
chore: more specs
Browse files Browse the repository at this point in the history
  • Loading branch information
ronaldtse committed Nov 27, 2024
1 parent b60386d commit 2201e1a
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 46 deletions.
8 changes: 6 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ source "https://rubygems.org"
# Specify your gem's dependencies in rng.gemspec
gemspec

gem "equivalent-xml"
gem "nokogiri"
gem "xml-c14n"
gem "rake", "~> 13.0"

gem "rspec", "~> 3.0"

gem "rubocop", "~> 1.21"
gem "rubocop-performance", require: false
gem "rubocop-rake", require: false
gem "rubocop-rspec", require: false
55 changes: 39 additions & 16 deletions lib/rng/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,45 @@ def build_rng(schema)
root.add_child(define_node)
end
elsif schema.is_a?(Rng::Element)
doc.root = build_rng_element(schema, doc)
el = build_rng_element(schema, doc)
el["xmlns"] = "http://relaxng.org/ns/structure/1.0"
doc.root = el
end

doc.to_xml
end

def build_rng_element(element, doc)
el = Nokogiri::XML::Node.new("element", doc)
el["name"] = element.name
if element.zero_or_more&.any?
zero_or_more = Nokogiri::XML::Node.new("zeroOrMore", doc)
el = Nokogiri::XML::Node.new("element", doc)
el["name"] = element.name
add_element_content(element, el, doc)
zero_or_more.add_child(el)
return zero_or_more
elsif element.one_or_more&.any?
one_or_more = Nokogiri::XML::Node.new("oneOrMore", doc)
el = Nokogiri::XML::Node.new("element", doc)
el["name"] = element.name
add_element_content(element, el, doc)
one_or_more.add_child(el)
return one_or_more
elsif element.optional&.any?
optional = Nokogiri::XML::Node.new("optional", doc)
el = Nokogiri::XML::Node.new("element", doc)
el["name"] = element.name
add_element_content(element, el, doc)
optional.add_child(el)
return optional
else
el = Nokogiri::XML::Node.new("element", doc)
el["name"] = element.name
add_element_content(element, el, doc)
return el
end
end

def add_element_content(element, el, doc)
element.attributes&.each do |attr|
attr_node = Nokogiri::XML::Node.new("attribute", doc)
attr_node["name"] = attr.name
Expand All @@ -76,14 +105,6 @@ def build_rng_element(element, doc)
text = Nokogiri::XML::Node.new("text", doc)
el.add_child(text)
end

if element.zero_or_more&.any?
zero_or_more = Nokogiri::XML::Node.new("zeroOrMore", doc)
zero_or_more.add_child(build_rng_element(element.zero_or_more.first, doc))
el = zero_or_more
end

el
end

def build_rnc(schema)
Expand All @@ -96,6 +117,8 @@ def build_rnc(schema)
end

def build_rnc_element(element, indent = 0)
return "" unless element # Handle nil elements

result = " " * indent
result += "element #{element.name} {\n"

Expand All @@ -105,11 +128,11 @@ def build_rnc_element(element, indent = 0)
result += ",\n" unless element.attributes.last == attr && !element.elements&.any? && !element.text
end

if element.elements&.any?
element.elements.each do |child|
result += build_rnc_element(child, indent + 1)
result += ",\n" unless element.elements.last == child && !element.text
end
element.elements&.each_with_index do |child, index|
child_result = build_rnc_element(child, indent + 1)
result += child_result
result += "," unless index == element.elements.size - 1 && !element.text
result += "\n"
end

if element.text
Expand Down
59 changes: 44 additions & 15 deletions lib/rng/rnc_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class RncParser < Parslet::Parser

rule(:attribute_def) {
str("attribute") >> space >>
identifier >>
identifier.as(:name) >>
whitespace >>
str("{") >>
whitespace >>
Expand All @@ -39,10 +39,10 @@ class RncParser < Parslet::Parser
rule(:text_def) { str("text").as(:text) }

rule(:content_item) {
(element_def | attribute_def | text_def).as(:item) >> comma?
((element_def | attribute_def | text_def).as(:item) >> comma?).repeat(1).as(:items)
}

rule(:content) { content_item.repeat(1) }
rule(:content) { content_item }

rule(:grammar) { whitespace >> element_def.as(:element) >> whitespace }

Expand All @@ -57,51 +57,80 @@ def parse(input)

def build_schema(tree)
element = tree[:element]
Rng::Schema.new(
start: Rng::Start.new(
Schema.new(
start: Start.new(
elements: [build_element(element)],
),
)
end

def build_element(element)
name = element[:identifier].to_s
content = element[:content]
content = element[:content]&.[](:items)
occurrence = element[:occurrence]

el = Rng::Element.new(
# Create base element
el = Element.new(
name: name,
attributes: [],
elements: [],
text: false,
)

if content
current_elements = []
current_attributes = []

content.each do |item|
case
when item[:item][:identifier] && item[:item][:type]
el.attributes << Rng::Attribute.new(
name: item[:item][:identifier].to_s,
when item[:item][:name] || (item[:item][:identifier] && item[:item][:type])
attr_name = item[:item][:name] || item[:item][:identifier]
attr = Attribute.new(
name: attr_name.to_s,
type: ["string"],
)
current_attributes << attr
when item[:item][:identifier]
el.elements << build_element(item[:item])
current_elements << build_element(item[:item])
when item[:item][:text]
el.text = true
end
end

el.attributes = current_attributes
el.elements = current_elements
end

# Handle occurrence modifiers
result = el
case occurrence
when "*"
el.zero_or_more = [el.dup]
result = Element.new(
name: el.name,
attributes: el.attributes,
elements: el.elements,
text: el.text,
)
result.zero_or_more = [el]
when "+"
el.one_or_more = [el.dup]
result = Element.new(
name: el.name,
attributes: el.attributes,
elements: el.elements,
text: el.text,
)
result.one_or_more = [el]
when "?"
el.optional = [el.dup]
result = Element.new(
name: el.name,
attributes: el.attributes,
elements: el.elements,
text: el.text,
)
result.optional = [el]
end

el
result
end
end
end
46 changes: 34 additions & 12 deletions lib/rng/rng_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,35 +47,57 @@ def parse_define(node)
end

def parse_element(node)
Element.new(
return nil unless node["name"]

element = Element.new(
name: node["name"],
attributes: node.xpath(".//attribute").map { |attr| parse_attribute(attr) },
elements: node.xpath("./element").map { |el| parse_element(el) },
text: !node.xpath(".//text").empty?,
zero_or_more: parse_zero_or_more(node),
one_or_more: parse_one_or_more(node),
optional: parse_optional(node),
choice: parse_choice(node),
attributes: [],
elements: [],
text: false,
)

node.children.each do |child|
parse_child(child, element)
end

element
end

def parse_child(node, element)
case node.name
when "attribute"
element.attributes << parse_attribute(node)
when "element"
element.elements << parse_element(node)
when "text"
element.text = true
when "zeroOrMore"
parse_zero_or_more(node).each { |el| element.zero_or_more << el }
when "oneOrMore"
parse_one_or_more(node).each { |el| element.one_or_more << el }
when "optional"
parse_optional(node).each { |el| element.optional << el }
end
end

def parse_attribute(node)
data_node = node.at_xpath(".//data")
Attribute.new(
name: node["name"],
type: node.xpath(".//data").map { |data| data["type"] },
type: data_node ? [data_node["type"]] : ["string"],
)
end

def parse_zero_or_more(node)
node.xpath(".//zeroOrMore/element").map { |el| parse_element(el) }
node.xpath("./element").map { |el| parse_element(el) }
end

def parse_one_or_more(node)
node.xpath(".//oneOrMore/element").map { |el| parse_element(el) }
node.xpath("./element").map { |el| parse_element(el) }
end

def parse_optional(node)
node.xpath(".//optional/element").map { |el| parse_element(el) }
node.xpath("./element").map { |el| parse_element(el) }
end

def parse_choice(node)
Expand Down
4 changes: 3 additions & 1 deletion spec/rng/schema_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
describe "RNG parsing and building" do
let(:rng_input) do
<<~RNG
<?xml version="1.0" encoding="UTF-8"?>
<element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
<zeroOrMore>
<element name="card">
Expand All @@ -31,7 +32,7 @@
it "correctly parses and rebuilds RNG" do
parsed = rng_parser.parse(rng_input)
rebuilt = builder.build(parsed, format: :rng)
expect(rebuilt.gsub(/\s+/, "")).to eq(rng_input.gsub(/\s+/, ""))
expect(normalize_xml(rebuilt)).to eq(normalize_xml(rng_input))
end
end

Expand All @@ -58,6 +59,7 @@
describe "RNG to RNC conversion" do
let(:rng_input) do
<<~RNG
<?xml version="1.0" encoding="UTF-8"?>
<element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
<zeroOrMore>
<element name="card">
Expand Down
8 changes: 8 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "rng"
require "xml/c14n"

RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
Expand All @@ -12,4 +13,11 @@
config.expect_with :rspec do |c|
c.syntax = :expect
end

# Add helper method for XML comparison
config.include(Module.new do
def normalize_xml(xml)
Xml::C14n.format(xml)
end
end)
end

0 comments on commit 2201e1a

Please sign in to comment.