rng 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +23 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/Gemfile +16 -0
- data/Rakefile +12 -0
- data/lib/rng/attribute.rb +13 -0
- data/lib/rng/builder.rb +158 -0
- data/lib/rng/define.rb +14 -0
- data/lib/rng/element.rb +26 -0
- data/lib/rng/rnc_parser.rb +136 -0
- data/lib/rng/rng_parser.rb +107 -0
- data/lib/rng/schema.rb +18 -0
- data/lib/rng/start.rb +14 -0
- data/lib/rng/version.rb +5 -0
- data/lib/rng.rb +12 -0
- data/rng.gemspec +38 -0
- data/sig/rng.rbs +4 -0
- data/spec/rng/rnc_parser_spec.rb +66 -0
- data/spec/rng/rng_parser_spec.rb +102 -0
- data/spec/rng/schema_spec.rb +193 -0
- data/spec/rng_spec.rb +7 -0
- data/spec/spec_helper.rb +23 -0
- metadata +114 -0
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
data/.rspec
ADDED
data/.rubocop.yml
ADDED
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,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
|
data/lib/rng/builder.rb
ADDED
@@ -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
|
data/lib/rng/element.rb
ADDED
@@ -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
|
data/lib/rng/version.rb
ADDED
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,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
data/spec/spec_helper.rb
ADDED
@@ -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: []
|