rng 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.rubocop_todo.yml +64 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/Gemfile +3 -1
  6. data/README.adoc +402 -0
  7. data/lib/rng/any_name.rb +26 -0
  8. data/lib/rng/attribute.rb +58 -4
  9. data/lib/rng/choice.rb +60 -0
  10. data/lib/rng/data.rb +32 -0
  11. data/lib/rng/define.rb +51 -3
  12. data/lib/rng/element.rb +62 -16
  13. data/lib/rng/empty.rb +23 -0
  14. data/lib/rng/except.rb +62 -0
  15. data/lib/rng/external_ref.rb +28 -0
  16. data/lib/rng/grammar.rb +36 -0
  17. data/lib/rng/group.rb +60 -0
  18. data/lib/rng/include.rb +24 -0
  19. data/lib/rng/interleave.rb +58 -0
  20. data/lib/rng/list.rb +56 -0
  21. data/lib/rng/mixed.rb +58 -0
  22. data/lib/rng/name.rb +28 -0
  23. data/lib/rng/not_allowed.rb +23 -0
  24. data/lib/rng/ns_name.rb +31 -0
  25. data/lib/rng/one_or_more.rb +58 -0
  26. data/lib/rng/optional.rb +58 -0
  27. data/lib/rng/param.rb +30 -0
  28. data/lib/rng/parent_ref.rb +28 -0
  29. data/lib/rng/parse_rnc.rb +26 -0
  30. data/lib/rng/pattern.rb +24 -0
  31. data/lib/rng/ref.rb +28 -0
  32. data/lib/rng/rnc_parser.rb +351 -94
  33. data/lib/rng/start.rb +54 -5
  34. data/lib/rng/text.rb +26 -0
  35. data/lib/rng/to_rnc.rb +55 -0
  36. data/lib/rng/value.rb +29 -0
  37. data/lib/rng/version.rb +1 -1
  38. data/lib/rng/zero_or_more.rb +58 -0
  39. data/lib/rng.rb +29 -5
  40. data/rng.gemspec +3 -2
  41. data/spec/fixtures/rnc/address_book.rnc +10 -0
  42. data/spec/fixtures/rnc/complex_example.rnc +61 -0
  43. data/spec/fixtures/rng/address_book.rng +20 -0
  44. data/spec/fixtures/rng/relaxng.rng +335 -0
  45. data/spec/fixtures/rng/testSuite.rng +163 -0
  46. data/spec/fixtures/spectest.xml +6845 -0
  47. data/spec/rng/rnc_parser_spec.rb +6 -4
  48. data/spec/rng/rnc_roundtrip_spec.rb +121 -0
  49. data/spec/rng/schema_spec.rb +115 -166
  50. data/spec/rng/spectest_spec.rb +195 -0
  51. data/spec/spec_helper.rb +33 -0
  52. metadata +54 -7
  53. data/lib/rng/builder.rb +0 -158
  54. data/lib/rng/rng_parser.rb +0 -107
  55. data/lib/rng/schema.rb +0 -18
  56. data/spec/rng/rng_parser_spec.rb +0 -102
data/spec/spec_helper.rb CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  require "rng"
4
4
  require "xml/c14n"
5
+ require "equivalent-xml"
6
+ require "nokogiri"
7
+ require "diffy"
5
8
 
6
9
  RSpec.configure do |config|
7
10
  # Enable flags like --only-failures and --next-failure
@@ -19,5 +22,35 @@ RSpec.configure do |config|
19
22
  def normalize_xml(xml)
20
23
  Xml::C14n.format(xml)
21
24
  end
25
+
26
+ def format_xml(xml_string)
27
+ # Use Nokogiri to parse and format the XML
28
+ # Strip comments for comparison purposes - we care about structure, not documentation
29
+ doc = Nokogiri::XML(xml_string, &:noblanks)
30
+
31
+ # Remove all comment nodes before comparison
32
+ doc.xpath("//comment()").remove
33
+
34
+ doc.to_xml(indent: 2)
35
+ end
36
+
37
+ def diff_xml(actual, expected)
38
+ # Generate a readable diff between the formatted XML strings
39
+ Diffy::Diff.new(expected, actual, context: 3).to_s
40
+ end
22
41
  end)
42
+
43
+ # Add custom matchers for XML comparison
44
+ RSpec::Matchers.define :be_equivalent_to_xml do |expected|
45
+ match do |actual|
46
+ @actual_formatted = format_xml(actual.to_s)
47
+ @expected_formatted = format_xml(expected.to_s)
48
+ EquivalentXml.equivalent?(@actual_formatted, @expected_formatted)
49
+ end
50
+
51
+ failure_message do
52
+ diff = diff_xml(@actual_formatted, @expected_formatted)
53
+ "Expected XML to be equivalent, but it wasn't.\n\nDiff:\n#{diff}"
54
+ end
55
+ end
23
56
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rng
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-27 00:00:00.000000000 Z
11
+ date: 2025-06-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: parslet
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +53,7 @@ dependencies:
39
53
  - !ruby/object:Gem::Version
40
54
  version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
- name: nokogiri
56
+ name: zeitwerk
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - ">="
@@ -64,23 +78,56 @@ files:
64
78
  - ".gitignore"
65
79
  - ".rspec"
66
80
  - ".rubocop.yml"
81
+ - ".rubocop_todo.yml"
82
+ - CODE_OF_CONDUCT.md
67
83
  - Gemfile
84
+ - README.adoc
68
85
  - Rakefile
69
86
  - lib/rng.rb
87
+ - lib/rng/any_name.rb
70
88
  - lib/rng/attribute.rb
71
- - lib/rng/builder.rb
89
+ - lib/rng/choice.rb
90
+ - lib/rng/data.rb
72
91
  - lib/rng/define.rb
73
92
  - lib/rng/element.rb
93
+ - lib/rng/empty.rb
94
+ - lib/rng/except.rb
95
+ - lib/rng/external_ref.rb
96
+ - lib/rng/grammar.rb
97
+ - lib/rng/group.rb
98
+ - lib/rng/include.rb
99
+ - lib/rng/interleave.rb
100
+ - lib/rng/list.rb
101
+ - lib/rng/mixed.rb
102
+ - lib/rng/name.rb
103
+ - lib/rng/not_allowed.rb
104
+ - lib/rng/ns_name.rb
105
+ - lib/rng/one_or_more.rb
106
+ - lib/rng/optional.rb
107
+ - lib/rng/param.rb
108
+ - lib/rng/parent_ref.rb
109
+ - lib/rng/parse_rnc.rb
110
+ - lib/rng/pattern.rb
111
+ - lib/rng/ref.rb
74
112
  - lib/rng/rnc_parser.rb
75
- - lib/rng/rng_parser.rb
76
- - lib/rng/schema.rb
77
113
  - lib/rng/start.rb
114
+ - lib/rng/text.rb
115
+ - lib/rng/to_rnc.rb
116
+ - lib/rng/value.rb
78
117
  - lib/rng/version.rb
118
+ - lib/rng/zero_or_more.rb
79
119
  - rng.gemspec
80
120
  - sig/rng.rbs
121
+ - spec/fixtures/rnc/address_book.rnc
122
+ - spec/fixtures/rnc/complex_example.rnc
123
+ - spec/fixtures/rng/address_book.rng
124
+ - spec/fixtures/rng/relaxng.rng
125
+ - spec/fixtures/rng/testSuite.rng
126
+ - spec/fixtures/spectest.xml
81
127
  - spec/rng/rnc_parser_spec.rb
82
- - spec/rng/rng_parser_spec.rb
128
+ - spec/rng/rnc_roundtrip_spec.rb
83
129
  - spec/rng/schema_spec.rb
130
+ - spec/rng/spectest_spec.rb
84
131
  - spec/rng_spec.rb
85
132
  - spec/spec_helper.rb
86
133
  homepage: https://github.com/lutaml/rng
data/lib/rng/builder.rb DELETED
@@ -1,158 +0,0 @@
1
- module Rng
2
- class Builder
3
- def build(schema, format:)
4
- case format
5
- when :rng
6
- build_rng(schema)
7
- when :rnc
8
- build_rnc(schema)
9
- else
10
- raise ArgumentError, "Unsupported format: #{format}"
11
- end
12
- end
13
-
14
- private
15
-
16
- def build_rng(schema)
17
- doc = Nokogiri::XML::Document.new
18
- doc.encoding = "UTF-8"
19
-
20
- if schema.is_a?(Rng::Schema)
21
- root = Nokogiri::XML::Node.new("grammar", doc)
22
- root["xmlns"] = "http://relaxng.org/ns/structure/1.0"
23
- doc.root = root
24
-
25
- start = Nokogiri::XML::Node.new("start", doc)
26
- root.add_child(start)
27
-
28
- if schema.start.ref
29
- ref = Nokogiri::XML::Node.new("ref", doc)
30
- ref["name"] = schema.start.ref
31
- start.add_child(ref)
32
- end
33
-
34
- schema.start.elements.each do |element|
35
- start.add_child(build_rng_element(element, doc))
36
- end
37
-
38
- schema.define&.each do |define|
39
- define_node = Nokogiri::XML::Node.new("define", doc)
40
- define_node["name"] = define.name
41
- define.elements.each do |element|
42
- define_node.add_child(build_rng_element(element, doc))
43
- end
44
- root.add_child(define_node)
45
- end
46
- elsif schema.is_a?(Rng::Element)
47
- el = build_rng_element(schema, doc)
48
- el["xmlns"] = "http://relaxng.org/ns/structure/1.0"
49
- doc.root = el
50
- end
51
-
52
- doc.to_xml
53
- end
54
-
55
- def build_rng_element(element, doc)
56
- if element.zero_or_more&.any?
57
- zero_or_more = Nokogiri::XML::Node.new("zeroOrMore", doc)
58
- el = Nokogiri::XML::Node.new("element", doc)
59
- el["name"] = element.name
60
- add_element_content(element, el, doc)
61
- zero_or_more.add_child(el)
62
- return zero_or_more
63
- elsif element.one_or_more&.any?
64
- one_or_more = Nokogiri::XML::Node.new("oneOrMore", doc)
65
- el = Nokogiri::XML::Node.new("element", doc)
66
- el["name"] = element.name
67
- add_element_content(element, el, doc)
68
- one_or_more.add_child(el)
69
- return one_or_more
70
- elsif element.optional&.any?
71
- optional = Nokogiri::XML::Node.new("optional", doc)
72
- el = Nokogiri::XML::Node.new("element", doc)
73
- el["name"] = element.name
74
- add_element_content(element, el, doc)
75
- optional.add_child(el)
76
- return optional
77
- else
78
- el = Nokogiri::XML::Node.new("element", doc)
79
- el["name"] = element.name
80
- add_element_content(element, el, doc)
81
- return el
82
- end
83
- end
84
-
85
- def add_element_content(element, el, doc)
86
- element.attributes&.each do |attr|
87
- attr_node = Nokogiri::XML::Node.new("attribute", doc)
88
- attr_node["name"] = attr.name
89
- if attr.type&.any?
90
- data = Nokogiri::XML::Node.new("data", doc)
91
- data["type"] = attr.type.first
92
- attr_node.add_child(data)
93
- else
94
- text = Nokogiri::XML::Node.new("text", doc)
95
- attr_node.add_child(text)
96
- end
97
- el.add_child(attr_node)
98
- end
99
-
100
- element.elements&.each do |child|
101
- el.add_child(build_rng_element(child, doc))
102
- end
103
-
104
- if element.text
105
- text = Nokogiri::XML::Node.new("text", doc)
106
- el.add_child(text)
107
- end
108
- end
109
-
110
- def build_rnc(schema)
111
- result = ""
112
- elements = schema.is_a?(Rng::Schema) ? schema.start.elements : [schema]
113
- elements.each do |element|
114
- result += build_rnc_element(element)
115
- end
116
- result
117
- end
118
-
119
- def build_rnc_element(element, indent = 0)
120
- return "" unless element # Handle nil elements
121
-
122
- result = " " * indent
123
- result += "element #{element.name} {\n"
124
-
125
- element.attributes&.each do |attr|
126
- result += " " * (indent + 1)
127
- result += "attribute #{attr.name} { text }"
128
- result += ",\n" unless element.attributes.last == attr && !element.elements&.any? && !element.text
129
- end
130
-
131
- element.elements&.each_with_index do |child, index|
132
- child_result = build_rnc_element(child, indent + 1)
133
- result += child_result
134
- result += "," unless index == element.elements.size - 1 && !element.text
135
- result += "\n"
136
- end
137
-
138
- if element.text
139
- result += " " * (indent + 1)
140
- result += "text"
141
- result += "\n"
142
- end
143
-
144
- result += " " * indent
145
- result += "}"
146
-
147
- if element.zero_or_more&.any?
148
- result += "*"
149
- elsif element.one_or_more&.any?
150
- result += "+"
151
- elsif element.optional&.any?
152
- result += "?"
153
- end
154
-
155
- result
156
- end
157
- end
158
- end
@@ -1,107 +0,0 @@
1
- require "nokogiri"
2
- require_relative "schema"
3
- require_relative "element"
4
-
5
- module Rng
6
- class RngParser
7
- RELAXNG_NS = "http://relaxng.org/ns/structure/1.0"
8
-
9
- def parse(input)
10
- doc = Nokogiri::XML(input)
11
- doc.remove_namespaces! # This simplifies namespace handling
12
-
13
- root = doc.root
14
- case root.name
15
- when "grammar"
16
- parse_grammar(doc)
17
- when "element"
18
- parse_element(doc)
19
- else
20
- raise Rng::Error, "Unexpected root element: #{root.name}"
21
- end
22
- end
23
-
24
- private
25
-
26
- def parse_grammar(doc)
27
- Schema.new(
28
- start: parse_start(doc.at_xpath("//start")),
29
- define: doc.xpath("//define").map { |define| parse_define(define) },
30
- )
31
- end
32
-
33
- def parse_start(node)
34
- return nil unless node
35
-
36
- Start.new(
37
- ref: node.at_xpath(".//ref")&.attr("name"),
38
- elements: node.xpath(".//element").map { |element| parse_element(element) },
39
- )
40
- end
41
-
42
- def parse_define(node)
43
- Define.new(
44
- name: node["name"],
45
- elements: node.xpath(".//element").map { |element| parse_element(element) },
46
- )
47
- end
48
-
49
- def parse_element(node)
50
- return nil unless node["name"]
51
-
52
- element = Element.new(
53
- name: node["name"],
54
- attributes: [],
55
- elements: [],
56
- text: false,
57
- )
58
-
59
- node.children.each do |child|
60
- parse_child(child, element)
61
- end
62
-
63
- element
64
- end
65
-
66
- def parse_child(node, element)
67
- case node.name
68
- when "attribute"
69
- element.attributes << parse_attribute(node)
70
- when "element"
71
- element.elements << parse_element(node)
72
- when "text"
73
- element.text = true
74
- when "zeroOrMore"
75
- parse_zero_or_more(node).each { |el| element.zero_or_more << el }
76
- when "oneOrMore"
77
- parse_one_or_more(node).each { |el| element.one_or_more << el }
78
- when "optional"
79
- parse_optional(node).each { |el| element.optional << el }
80
- end
81
- end
82
-
83
- def parse_attribute(node)
84
- data_node = node.at_xpath(".//data")
85
- Attribute.new(
86
- name: node["name"],
87
- type: data_node ? [data_node["type"]] : ["string"],
88
- )
89
- end
90
-
91
- def parse_zero_or_more(node)
92
- node.xpath("./element").map { |el| parse_element(el) }
93
- end
94
-
95
- def parse_one_or_more(node)
96
- node.xpath("./element").map { |el| parse_element(el) }
97
- end
98
-
99
- def parse_optional(node)
100
- node.xpath("./element").map { |el| parse_element(el) }
101
- end
102
-
103
- def parse_choice(node)
104
- node.xpath(".//choice/element").map { |el| parse_element(el) }
105
- end
106
- end
107
- end
data/lib/rng/schema.rb DELETED
@@ -1,18 +0,0 @@
1
- require "lutaml/model"
2
- require_relative "start"
3
- require_relative "define"
4
-
5
- module Rng
6
- class Schema < Lutaml::Model::Serializable
7
- attribute :start, Start
8
- attribute :define, Define, collection: true
9
-
10
- xml do
11
- root "grammar"
12
- namespace "http://relaxng.org/ns/structure/1.0"
13
-
14
- map_element "start", to: :start
15
- map_element "define", to: :define
16
- end
17
- end
18
- end
@@ -1,102 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe Rng::RngParser do
4
- let(:parser) { described_class.new }
5
-
6
- describe "#parse" do
7
- context "with a simple RNG schema" do
8
- let(:input) do
9
- <<~RNG
10
- <grammar xmlns="http://relaxng.org/ns/structure/1.0">
11
- <start>
12
- <element name="addressBook">
13
- <zeroOrMore>
14
- <element name="card">
15
- <element name="name">
16
- <text/>
17
- </element>
18
- <element name="email">
19
- <text/>
20
- </element>
21
- </element>
22
- </zeroOrMore>
23
- </element>
24
- </start>
25
- </grammar>
26
- RNG
27
- end
28
-
29
- it "correctly parses the schema" do
30
- result = parser.parse(input)
31
- expect(result).to be_a(Rng::Schema)
32
- expect(result.start.elements.first.name).to eq("addressBook")
33
- expect(result.start.elements.first.zero_or_more.first.name).to eq("card")
34
- expect(result.start.elements.first.zero_or_more.first.elements.map(&:name)).to eq(["name", "email"])
35
- end
36
- end
37
-
38
- context "with a complex RNG schema" do
39
- let(:input) do
40
- <<~RNG
41
- <grammar xmlns="http://relaxng.org/ns/structure/1.0">
42
- <start>
43
- <ref name="addressBook"/>
44
- </start>
45
- <define name="addressBook">
46
- <element name="addressBook">
47
- <zeroOrMore>
48
- <element name="card">
49
- <element name="name">
50
- <text/>
51
- </element>
52
- <element name="email">
53
- <text/>
54
- </element>
55
- <optional>
56
- <element name="note">
57
- <text/>
58
- </element>
59
- </optional>
60
- </element>
61
- </zeroOrMore>
62
- </element>
63
- </define>
64
- </grammar>
65
- RNG
66
- end
67
-
68
- it "correctly parses the schema" do
69
- result = parser.parse(input)
70
- expect(result).to be_a(Rng::Schema)
71
- expect(result.start.ref).to eq("addressBook")
72
- expect(result.define.first.name).to eq("addressBook")
73
- expect(result.define.first.elements.first.name).to eq("addressBook")
74
- end
75
- end
76
-
77
- context "with attributes" do
78
- let(:input) do
79
- <<~RNG
80
- <grammar xmlns="http://relaxng.org/ns/structure/1.0">
81
- <start>
82
- <element name="person">
83
- <attribute name="id">
84
- <data type="ID"/>
85
- </attribute>
86
- <element name="name">
87
- <text/>
88
- </element>
89
- </element>
90
- </start>
91
- </grammar>
92
- RNG
93
- end
94
-
95
- it "correctly parses attributes" do
96
- result = parser.parse(input)
97
- expect(result.start.elements.first.attributes.first.name).to eq("id")
98
- expect(result.start.elements.first.attributes.first.type).to eq(["ID"])
99
- end
100
- end
101
- end
102
- end