rng 0.1.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3b70e1b28d17dd9d6a287a404f218151f5a25ab38eba65aba55330d1e5692a08
4
+ data.tar.gz: d8524a2367eb0c73b64f9040555a2269ad2e6f8121a5ed5d05021c20ad00042b
5
+ SHA512:
6
+ metadata.gz: 06bc5ef5f3d56f356730e71b42ee2080ab449ed1c769446a315a8077a015327bea94635e172ab9dcbd1d2559eacbb21ad519f3c5dc48fc45c5c7c7ee577326e2
7
+ data.tar.gz: 2eca01084c177a824f02c5f30c03fc2463de4fee55fab370c9fc951bc51cbf56fadebee4f326c1472754f394361a4e932979d269bf130b1a23312af20feb6483
@@ -0,0 +1,15 @@
1
+ # Auto-generated by Cimas: Do not edit it manually!
2
+ # See https://github.com/metanorma/cimas
3
+ name: rake
4
+
5
+ on:
6
+ push:
7
+ branches: [ master, main ]
8
+ tags: [ v* ]
9
+ pull_request:
10
+
11
+ jobs:
12
+ rake:
13
+ uses: metanorma/ci/.github/workflows/generic-rake.yml@main
14
+ secrets:
15
+ pat_token: ${{ secrets.LUTAML_CI_PAT_TOKEN }}
@@ -0,0 +1,23 @@
1
+ # Auto-generated by Cimas: Do not edit it manually!
2
+ # See https://github.com/metanorma/cimas
3
+ name: release
4
+
5
+ on:
6
+ workflow_dispatch:
7
+ inputs:
8
+ next_version:
9
+ description: |
10
+ Next release version. Possible values: x.y.z, major, minor, patch or pre|rc|etc
11
+ required: true
12
+ default: 'skip'
13
+ repository_dispatch:
14
+ types: [ do-release ]
15
+
16
+ jobs:
17
+ release:
18
+ uses: metanorma/ci/.github/workflows/rubygems-release.yml@main
19
+ with:
20
+ next_version: ${{ github.event.inputs.next_version }}
21
+ secrets:
22
+ rubygems-api-key: ${{ secrets.LUTAML_CI_RUBYGEMS_API_KEY }}
23
+ pat_token: ${{ secrets.LUTAML_CI_PAT_TOKEN }}
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in rng.gemspec
6
+ gemspec
7
+
8
+ gem "equivalent-xml"
9
+ gem "nokogiri"
10
+ gem "xml-c14n"
11
+ gem "rake", "~> 13.0"
12
+ gem "rspec", "~> 3.0"
13
+ gem "rubocop", "~> 1.21"
14
+ gem "rubocop-performance", require: false
15
+ gem "rubocop-rake", require: false
16
+ gem "rubocop-rspec", require: false
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,13 @@
1
+ require "lutaml/model"
2
+
3
+ module Rng
4
+ class Attribute < Lutaml::Model::Serializable
5
+ attribute :name, :string
6
+ attribute :type, :string, collection: true
7
+
8
+ xml do
9
+ map_attribute "name", to: :name
10
+ map_element "data", to: :type
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,158 @@
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
data/lib/rng/define.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "lutaml/model"
2
+ require_relative "element"
3
+
4
+ module Rng
5
+ class Define < Lutaml::Model::Serializable
6
+ attribute :name, :string
7
+ attribute :elements, Element, collection: true
8
+
9
+ xml do
10
+ map_attribute "name", to: :name
11
+ map_element "element", to: :elements
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ require "lutaml/model"
2
+ require_relative "attribute"
3
+
4
+ module Rng
5
+ class Element < Lutaml::Model::Serializable
6
+ attribute :name, :string
7
+ attribute :attributes, Attribute, collection: true
8
+ attribute :elements, Element, collection: true
9
+ attribute :text, :boolean
10
+ attribute :zero_or_more, Element, collection: true
11
+ attribute :one_or_more, Element, collection: true
12
+ attribute :optional, Element, collection: true
13
+ attribute :choice, Element, collection: true
14
+
15
+ xml do
16
+ map_attribute "name", to: :name
17
+ map_element "attribute", to: :attributes
18
+ map_element "element", to: :elements
19
+ map_element "text", to: :text
20
+ map_element "zeroOrMore", to: :zero_or_more
21
+ map_element "oneOrMore", to: :one_or_more
22
+ map_element "optional", to: :optional
23
+ map_element "choice", to: :choice
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,136 @@
1
+ require "parslet"
2
+ require_relative "schema"
3
+
4
+ module Rng
5
+ class RncParser < Parslet::Parser
6
+ rule(:space) { match('\s').repeat(1) }
7
+ rule(:space?) { space.maybe }
8
+ rule(:newline) { (str("\r").maybe >> str("\n")).repeat(1) }
9
+ rule(:newline?) { newline.maybe }
10
+ rule(:whitespace) { (space | newline).repeat }
11
+ rule(:comma) { str(",") }
12
+ rule(:comma?) { (whitespace >> comma >> whitespace).maybe }
13
+
14
+ rule(:identifier) { match("[a-zA-Z0-9_]").repeat(1).as(:identifier) }
15
+
16
+ rule(:element_def) {
17
+ str("element") >> space >>
18
+ identifier >>
19
+ whitespace >>
20
+ str("{") >>
21
+ whitespace >>
22
+ content.maybe.as(:content) >>
23
+ whitespace >>
24
+ str("}") >>
25
+ (str("*") | str("+") | str("?")).maybe.as(:occurrence)
26
+ }
27
+
28
+ rule(:attribute_def) {
29
+ str("attribute") >> space >>
30
+ identifier.as(:name) >>
31
+ whitespace >>
32
+ str("{") >>
33
+ whitespace >>
34
+ (str("text")).as(:type) >>
35
+ whitespace >>
36
+ str("}")
37
+ }
38
+
39
+ rule(:text_def) { str("text").as(:text) }
40
+
41
+ rule(:content_item) {
42
+ ((element_def | attribute_def | text_def).as(:item) >> comma?).repeat(1).as(:items)
43
+ }
44
+
45
+ rule(:content) { content_item }
46
+
47
+ rule(:grammar) { whitespace >> element_def.as(:element) >> whitespace }
48
+
49
+ root(:grammar)
50
+
51
+ def parse(input)
52
+ tree = super(input.strip)
53
+ build_schema(tree)
54
+ end
55
+
56
+ private
57
+
58
+ def build_schema(tree)
59
+ element = tree[:element]
60
+ Schema.new(
61
+ start: Start.new(
62
+ elements: [build_element(element)],
63
+ ),
64
+ )
65
+ end
66
+
67
+ def build_element(element)
68
+ name = element[:identifier].to_s
69
+ content = element[:content]&.[](:items)
70
+ occurrence = element[:occurrence]
71
+
72
+ # Create base element
73
+ el = Element.new(
74
+ name: name,
75
+ attributes: [],
76
+ elements: [],
77
+ text: false,
78
+ )
79
+
80
+ if content
81
+ current_elements = []
82
+ current_attributes = []
83
+
84
+ content.each do |item|
85
+ case
86
+ when item[:item][:name] || (item[:item][:identifier] && item[:item][:type])
87
+ attr_name = item[:item][:name] || item[:item][:identifier]
88
+ attr = Attribute.new(
89
+ name: attr_name.to_s,
90
+ type: ["string"],
91
+ )
92
+ current_attributes << attr
93
+ when item[:item][:identifier]
94
+ current_elements << build_element(item[:item])
95
+ when item[:item][:text]
96
+ el.text = true
97
+ end
98
+ end
99
+
100
+ el.attributes = current_attributes
101
+ el.elements = current_elements
102
+ end
103
+
104
+ # Handle occurrence modifiers
105
+ result = el
106
+ case occurrence
107
+ when "*"
108
+ result = Element.new(
109
+ name: el.name,
110
+ attributes: el.attributes,
111
+ elements: el.elements,
112
+ text: el.text,
113
+ )
114
+ result.zero_or_more = [el]
115
+ when "+"
116
+ result = Element.new(
117
+ name: el.name,
118
+ attributes: el.attributes,
119
+ elements: el.elements,
120
+ text: el.text,
121
+ )
122
+ result.one_or_more = [el]
123
+ when "?"
124
+ result = Element.new(
125
+ name: el.name,
126
+ attributes: el.attributes,
127
+ elements: el.elements,
128
+ text: el.text,
129
+ )
130
+ result.optional = [el]
131
+ end
132
+
133
+ result
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,107 @@
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 ADDED
@@ -0,0 +1,18 @@
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
data/lib/rng/start.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "lutaml/model"
2
+ require_relative "element"
3
+
4
+ module Rng
5
+ class Start < Lutaml::Model::Serializable
6
+ attribute :ref, :string
7
+ attribute :elements, Element, collection: true
8
+
9
+ xml do
10
+ map_attribute "ref", to: :ref
11
+ map_element "element", to: :elements
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rng
4
+ VERSION = "0.1.1"
5
+ end
data/lib/rng.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rng/version"
4
+ require_relative "rng/rnc_parser"
5
+ require_relative "rng/rng_parser"
6
+ require_relative "rng/builder"
7
+
8
+ module Rng
9
+ class Error < StandardError; end
10
+
11
+ # Your code goes here...
12
+ end
data/rng.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rng/version"
4
+
5
+ all_files_in_git = Dir.chdir(File.expand_path(__dir__)) do
6
+ `git ls-files -z`.split("\x0")
7
+ end
8
+
9
+ Gem::Specification.new do |spec|
10
+ spec.name = "rng"
11
+ spec.version = Rng::VERSION
12
+ spec.authors = ["Ribose"]
13
+ spec.email = ["open.source@ribose.com"]
14
+
15
+ spec.summary = "Library to parse and build RELAX NG (RNG) and RELAX NG Compact Syntax (RNC) schemas."
16
+ spec.homepage = "https://github.com/lutaml/rng"
17
+ spec.license = "BSD-2-Clause"
18
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
19
+
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = spec.homepage
22
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ spec.files = all_files_in_git
26
+ .reject { |f| f.match(%r{\A(?:test|features|bin|\.)/}) }
27
+
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_dependency "lutaml-model"
33
+ spec.add_dependency "parslet"
34
+ spec.add_dependency "nokogiri"
35
+
36
+ # spec.add_dependency "thor"
37
+ spec.metadata["rubygems_mfa_required"] = "true"
38
+ end
data/sig/rng.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Rng
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Rng::RncParser do
4
+ let(:parser) { described_class.new }
5
+
6
+ describe "#parse" do
7
+ context "with a simple RNC schema" do
8
+ let(:input) do
9
+ <<~RNC
10
+ element addressBook {
11
+ element card {
12
+ element name { text },
13
+ element email { text }
14
+ }*
15
+ }
16
+ RNC
17
+ end
18
+
19
+ it "correctly parses the schema" do
20
+ result = parser.parse(input)
21
+ expect(result).to be_a(Rng::Schema)
22
+ expect(result.start.elements.first.name).to eq("addressBook")
23
+ expect(result.start.elements.first.elements.first.name).to eq("card")
24
+ expect(result.start.elements.first.elements.first.elements.map(&:name)).to eq(["name", "email"])
25
+ end
26
+ end
27
+
28
+ context "with attributes" do
29
+ let(:input) do
30
+ <<~RNC
31
+ element person {
32
+ attribute id { text },
33
+ element name { text }
34
+ }
35
+ RNC
36
+ end
37
+
38
+ it "correctly parses attributes" do
39
+ result = parser.parse(input)
40
+ expect(result.start.elements.first.name).to eq("person")
41
+ expect(result.start.elements.first.elements.first).to be_a(Rng::Attribute)
42
+ expect(result.start.elements.first.elements.first.name).to eq("id")
43
+ end
44
+ end
45
+
46
+ context "with nested elements" do
47
+ let(:input) do
48
+ <<~RNC
49
+ element root {
50
+ element child1 {
51
+ element grandchild { text }
52
+ },
53
+ element child2 { text }
54
+ }
55
+ RNC
56
+ end
57
+
58
+ it "correctly parses nested elements" do
59
+ result = parser.parse(input)
60
+ expect(result.start.elements.first.name).to eq("root")
61
+ expect(result.start.elements.first.elements.map(&:name)).to eq(["child1", "child2"])
62
+ expect(result.start.elements.first.elements.first.elements.first.name).to eq("grandchild")
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,102 @@
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
@@ -0,0 +1,193 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Rng::Schema do
4
+ let(:rng_parser) { Rng::RngParser.new }
5
+ let(:rnc_parser) { Rng::RncParser.new }
6
+ let(:builder) { Rng::Builder.new }
7
+
8
+ describe "RNG parsing and building" do
9
+ let(:rng_input) do
10
+ <<~RNG
11
+ <?xml version="1.0" encoding="UTF-8"?>
12
+ <element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
13
+ <zeroOrMore>
14
+ <element name="card">
15
+ <element name="name">
16
+ <text/>
17
+ </element>
18
+ <element name="email">
19
+ <text/>
20
+ </element>
21
+ <optional>
22
+ <element name="note">
23
+ <text/>
24
+ </element>
25
+ </optional>
26
+ </element>
27
+ </zeroOrMore>
28
+ </element>
29
+ RNG
30
+ end
31
+
32
+ it "correctly parses and rebuilds RNG" do
33
+ parsed = rng_parser.parse(rng_input)
34
+ rebuilt = builder.build(parsed, format: :rng)
35
+ expect(normalize_xml(rebuilt)).to eq(normalize_xml(rng_input))
36
+ end
37
+ end
38
+
39
+ describe "RNC parsing and building" do
40
+ let(:rnc_input) do
41
+ <<~RNC
42
+ element addressBook {
43
+ element card {
44
+ element name { text },
45
+ element email { text },
46
+ element note { text }?
47
+ }*
48
+ }
49
+ RNC
50
+ end
51
+
52
+ it "correctly parses and rebuilds RNC" do
53
+ parsed = rnc_parser.parse(rnc_input)
54
+ rebuilt = builder.build(parsed, format: :rnc)
55
+ expect(rebuilt.gsub(/\s+/, "")).to eq(rnc_input.gsub(/\s+/, ""))
56
+ end
57
+ end
58
+
59
+ describe "RNG to RNC conversion" do
60
+ let(:rng_input) do
61
+ <<~RNG
62
+ <?xml version="1.0" encoding="UTF-8"?>
63
+ <element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
64
+ <zeroOrMore>
65
+ <element name="card">
66
+ <element name="name">
67
+ <text/>
68
+ </element>
69
+ <element name="email">
70
+ <text/>
71
+ </element>
72
+ </element>
73
+ </zeroOrMore>
74
+ </element>
75
+ RNG
76
+ end
77
+
78
+ let(:expected_rnc) do
79
+ <<~RNC
80
+ element addressBook {
81
+ element card {
82
+ element name { text },
83
+ element email { text }
84
+ }*
85
+ }
86
+ RNC
87
+ end
88
+
89
+ it "correctly converts RNG to RNC" do
90
+ parsed = rng_parser.parse(rng_input)
91
+ rnc = builder.build(parsed, format: :rnc)
92
+ expect(rnc.gsub(/\s+/, "")).to eq(expected_rnc.gsub(/\s+/, ""))
93
+ end
94
+ end
95
+
96
+ describe "RNC to RNG conversion" do
97
+ let(:rnc_input) do
98
+ <<~RNC
99
+ element addressBook {
100
+ element card {
101
+ element name { text },
102
+ element email { text }
103
+ }*
104
+ }
105
+ RNC
106
+ end
107
+
108
+ let(:expected_rng) do
109
+ <<~RNG
110
+ <element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
111
+ <zeroOrMore>
112
+ <element name="card">
113
+ <element name="name">
114
+ <text/>
115
+ </element>
116
+ <element name="email">
117
+ <text/>
118
+ </element>
119
+ </element>
120
+ </zeroOrMore>
121
+ </element>
122
+ RNG
123
+ end
124
+
125
+ it "correctly converts RNC to RNG" do
126
+ parsed = rnc_parser.parse(rnc_input)
127
+ rng = builder.build(parsed, format: :rng)
128
+ expect(rng.gsub(/\s+/, "")).to eq(expected_rng.gsub(/\s+/, ""))
129
+ end
130
+ end
131
+
132
+ describe "Complex schema parsing and building" do
133
+ let(:complex_rng_input) do
134
+ <<~RNG
135
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0">
136
+ <start>
137
+ <ref name="addressBook"/>
138
+ </start>
139
+
140
+ <define name="addressBook">
141
+ <element name="addressBook">
142
+ <zeroOrMore>
143
+ <ref name="card"/>
144
+ </zeroOrMore>
145
+ </element>
146
+ </define>
147
+
148
+ <define name="card">
149
+ <element name="card">
150
+ <ref name="name"/>
151
+ <ref name="email"/>
152
+ <optional>
153
+ <ref name="note"/>
154
+ </optional>
155
+ </element>
156
+ </define>
157
+
158
+ <define name="name">
159
+ <element name="name">
160
+ <text/>
161
+ </element>
162
+ </define>
163
+
164
+ <define name="email">
165
+ <element name="email">
166
+ <text/>
167
+ </element>
168
+ </define>
169
+
170
+ <define name="note">
171
+ <element name="note">
172
+ <text/>
173
+ </element>
174
+ </define>
175
+ </grammar>
176
+ RNG
177
+ end
178
+
179
+ it "correctly parses and rebuilds complex RNG" do
180
+ parsed = rng_parser.parse(complex_rng_input)
181
+ rebuilt = builder.build(parsed, format: :rng)
182
+ expect(rebuilt.gsub(/\s+/, "")).to eq(complex_rng_input.gsub(/\s+/, ""))
183
+ end
184
+
185
+ it "correctly converts complex RNG to RNC" do
186
+ parsed = rng_parser.parse(complex_rng_input)
187
+ rnc = builder.build(parsed, format: :rnc)
188
+ reparsed = rnc_parser.parse(rnc)
189
+ rng_again = builder.build(reparsed, format: :rng)
190
+ expect(rng_again.gsub(/\s+/, "")).to eq(complex_rng_input.gsub(/\s+/, ""))
191
+ end
192
+ end
193
+ end
data/spec/rng_spec.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Rng do
4
+ it "has a version number" do
5
+ expect(Rng::VERSION).not_to be nil
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rng"
4
+ require "xml/c14n"
5
+
6
+ RSpec.configure do |config|
7
+ # Enable flags like --only-failures and --next-failure
8
+ config.example_status_persistence_file_path = ".rspec_status"
9
+
10
+ # Disable RSpec exposing methods globally on `Module` and `main`
11
+ config.disable_monkey_patching!
12
+
13
+ config.expect_with :rspec do |c|
14
+ c.syntax = :expect
15
+ end
16
+
17
+ # Add helper method for XML comparison
18
+ config.include(Module.new do
19
+ def normalize_xml(xml)
20
+ Xml::C14n.format(xml)
21
+ end
22
+ end)
23
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rng
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Ribose
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-11-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: lutaml-model
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: parslet
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'
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - open.source@ribose.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".github/workflows/rake.yml"
63
+ - ".github/workflows/release.yml"
64
+ - ".gitignore"
65
+ - ".rspec"
66
+ - ".rubocop.yml"
67
+ - Gemfile
68
+ - Rakefile
69
+ - lib/rng.rb
70
+ - lib/rng/attribute.rb
71
+ - lib/rng/builder.rb
72
+ - lib/rng/define.rb
73
+ - lib/rng/element.rb
74
+ - lib/rng/rnc_parser.rb
75
+ - lib/rng/rng_parser.rb
76
+ - lib/rng/schema.rb
77
+ - lib/rng/start.rb
78
+ - lib/rng/version.rb
79
+ - rng.gemspec
80
+ - sig/rng.rbs
81
+ - spec/rng/rnc_parser_spec.rb
82
+ - spec/rng/rng_parser_spec.rb
83
+ - spec/rng/schema_spec.rb
84
+ - spec/rng_spec.rb
85
+ - spec/spec_helper.rb
86
+ homepage: https://github.com/lutaml/rng
87
+ licenses:
88
+ - BSD-2-Clause
89
+ metadata:
90
+ homepage_uri: https://github.com/lutaml/rng
91
+ source_code_uri: https://github.com/lutaml/rng
92
+ bug_tracker_uri: https://github.com/lutaml/rng/issues
93
+ rubygems_mfa_required: 'true'
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 3.0.0
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.5.22
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Library to parse and build RELAX NG (RNG) and RELAX NG Compact Syntax (RNC)
113
+ schemas.
114
+ test_files: []