rng 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: []