rfcxml 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/roundtrip.yml +79 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +9 -3
  5. data/.rubocop_todo.yml +86 -20
  6. data/Gemfile +3 -1
  7. data/README.adoc +255 -35
  8. data/Rakefile +55 -0
  9. data/lib/rfcxml/v3/abstract.rb +2 -1
  10. data/lib/rfcxml/v3/address.rb +2 -1
  11. data/lib/rfcxml/v3/annotation.rb +2 -1
  12. data/lib/rfcxml/v3/area.rb +1 -1
  13. data/lib/rfcxml/v3/artset.rb +2 -1
  14. data/lib/rfcxml/v3/artwork.rb +13 -10
  15. data/lib/rfcxml/v3/aside.rb +2 -1
  16. data/lib/rfcxml/v3/author.rb +16 -9
  17. data/lib/rfcxml/v3/back.rb +2 -1
  18. data/lib/rfcxml/v3/bcp14.rb +1 -1
  19. data/lib/rfcxml/v3/blockquote.rb +2 -1
  20. data/lib/rfcxml/v3/boilerplate.rb +2 -1
  21. data/lib/rfcxml/v3/br.rb +1 -1
  22. data/lib/rfcxml/v3/c.rb +2 -1
  23. data/lib/rfcxml/v3/city.rb +1 -1
  24. data/lib/rfcxml/v3/cityarea.rb +1 -1
  25. data/lib/rfcxml/v3/code.rb +1 -1
  26. data/lib/rfcxml/v3/contact.rb +2 -1
  27. data/lib/rfcxml/v3/country.rb +1 -1
  28. data/lib/rfcxml/v3/cref.rb +2 -1
  29. data/lib/rfcxml/v3/date.rb +4 -4
  30. data/lib/rfcxml/v3/dd.rb +2 -1
  31. data/lib/rfcxml/v3/displayreference.rb +1 -1
  32. data/lib/rfcxml/v3/dl.rb +2 -1
  33. data/lib/rfcxml/v3/dt.rb +2 -1
  34. data/lib/rfcxml/v3/email.rb +1 -1
  35. data/lib/rfcxml/v3/eref.rb +1 -1
  36. data/lib/rfcxml/v3/extaddr.rb +1 -1
  37. data/lib/rfcxml/v3/facsimile.rb +1 -1
  38. data/lib/rfcxml/v3/figure.rb +17 -11
  39. data/lib/rfcxml/v3/format.rb +1 -1
  40. data/lib/rfcxml/v3/front.rb +2 -1
  41. data/lib/rfcxml/v3/iref.rb +4 -3
  42. data/lib/rfcxml/v3/keyword.rb +1 -1
  43. data/lib/rfcxml/v3/li.rb +2 -1
  44. data/lib/rfcxml/v3/link.rb +1 -1
  45. data/lib/rfcxml/v3/list.rb +2 -1
  46. data/lib/rfcxml/v3/middle.rb +2 -1
  47. data/lib/rfcxml/v3/name.rb +2 -1
  48. data/lib/rfcxml/v3/note.rb +2 -1
  49. data/lib/rfcxml/v3/ol.rb +5 -2
  50. data/lib/rfcxml/v3/organization.rb +10 -5
  51. data/lib/rfcxml/v3/phone.rb +1 -1
  52. data/lib/rfcxml/v3/pobox.rb +1 -1
  53. data/lib/rfcxml/v3/postal.rb +2 -1
  54. data/lib/rfcxml/v3/postal_line.rb +1 -1
  55. data/lib/rfcxml/v3/postamble.rb +2 -1
  56. data/lib/rfcxml/v3/preamble.rb +2 -1
  57. data/lib/rfcxml/v3/refcontent.rb +2 -1
  58. data/lib/rfcxml/v3/reference.rb +7 -4
  59. data/lib/rfcxml/v3/referencegroup.rb +2 -1
  60. data/lib/rfcxml/v3/references.rb +2 -1
  61. data/lib/rfcxml/v3/region.rb +1 -1
  62. data/lib/rfcxml/v3/relref.rb +1 -1
  63. data/lib/rfcxml/v3/rfc.rb +60 -12
  64. data/lib/rfcxml/v3/section.rb +11 -4
  65. data/lib/rfcxml/v3/series_info.rb +5 -4
  66. data/lib/rfcxml/v3/sortingcode.rb +1 -1
  67. data/lib/rfcxml/v3/sourcecode.rb +13 -9
  68. data/lib/rfcxml/v3/spanx.rb +1 -1
  69. data/lib/rfcxml/v3/street.rb +1 -1
  70. data/lib/rfcxml/v3/strong.rb +2 -1
  71. data/lib/rfcxml/v3/sub.rb +2 -1
  72. data/lib/rfcxml/v3/sup.rb +2 -1
  73. data/lib/rfcxml/v3/table.rb +1 -1
  74. data/lib/rfcxml/v3/tbody.rb +1 -1
  75. data/lib/rfcxml/v3/td.rb +23 -4
  76. data/lib/rfcxml/v3/text.rb +2 -1
  77. data/lib/rfcxml/v3/texttable.rb +12 -6
  78. data/lib/rfcxml/v3/tfoot.rb +1 -1
  79. data/lib/rfcxml/v3/th.rb +1 -1
  80. data/lib/rfcxml/v3/thead.rb +1 -1
  81. data/lib/rfcxml/v3/title.rb +3 -2
  82. data/lib/rfcxml/v3/toc.rb +1 -1
  83. data/lib/rfcxml/v3/tr.rb +3 -1
  84. data/lib/rfcxml/v3/tt.rb +1 -1
  85. data/lib/rfcxml/v3/ttcol.rb +4 -2
  86. data/lib/rfcxml/v3/u.rb +1 -1
  87. data/lib/rfcxml/v3/ul.rb +10 -4
  88. data/lib/rfcxml/v3/uri.rb +1 -1
  89. data/lib/rfcxml/v3/vspace.rb +1 -1
  90. data/lib/rfcxml/v3/workgroup.rb +1 -1
  91. data/lib/rfcxml/v3/xref.rb +2 -1
  92. data/lib/rfcxml/version.rb +1 -1
  93. data/lib/rfcxml.rb +1 -0
  94. data/scripts/README.md +110 -0
  95. data/scripts/roundtrip_test.rb +361 -0
  96. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53aebb436f8ea53c627fba3203b2fa9f0186b8f7ac0f4ffd794d2426728172ed
4
- data.tar.gz: 7cac09e19781c1bbb31820aaa40bbc7df7f8cb35b7306fbc87496429cf1e7135
3
+ metadata.gz: d78bc97fa8b73803d80d88b9e3d69d3ab98ea34b481d45b4844b685943cb518e
4
+ data.tar.gz: 144d9c7c64dc113af0764099683f33bc8eb9f3f3e6ae70077ae51e43d2e2891b
5
5
  SHA512:
6
- metadata.gz: ea24ee3e3386a8907be82e4a54e6616900b75ec4143ff4bad0e6d79a6d6da8215d7350e778f8f07850156f4789ab010ebc5d0d82533fbaa3a6503496b3a50589
7
- data.tar.gz: 3f3a69b7bfd73af3b56fac723256aabcc820c81b572003381ede682f41a0f84535f007465d0368a9644163f930673722f14817800b2e15986eeffcb3529c51c8
6
+ metadata.gz: 284650ce2accd2ff0a7d75089dd773dce3126e20dcb3a7c02d600612ee4f7bce134b16662e001c2642dedf2d949018550c6d7db6f3781524747ca8aa87b387b9
7
+ data.tar.gz: cee43215ce256f935a1e7986ca1fdfd30de4ac3f44bdd47caf8a5d34ed7fdc7166bb51a3f7db4d993132868e72aeafe255f2c013491c2ccee29c604abed80afc
@@ -0,0 +1,79 @@
1
+ # GitHub Actions workflow for RFC XML round-trip testing
2
+ # Tests all RFC XML v3 files from rfc-editor.org
3
+ #
4
+ # Issue: https://github.com/metanorma/rfcxml/issues/4
5
+
6
+ name: RFC Round-Trip Tests
7
+
8
+ on:
9
+ # Manual trigger
10
+ workflow_dispatch:
11
+ # Weekly run to catch new RFCs (Sundays at 00:00 UTC)
12
+ schedule:
13
+ - cron: '0 0 * * 0'
14
+ # Also run on pushes to main for early detection of issues
15
+ push:
16
+ branches: [ main ]
17
+ paths:
18
+ - 'lib/**'
19
+ - 'spec/**'
20
+ - 'scripts/roundtrip_test.rb'
21
+ pull_request:
22
+ paths:
23
+ - 'lib/**'
24
+ - 'spec/**'
25
+ - 'scripts/roundtrip_test.rb'
26
+
27
+ permissions:
28
+ contents: read
29
+
30
+ jobs:
31
+ roundtrip:
32
+ name: Test RFC XML Round-Trips
33
+ runs-on: ubuntu-latest
34
+
35
+ steps:
36
+ - name: Checkout repository
37
+ uses: actions/checkout@v4
38
+
39
+ - name: Set up Ruby
40
+ uses: ruby/setup-ruby@v1
41
+ with:
42
+ ruby-version: '3.2'
43
+ bundler-cache: true
44
+
45
+ - name: Download RFC XML files
46
+ run: |
47
+ mkdir -p rfc-xml-source
48
+ echo "Downloading RFC XML tarball..."
49
+ curl -L -o xmlsource.tar.gz "https://www.rfc-editor.org/in-notes/tar/xmlsource-all.tar.gz"
50
+ echo "Extracting..."
51
+ tar -xzf xmlsource.tar.gz -C rfc-xml-source
52
+ # The tarball extracts to a subdirectory, find it
53
+ XML_DIR=$(find rfc-xml-source -name "*.xml" -type f | head -1 | xargs dirname)
54
+ echo "XML_DIR=$XML_DIR" >> $GITHUB_ENV
55
+ FILE_COUNT=$(find "$XML_DIR" -name "*.xml" -type f | wc -l)
56
+ echo "Found $FILE_COUNT XML files in $XML_DIR"
57
+
58
+ - name: Run round-trip tests
59
+ run: |
60
+ echo "Testing all RFC XML files..."
61
+ ruby scripts/roundtrip_test.rb "$XML_DIR"
62
+ continue-on-error: true
63
+ id: roundtrip
64
+
65
+ - name: Upload test results
66
+ if: always()
67
+ uses: actions/upload-artifact@v4
68
+ with:
69
+ name: roundtrip-results
70
+ path: |
71
+ *.log
72
+ retention-days: 30
73
+ if-no-files-found: ignore
74
+
75
+ - name: Check results
76
+ if: steps.roundtrip.outcome == 'failure'
77
+ run: |
78
+ echo "::error::Some RFC XML files failed round-trip testing"
79
+ exit 1
data/.gitignore CHANGED
@@ -7,5 +7,8 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
 
10
+ # Downloaded RFC XML files
11
+ /spec/fixtures/xmlsource/
12
+
10
13
  # rspec failure tracking
11
14
  .rspec_status
data/.rubocop.yml CHANGED
@@ -1,11 +1,17 @@
1
1
  # Auto-generated by Cimas: Do not edit it manually!
2
2
  # See https://github.com/metanorma/cimas
3
3
  inherit_from:
4
+ - https://raw.githubusercontent.com/riboseinc/oss-guides/main/ci/rubocop.yml
4
5
  - .rubocop_todo.yml
5
- - https://raw.githubusercontent.com/riboseinc/oss-guides/master/ci/rubocop.yml
6
+
7
+ inherit_mode:
8
+ merge:
9
+ - Exclude
6
10
 
7
11
  # local repo-specific modifications
8
12
  # ...
13
+ plugins:
14
+ - rubocop-rspec
15
+ - rubocop-performance
16
+ - rubocop-rake
9
17
 
10
- AllCops:
11
- TargetRubyVersion: 3.4
data/.rubocop_todo.yml CHANGED
@@ -1,42 +1,108 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-03-18 06:11:32 UTC using RuboCop version 1.85.1.
3
+ # on 2026-03-19 12:31:01 UTC using RuboCop version 1.85.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
9
  # Offense count: 1
10
- # This cop supports safe autocorrection (--autocorrect).
11
- Gemspec/RequireMFA:
10
+ Gemspec/RequiredRubyVersion:
12
11
  Exclude:
13
12
  - 'rfcxml.gemspec'
14
13
 
15
- # Offense count: 1
16
- Gemspec/RequiredRubyVersion:
14
+ # Offense count: 22
15
+ # This cop supports safe autocorrection (--autocorrect).
16
+ # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
17
+ # URISchemes: http, https
18
+ Layout/LineLength:
17
19
  Exclude:
18
- - 'rfcxml.gemspec'
20
+ - 'canon_attribute_order_bug.rb'
21
+ - 'lib/rfcxml/v3/li.rb'
22
+ - 'lib/rfcxml/v3/rfc.rb'
23
+ - 'scripts/roundtrip_test.rb'
24
+ - 'spec/v3/document_creation_spec.rb'
25
+
26
+ # Offense count: 4
27
+ Lint/NoReturnInBeginEndBlocks:
28
+ Exclude:
29
+ - 'scripts/roundtrip_test.rb'
30
+
31
+ # Offense count: 6
32
+ # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
33
+ Metrics/AbcSize:
34
+ Exclude:
35
+ - 'lib/rfcxml/v3/rfc.rb'
36
+ - 'scripts/roundtrip_test.rb'
37
+
38
+ # Offense count: 3
39
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
40
+ # AllowedMethods: refine
41
+ Metrics/BlockLength:
42
+ Max: 37
43
+
44
+ # Offense count: 3
45
+ # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
46
+ Metrics/CyclomaticComplexity:
47
+ Exclude:
48
+ - 'lib/rfcxml/v3/rfc.rb'
49
+ - 'scripts/roundtrip_test.rb'
50
+
51
+ # Offense count: 9
52
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
53
+ Metrics/MethodLength:
54
+ Max: 50
55
+
56
+ # Offense count: 3
57
+ # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
58
+ Metrics/PerceivedComplexity:
59
+ Exclude:
60
+ - 'lib/rfcxml/v3/rfc.rb'
61
+ - 'scripts/roundtrip_test.rb'
19
62
 
20
63
  # Offense count: 1
21
64
  # This cop supports safe autocorrection (--autocorrect).
22
65
  # Configuration parameters: EnforcedStyle.
23
- # SupportedStyles: normal, indented_internal_methods
24
- Layout/IndentationConsistency:
66
+ # SupportedStyles: be, be_nil
67
+ RSpec/BeNil:
25
68
  Exclude:
26
- - 'rfcxml.gemspec'
69
+ - 'spec/rfcxml_spec.rb'
27
70
 
28
71
  # Offense count: 1
29
- # This cop supports safe autocorrection (--autocorrect).
30
- # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
31
- # URISchemes: http, https
32
- Layout/LineLength:
72
+ # Configuration parameters: IgnoredMetadata.
73
+ RSpec/DescribeClass:
33
74
  Exclude:
34
- - 'lib/rfcxml/v3/li.rb'
75
+ - 'spec/v3/document_creation_spec.rb'
35
76
 
36
- # Offense count: 2
37
- # This cop supports safe autocorrection (--autocorrect).
38
- # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
39
- # SupportedStyles: single_quotes, double_quotes
40
- Style/StringLiterals:
77
+ # Offense count: 19
78
+ # This cop supports unsafe autocorrection (--autocorrect-all).
79
+ # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants.
80
+ # SupportedStyles: described_class, explicit
81
+ RSpec/DescribedClass:
41
82
  Exclude:
42
- - 'rfcxml.gemspec'
83
+ - 'spec/v3/author_spec.rb'
84
+ - 'spec/v3/rfc_spec.rb'
85
+ - 'spec/v3/section_spec.rb'
86
+
87
+ # Offense count: 11
88
+ # Configuration parameters: CountAsOne.
89
+ RSpec/ExampleLength:
90
+ Max: 29
91
+
92
+ # Offense count: 11
93
+ RSpec/MultipleExpectations:
94
+ Max: 9
95
+
96
+ # Offense count: 3
97
+ # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
98
+ # SupportedInflectors: default, active_support
99
+ RSpec/SpecFilePathFormat:
100
+ Exclude:
101
+ - 'spec/v3/author_spec.rb'
102
+ - 'spec/v3/rfc_spec.rb'
103
+ - 'spec/v3/section_spec.rb'
104
+
105
+ # Offense count: 1
106
+ Security/Open:
107
+ Exclude:
108
+ - 'Rakefile'
data/Gemfile CHANGED
@@ -5,10 +5,12 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in genericode.gemspec
6
6
  gemspec
7
7
 
8
- gem "canon"
8
+ gem "canon", github: "ronaldtse/canon", branch: "fix/empty-attribute-value"
9
9
  gem "lutaml-model", github: "lutaml/lutaml-model", ref: "main"
10
10
  gem "nokogiri"
11
11
  gem "rake", "~> 13.0"
12
12
  gem "rspec", "~> 3.0"
13
13
  gem "rubocop"
14
14
  gem "rubocop-performance"
15
+ gem "rubocop-rake"
16
+ gem "rubocop-rspec"
data/README.adoc CHANGED
@@ -1,6 +1,7 @@
1
1
  = RFC XML Ruby library
2
2
 
3
3
  The `rfcxml` library is a parser and generator for RFC XML v3 documents.
4
+
4
5
  It provides bidirectional XML serialization using the lutaml-model framework,
5
6
  enabling perfect round-trips with namespace preservation.
6
7
 
@@ -64,56 +65,153 @@ puts rfc.category # => "std"
64
65
  puts rfc.submission_type # => "IETF"
65
66
 
66
67
  # Access front matter
67
- puts rfc.front.title.to_s
68
+ puts rfc.front.title.content
68
69
  rfc.front.author.each do |author|
69
70
  puts author.fullname
70
71
  end
71
72
 
72
73
  # Access sections
73
74
  rfc.middle.section.each do |section|
74
- puts section.name
75
+ puts section.name&.content
75
76
  end
76
77
  ----
77
78
 
78
- === Generating RFC XML
79
+ === Creating an Internet-Draft
80
+
81
+ Internet-Drafts have a `doc_name` attribute but no `number`:
79
82
 
80
83
  [source,ruby]
81
84
  ----
82
85
  require 'rfcxml'
83
86
 
84
- # Create a new RFC document
85
- rfc = Rfcxml::V3::Rfc.new(
86
- number: "1234",
87
- category: "info",
88
- submission_type: "IETF"
87
+ draft = Rfcxml::V3::Rfc.new(
88
+ doc_name: "draft-ietf-example-protocol-01",
89
+ submission_type: "IETF",
90
+ ipr: "trust200902",
91
+ category: "std",
92
+ version: "3",
93
+
94
+ front: Rfcxml::V3::Front.new(
95
+ title: Rfcxml::V3::Title.new(
96
+ content: "Example Protocol for Testing",
97
+ abbrev: "Example Protocol"
98
+ ),
99
+ author: [
100
+ Rfcxml::V3::Author.new(
101
+ fullname: "Jane Doe",
102
+ initials: "J.",
103
+ surname: "Doe",
104
+ role: "editor"
105
+ )
106
+ ],
107
+ date: Rfcxml::V3::Date.new(year: "2025", month: "January")
108
+ ),
109
+
110
+ middle: Rfcxml::V3::Middle.new(
111
+ section: [
112
+ Rfcxml::V3::Section.new(
113
+ anchor: "introduction",
114
+ name: Rfcxml::V3::Name.new(content: "Introduction"),
115
+ t: [
116
+ Rfcxml::V3::Text.new(content: "This is an example Internet-Draft.")
117
+ ]
118
+ )
119
+ ]
120
+ )
89
121
  )
90
122
 
91
- # Build front matter
92
- rfc.front = Rfcxml::V3::Front.new(
93
- title: Rfcxml::V3::Title.new(content: "My RFC Title"),
94
- author: [
95
- Rfcxml::V3::Author.new(
96
- fullname: "John Doe",
97
- email: "john@example.com"
98
- )
99
- ]
100
- )
123
+ xml = draft.to_xml(pretty: true, declaration: true, encoding: "utf-8")
124
+ ----
125
+
126
+ === Creating a Published RFC
127
+
128
+ Published RFCs have a `number` attribute and may have `obsoletes`/`updates`:
129
+
130
+ [source,ruby]
131
+ ----
132
+ require 'rfcxml'
101
133
 
102
- # Build middle section
103
- rfc.middle = Rfcxml::V3::Middle.new(
104
- section: [
105
- Rfcxml::V3::Section.new(
106
- name: Rfcxml::V3::Name.new(content: "Introduction"),
107
- t: [
108
- Rfcxml::V3::T.new(content: "This is the introduction.")
109
- ]
110
- )
111
- ]
134
+ rfc = Rfcxml::V3::Rfc.new(
135
+ number: "9999",
136
+ category: "std",
137
+ obsoletes: "9998",
138
+ updates: "9997,9996",
139
+ consensus: "true",
140
+ ipr: "trust200902",
141
+ version: "3",
142
+
143
+ front: Rfcxml::V3::Front.new(
144
+ title: Rfcxml::V3::Title.new(
145
+ content: "A Standard Protocol for Example Purposes",
146
+ abbrev: "Example Standard"
147
+ ),
148
+ series_info: [
149
+ Rfcxml::V3::SeriesInfo.new(name: "RFC", value: "9999", stream: "IETF")
150
+ ],
151
+ author: [
152
+ Rfcxml::V3::Author.new(fullname: "John Smith", surname: "Smith")
153
+ ],
154
+ date: Rfcxml::V3::Date.new(year: "2025", month: "March")
155
+ ),
156
+
157
+ middle: Rfcxml::V3::Middle.new(
158
+ section: [
159
+ Rfcxml::V3::Section.new(
160
+ name: Rfcxml::V3::Name.new(content: "Overview"),
161
+ t: [
162
+ Rfcxml::V3::Text.new(content: "This document specifies a standard protocol.")
163
+ ]
164
+ )
165
+ ]
166
+ )
112
167
  )
113
168
 
114
- # Serialize to XML
115
169
  xml = rfc.to_xml(pretty: true, declaration: true, encoding: "utf-8")
116
- puts xml
170
+ ----
171
+
172
+ === Bibliography-Only Usage
173
+
174
+ If you only need to parse or generate bibliography references (the `<reference>` element),
175
+ you can use `Rfcxml::V3::Reference` directly:
176
+
177
+ [source,ruby]
178
+ ----
179
+ require 'rfcxml'
180
+
181
+ # Parse a reference from XML
182
+ ref_xml = <<~XML
183
+ <reference anchor="RFC2119">
184
+ <front>
185
+ <title>Key words for use in RFCs to Indicate Requirement Levels</title>
186
+ <author fullname="S. Bradner" surname="Bradner"/>
187
+ <date month="March" year="1997"/>
188
+ </front>
189
+ <seriesInfo name="RFC" value="2119"/>
190
+ </reference>
191
+ XML
192
+
193
+ ref = Rfcxml::V3::Reference.from_xml(ref_xml)
194
+ puts ref.anchor # => "RFC2119"
195
+ puts ref.front.title.content
196
+
197
+ # Create a reference programmatically
198
+ ref = Rfcxml::V3::Reference.new(
199
+ anchor: "RFC8650",
200
+ front: Rfcxml::V3::Front.new(
201
+ title: Rfcxml::V3::Title.new(
202
+ content: "Dynamic Subscription to YANG Events and Datastores over RESTCONF"
203
+ ),
204
+ author: [
205
+ Rfcxml::V3::Author.new(fullname: "E. Voit", surname: "Voit")
206
+ ],
207
+ date: Rfcxml::V3::Date.new(year: "2019", month: "September"),
208
+ series_info: [
209
+ Rfcxml::V3::SeriesInfo.new(name: "RFC", value: "8650")
210
+ ]
211
+ )
212
+ )
213
+
214
+ xml = ref.to_xml(pretty: true)
117
215
  ----
118
216
 
119
217
  === Round-trip preservation
@@ -152,14 +250,22 @@ The RFC XML v3 structure maps to these Ruby classes:
152
250
  | `Rfcxml::V3::Title` | `<title>`
153
251
  | `Rfcxml::V3::Author` | `<author>`
154
252
  | `Rfcxml::V3::Section` | `<section>`
155
- | `Rfcxml::V3::T` | `<t>`
156
- | `Rfcxml::V3::List` | `<ul>`, `<ol>`, `<dl>`
157
- | `Rfcxml::V3::Figure` | `<figure>`
158
- | `Rfcxml::V3::Table` | `<table>`
159
- | `Rfcxml::V3::References` | `<references>`
253
+ | `Rfcxml::V3::Text` | `<t>` (paragraph)
160
254
  | `Rfcxml::V3::Reference` | `<reference>`
255
+ | `Rfcxml::V3::References` | `<references>`
161
256
  |===
162
257
 
258
+ === Key Differences: Internet-Draft vs Published RFC
259
+
260
+ | Attribute | Internet-Draft | Published RFC |
261
+ |-----------|----------------|---------------|
262
+ | `doc_name` | Required (e.g., `draft-ietf-wg-topic-01`) | Optional |
263
+ | `number` | Not used | Required (e.g., `"8650"`) |
264
+ | `expires_date` | Optional | Not used |
265
+ | `obsoletes` | Not used | Optional |
266
+ | `updates` | Not used | Optional |
267
+ | `series_info` | Optional | Usually present |
268
+
163
269
  == Serialization Options
164
270
 
165
271
  The `to_xml` method supports these options:
@@ -185,6 +291,54 @@ xml = rfc.to_xml(
185
291
  )
186
292
  ----
187
293
 
294
+ == Enumeration Validation
295
+
296
+ The library provides built-in validation for all RFC XML v3 enumeration
297
+ attributes.
298
+
299
+ These are defined in the v3.rnc schema and implemented using lutaml-model's
300
+ `values:` constraint.
301
+
302
+ === Core Document Attributes
303
+
304
+ |===
305
+ | Attribute | Valid Values | Default
306
+ | `category` | `std`, `bcp`, `exp`, `info`, `historic` | -
307
+ | `consensus` | `no`, `yes`, `false`, `true` | `false`
308
+ | `submissionType` | `IETF`, `IAB`, `IRTF`, `independent`, `editorial` | `IETF`
309
+ | `sortRefs` | `true`, `false` | `false`
310
+ | `symRefs` | `true`, `false` | `true`
311
+ | `tocInclude` | `true`, `false` | `true`
312
+ | `indexInclude` | `true`, `false` | `true`
313
+ |===
314
+
315
+ === Section Attributes
316
+
317
+ |===
318
+ | Attribute | Valid Values | Default
319
+ | `numbered` | `true`, `false` | `true`
320
+ | `toc` | `include`, `exclude`, `default` | `default`
321
+ | `removeInRFC` | `true`, `false` | `false`
322
+ |===
323
+
324
+ === Author Attributes
325
+
326
+ |===
327
+ | Attribute | Valid Values | Default
328
+ | `role` | `editor` | -
329
+ |===
330
+
331
+ === Validation
332
+
333
+ Validation requires an explicit call to `validate` or `validate!`:
334
+
335
+ [source,ruby]
336
+ ----
337
+ rfc = Rfcxml::V3::Rfc.new(category: "invalid")
338
+ errors = rfc.validate # => array of validation errors
339
+ rfc.validate! # => raises Lutaml::Model::ValidationError
340
+ ----
341
+
188
342
  == Development
189
343
 
190
344
  After checking out the repo, run:
@@ -202,6 +356,72 @@ This runs tests + linting. To run tests only:
202
356
  $ bundle exec rake spec
203
357
  ----
204
358
 
359
+ === Round-Trip Testing
360
+
361
+ The library includes comprehensive round-trip tests that verify XML parsing and
362
+ serialization preserve all document structure. These tests use the
363
+ https://github.com/lutaml/canon[Canon] gem for semantic XML comparison.
364
+
365
+ ==== Downloading RFC XML Fixtures
366
+
367
+ To run round-trip tests, you need to download RFC XML fixtures from
368
+ rfc-editor.org:
369
+
370
+ [source]
371
+ ----
372
+ $ bundle exec rake rfc:download
373
+ ----
374
+
375
+ This downloads and extracts RFC XML files to `spec/fixtures/xmlsource/`. The
376
+ download is automatically excluded from version control via `.gitignore`.
377
+
378
+ To re-download fresh fixtures:
379
+
380
+ [source]
381
+ ----
382
+ $ bundle exec rake rfc:redownload
383
+ ----
384
+
385
+ To clean downloaded fixtures:
386
+
387
+ [source]
388
+ ----
389
+ $ bundle exec rake rfc:clean
390
+ ----
391
+
392
+ ==== Running Round-Trip Tests
393
+
394
+ Run round-trip tests for all downloaded RFC XML files:
395
+
396
+ [source]
397
+ ----
398
+ $ bundle exec ruby scripts/roundtrip_test.rb
399
+ ----
400
+
401
+ The test script will:
402
+
403
+ 1. Parse each RFC XML file using `Rfcxml::V3::Rfc.from_xml`
404
+ 2. Serialize back to XML using `to_xml`
405
+ 3. Compare original and serialized XML using Canon with `spec_friendly` profile
406
+ 4. Report pass/fail statistics
407
+
408
+ Run round-trip tests for specific files:
409
+
410
+ [source]
411
+ ----
412
+ $ bundle exec ruby scripts/roundtrip_test.rb spec/fixtures/xmlsource/rfc8650.xml
413
+ $ bundle exec ruby scripts/roundtrip_test.rb spec/fixtures/xmlsource/rfc8650.xml spec/fixtures/xmlsource/rfc8651.xml
414
+ ----
415
+
416
+ Run round-trip tests with custom thread count:
417
+
418
+ [source]
419
+ ----
420
+ $ bundle exec ruby scripts/roundtrip_test.rb -- --threads 4
421
+ ----
422
+
423
+ Output is saved to `tmp/roundtrip-results-{timestamp}/` with detailed failure reports.
424
+
205
425
  == Architecture
206
426
 
207
427
  The library uses lutaml-model for XML serialization:
data/Rakefile CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
+ require "open-uri"
6
+ require "rubygems/package"
7
+ require "zlib"
8
+ require "fileutils"
5
9
 
6
10
  RSpec::Core::RakeTask.new(:spec)
7
11
 
@@ -10,3 +14,54 @@ require "rubocop/rake_task"
10
14
  RuboCop::RakeTask.new
11
15
 
12
16
  task default: %i[spec rubocop]
17
+
18
+ RFC_XML_URL = "https://www.rfc-editor.org/in-notes/tar/xmlsource-all.tar.gz"
19
+ RFC_XML_DIR = File.expand_path("spec/fixtures/xmlsource", __dir__)
20
+ RFC_XML_TAR = File.expand_path("tmp/xmlsource-all.tar.gz", __dir__)
21
+
22
+ namespace :rfc do
23
+ desc "Download and extract RFC XML files from rfc-editor.org"
24
+ task :download do
25
+ FileUtils.mkdir_p(RFC_XML_DIR)
26
+ FileUtils.mkdir_p(File.dirname(RFC_XML_TAR))
27
+
28
+ # Download tarball if not exists
29
+ unless File.exist?(RFC_XML_TAR)
30
+ puts "Downloading RFC XML tarball from #{RFC_XML_URL}..."
31
+ URI.open(RFC_XML_URL) do |remote|
32
+ File.binwrite(RFC_XML_TAR, remote.read)
33
+ end
34
+ puts "Downloaded to #{RFC_XML_TAR}"
35
+ end
36
+
37
+ # Extract tarball
38
+ puts "Extracting to #{RFC_XML_DIR}..."
39
+ tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open(RFC_XML_TAR))
40
+ tar_extract.rewind
41
+ tar_extract.each do |entry|
42
+ next unless entry.file? && entry.full_name.end_with?(".xml")
43
+
44
+ # Extract only the filename, flatten directory structure
45
+ filename = File.basename(entry.full_name)
46
+ target_path = File.join(RFC_XML_DIR, filename)
47
+
48
+ File.binwrite(target_path, entry.read)
49
+ end
50
+ tar_extract.close
51
+
52
+ xml_count = Dir.glob(File.join(RFC_XML_DIR, "*.xml")).count
53
+ puts "Done! Extracted #{xml_count} XML files to #{RFC_XML_DIR}"
54
+ end
55
+
56
+ desc "Clean downloaded RFC XML files"
57
+ task :clean do
58
+ puts "Removing #{RFC_XML_DIR}..."
59
+ FileUtils.rm_rf(RFC_XML_DIR)
60
+ puts "Removing #{RFC_XML_TAR}..."
61
+ FileUtils.rm_f(RFC_XML_TAR)
62
+ puts "Done!"
63
+ end
64
+
65
+ desc "Re-download RFC XML files (clean + download)"
66
+ task redownload: %i[clean download]
67
+ end
@@ -13,7 +13,8 @@ module Rfcxml
13
13
  attribute :ul, Ul, collection: true
14
14
 
15
15
  xml do
16
- root "abstract"
16
+ element "abstract"
17
+ ordered
17
18
 
18
19
  map_attribute "anchor", to: :anchor
19
20
  map_attribute "pn", to: :pn