relaton-ietf 2.1.1 → 2.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca0c823ba49c0bf4b1f184a0172e38e525312171f55b1ee17a4bf611926d0477
4
- data.tar.gz: bc80ad922e473bf98c412a8dddd35b150093b0b16f18bb7c47f00c7c28983321
3
+ metadata.gz: 316a5b5786dc1730e23428a30445debf9cd52a11d32dd34511d992bbeceafec5
4
+ data.tar.gz: 0f94341cfcc4a18a1dc14a831c482564166d145342a093dbf8f5a65f43c6e334
5
5
  SHA512:
6
- metadata.gz: 7394ae875b66647a0d24d6509f266a6396343a5083fb6737c4fb4212fc8fc892aeb55a87faedccdf2b6711641e14f4619dfa8bf05ccae94fd4d39a1ad0132d2b
7
- data.tar.gz: 00a969e3ccfd8296de2417541c2a887e23767798645f8f1ea07321523ab626146e452759cbd6774c5e1a8aea188e3f2c8408f0fc119adb13c626a2f6ab9e5d04
6
+ metadata.gz: 9072422ff3c903d155f89f95160636d83352f88379513fb1c17a2dec5acdd983dbbaf3f2dfe4a8d07180856fdee87a8dcdd56c2f3d8a8497de217233c6af248b
7
+ data.tar.gz: 9a8d31c7a865112f409bd1ab38a7390a8530b21090b98d104cf82df0b60748ae127f599b21898aca95c1ec7a56231ae1348e0ae9661159f4cba2ace6a1a61ab8
@@ -1,3 +1,5 @@
1
+ require "etc"
2
+ require "parallel"
1
3
  require "relaton/core"
2
4
  require_relative "../ietf"
3
5
  require_relative "bibxml_parser"
@@ -41,107 +43,158 @@ module Relaton
41
43
  end
42
44
 
43
45
  #
44
- # Fetches ietf-internet-drafts documents
46
+ # Fetches ietf-internet-drafts documents.
45
47
  #
46
- def fetch_ieft_internet_drafts # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
47
- versions = Dir["bibxml-ids/*.xml"].each_with_object([]) do |path, vers|
48
- file = File.basename path, ".xml"
49
- draft = file.include?("D.draft-")
50
- /(?<ver>\d+)$/ =~ file if draft
51
- bib = BibXMLParser.parse(File.read(path, encoding: "UTF-8"))
52
- if ver
53
- version = Bib::Version.new(draft: ver)
54
- bib.version = [version]
55
- end
56
- if draft
57
- vers << { ref: file.sub(/^reference\.I-D\./, "").downcase, source: bib.source }
48
+ # Each work unit (one series, or one singleton XML) is processed
49
+ # end-to-end in a worker process: parse → link relations → serialize →
50
+ # write. Workers return Marshal-friendly index entries; the parent
51
+ # collects them and updates `Relaton::Index` and the duplicate-check set
52
+ # serially. Set `RELATON_IETF_PARALLEL_WORKERS=0` to force serial
53
+ # execution (useful for tests and debugging).
54
+ #
55
+ def fetch_ieft_internet_drafts
56
+ series_groups, singleton_paths = group_draft_paths
57
+
58
+ series_results = parallelize(series_groups.to_a) do |(series, paths_info)|
59
+ process_series(series, paths_info)
60
+ end.flatten(1)
61
+
62
+ singleton_results = parallelize(singleton_paths) do |path|
63
+ process_singleton(path)
64
+ end
65
+
66
+ (series_results + singleton_results).compact.each { |r| record_index_entry(r) }
67
+ end
68
+
69
+ #
70
+ # Run `block` once per item, in parallel worker processes when configured.
71
+ # `Parallel.map(items, in_processes: 0)` runs synchronously in the
72
+ # current process, which keeps tests deterministic and lets mocks work.
73
+ #
74
+ def parallelize(items, &block)
75
+ Parallel.map(items, in_processes: worker_count, &block)
76
+ end
77
+
78
+ def worker_count
79
+ ENV.fetch("RELATON_IETF_PARALLEL_WORKERS", Etc.nprocessors.to_s).to_i
80
+ end
81
+
82
+ #
83
+ # Filename-only scan: group versioned drafts by normalized series stem;
84
+ # everything else (non-versioned, non-`D.draft-`) goes to singletons.
85
+ # No XML parsing happens here — workers do that.
86
+ #
87
+ # @return [Array(Hash, Array<String>)]
88
+ # series_groups: { normalized_series => [{path, ver, ref}, ...] }
89
+ # singleton_paths: [path, ...]
90
+ #
91
+ def group_draft_paths
92
+ series_groups = {}
93
+ singleton_paths = []
94
+ Dir["bibxml-ids/*.xml"].each do |path|
95
+ file = File.basename(path, ".xml")
96
+ is_draft = file.include?("D.draft-")
97
+ ver = is_draft ? file[/(\d+)$/, 1] : nil
98
+ ref = file.sub(/^reference\.I-D\./, "").downcase
99
+ stem_match = is_draft && ver ? /^(draft-.+)-(\d{2})$/.match(ref) : nil
100
+ if stem_match
101
+ series = stem_match[1].gsub(/[.\s\/:-]+/, "-")
102
+ (series_groups[series] ||= []) << { path: path, ver: ver, ref: ref }
103
+ else
104
+ singleton_paths << path
58
105
  end
59
- save_doc bib
60
106
  end
61
- update_versions(versions) if versions.any? && @format != "bibxml"
62
- end
63
-
64
- #
65
- # Updates I-D's versions
66
- #
67
- # @param [Array<String>] versions list of versions
68
- #
69
- def update_versions(versions) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
70
- series = ""
71
- bib_versions = []
72
- Dir["#{@output}/*.#{@ext}"].each do |file|
73
- match = /(?<series>draft-.+)-(?<ver>\d{2})\.#{@ext}$/.match file
74
- if match
75
- if series != match[:series]
76
- bib_versions = versions.select { |v| v[:ref].downcase.gsub(/[.\s\/:-]+/, "-").match?(/^#{Regexp.quote match[:series]}-\d{2}/) }
77
- create_series match[:series], bib_versions
78
- series = match[:series]
79
- end
80
- lv = bib_versions.select { |v| v[:ref].match(/\d+$/).to_s.to_i < match[:ver].to_i }
81
- hv = bib_versions.select { |v| v[:ref].match(/\d+$/).to_s.to_i > match[:ver].to_i }
82
- if lv.any? || hv.any?
83
- bib = read_doc(file)
84
- bib.relation << version_relation(lv.last, "updates") if lv.any?
85
- bib.relation << version_relation(hv.first, "updatedBy") if hv.any?
86
- save_doc bib, check_duplicate: false
87
- end
107
+ [series_groups, singleton_paths]
108
+ end
109
+
110
+ #
111
+ # Worker: parse all files in a series, sort by version, append
112
+ # immediate-neighbor relations (skipped for bibxml), write each version
113
+ # and the un-versioned aggregator doc. Returns an array of index entries
114
+ # for the parent.
115
+ #
116
+ def process_series(series, paths_info)
117
+ sorted = paths_info.sort_by { |p| p[:ver].to_i }.filter_map do |p|
118
+ bib = BibXMLParser.parse(File.read(p[:path], encoding: "UTF-8"))
119
+ bib.version = [Bib::Version.new(draft: p[:ver])]
120
+ p.merge(bib: bib, source: bib.source)
121
+ rescue StandardError => e
122
+ Util.error "Error parsing #{p[:path]}: #{e.message}\n#{e.backtrace[0..5].join("\n")}"
123
+ nil
124
+ end
125
+ link_neighbor_relations(sorted) if @format != "bibxml"
126
+
127
+ results = sorted.map { |entry| serialize_and_write(entry[:bib]) }
128
+ results << serialize_and_write(build_unversioned_doc(series, sorted)) if @format != "bibxml"
129
+ results.compact
130
+ end
131
+
132
+ #
133
+ # Worker: parse + serialize + write a single non-grouped XML.
134
+ #
135
+ def process_singleton(path)
136
+ file = File.basename(path, ".xml")
137
+ is_draft = file.include?("D.draft-")
138
+ ver = is_draft ? file[/(\d+)$/, 1] : nil
139
+ bib = BibXMLParser.parse(File.read(path, encoding: "UTF-8"))
140
+ bib.version = [Bib::Version.new(draft: ver)] if ver
141
+ serialize_and_write(bib)
142
+ rescue StandardError => e
143
+ Util.error "Error parsing #{path}: #{e.message}\n#{e.backtrace[0..5].join("\n")}"
144
+ nil
145
+ end
146
+
147
+ #
148
+ # Append immediate-neighbor `updates` / `updatedBy` relations in memory.
149
+ # Single-version series get no relations (no neighbors).
150
+ #
151
+ def link_neighbor_relations(sorted)
152
+ sorted.each_with_index do |entry, i|
153
+ if i.positive?
154
+ prev = sorted[i - 1]
155
+ entry[:bib].relation << version_relation({ ref: prev[:ref], source: prev[:source] }, "updates")
156
+ end
157
+ if i < sorted.size - 1
158
+ nxt = sorted[i + 1]
159
+ entry[:bib].relation << version_relation({ ref: nxt[:ref], source: nxt[:source] }, "updatedBy")
88
160
  end
89
161
  end
90
162
  end
91
163
 
92
164
  #
93
- # Create unversioned bibliographic item
165
+ # Build (but do not write) the un-versioned series aggregator doc with
166
+ # `includes` relations to every version. Uses the latest version's
167
+ # title/abstract from memory.
94
168
  #
95
- # @param [String] ref reference
96
- # @param [Array<String>] versions list of versions
169
+ # @return [Relaton::Ietf::ItemData, nil]
97
170
  #
98
- def create_series(ref, versions) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
99
- vs = versions.sort_by { |v| v[:ref].match(/\d+$/).to_s.to_i }
100
- if vs.empty?
101
- Util.warn "No versions found for #{ref}"
102
- return
171
+ def build_unversioned_doc(series, sorted)
172
+ if sorted.empty?
173
+ Util.warn "No versions found for #{series}"
174
+ return nil
103
175
  end
104
- file = output_file(vs.last[:ref])
105
- # return unless File.exist?(file)
106
-
107
- docid = Bib::Docidentifier.new(type: "Internet-Draft", content: ref, primary: true)
108
- rel = vs.map { |v| version_relation v, "includes" }
109
- last_v = Item.from_yaml(File.read(file, encoding: "UTF-8"))
110
- bib = ItemData.new(
111
- title: last_v.title, abstract: last_v.abstract, formattedref: Bib::Formattedref.new(content: ref),
176
+
177
+ last_v = sorted.last[:bib]
178
+ docid = Bib::Docidentifier.new(type: "Internet-Draft", content: series, primary: true)
179
+ rel = sorted.map { |e| version_relation({ ref: e[:ref], source: e[:source] }, "includes") }
180
+ ItemData.new(
181
+ title: last_v.title, abstract: last_v.abstract, formattedref: Bib::Formattedref.new(content: series),
112
182
  docidentifier: [docid], relation: rel
113
183
  )
114
- save_doc bib
115
184
  end
116
185
 
117
186
  #
118
187
  # Create bibitem relation
119
188
  #
120
- # @param [String] ref reference
189
+ # @param [Hash] ver version reference, { ref:, source: }
121
190
  # @param [String] type relation type
122
191
  #
123
- # @return [Relaton::Bib::Relation] relation
192
+ # @return [Relaton::Ietf::Relation] relation
124
193
  #
125
194
  def version_relation(ver, type)
126
195
  docid = Bib::Docidentifier.new(type: "Internet-Draft", content: ver[:ref], primary: true)
127
196
  bibitem = ItemData.new(formattedref: Bib::Formattedref.new(content: ver[:ref]), docidentifier: [docid], source: ver[:source])
128
- Bib::Relation.new(type: type, bibitem: bibitem)
129
- end
130
-
131
- #
132
- # Redad saved documents
133
- #
134
- # @param [String] file path to file
135
- #
136
- # @return [Relaton::Ietf::ItemData] bibliographic item
137
- #
138
- def read_doc(file)
139
- doc = File.read(file, encoding: "UTF-8")
140
- case @format
141
- when "xml" then Item.from_xml(doc)
142
- when "yaml" then Item.from_yaml(doc)
143
- else BibXMLParser.parse(doc)
144
- end
197
+ Relaton::Ietf::Relation.new(type: type, bibitem: bibitem)
145
198
  end
146
199
 
147
200
  #
@@ -172,38 +225,58 @@ module Relaton
172
225
  end
173
226
 
174
227
  #
175
- # Save document to file
228
+ # Save document to file (sequential path: serialize, write, index).
229
+ # Used by the rfcsubseries / rfc-entries fetchers; the I-D fetcher splits
230
+ # this into worker-safe `serialize_and_write` plus parent-only
231
+ # `record_index_entry` so the index is touched only in the main process.
176
232
  #
177
- # @param [Relaton::Ietf::Rfc::Entry, nil] rfc index entry
233
+ # @param [Relaton::Ietf::Rfc::Entry, nil] entry
178
234
  # @param [Boolean] check_duplicate check for duplicate
179
235
  #
180
- def save_doc(entry, check_duplicate: true) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
181
- return unless entry
236
+ def save_doc(entry, check_duplicate: true)
237
+ result = serialize_and_write(entry)
238
+ record_index_entry(result, check_duplicate: check_duplicate) if result
239
+ end
240
+
241
+ #
242
+ # Worker-safe: serialize, compute output filename, write to disk, return
243
+ # a Marshal-friendly hash with the docid+file pair the parent needs to
244
+ # update `Relaton::Index` and `@files`. Does NOT touch instance state
245
+ # that has to stay consistent across workers (`@files`, the index).
246
+ #
247
+ # @param [#to_yaml, #to_xml, #to_rfcxml, nil] entry
248
+ # @return [Hash, nil]
249
+ #
250
+ def serialize_and_write(entry) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
251
+ return nil unless entry
182
252
 
183
- c = case @format
184
- when "xml" then entry.to_xml(bibdata: true)
185
- when "yaml" then entry.to_yaml
186
- when "bibxml" then entry.to_rfcxml
187
- else entry.send("to_#{@format}")
188
- end
253
+ content = case @format
254
+ when "xml" then entry.to_xml(bibdata: true)
255
+ when "yaml" then entry.to_yaml
256
+ when "bibxml" then entry.to_rfcxml
257
+ else entry.send("to_#{@format}")
258
+ end
189
259
  id = if entry.respond_to?(:docidentifier)
190
260
  entry.docidentifier.detect { |i| i.type == "Internet-Draft" && i.primary }&.content
191
261
  end
192
262
  id ||= entry.docnumber || entry.formattedref.content
193
263
  file = output_file(id)
194
- if check_duplicate && @files.include?(file)
195
- Util.warn "File #{file} already exists. Document: #{entry.docnumber}"
196
- elsif check_duplicate
197
- @files << file
198
- end
199
- File.write file, c, encoding: "UTF-8"
200
- add_to_index entry, file
264
+ File.write file, content, encoding: "UTF-8"
265
+ primary = entry.docidentifier.detect(&:primary) || entry.docidentifier.first
266
+ { docnumber: entry.docnumber, file: file, index_id: primary.content }
201
267
  end
202
268
 
203
- def add_to_index(entry, file)
204
- docid = entry.docidentifier.detect(&:primary)
205
- docid ||= entry.docidentifier.first
206
- index.add_or_update docid.content, file
269
+ #
270
+ # Parent-only: dedupe-check `@files` and update `Relaton::Index`. Called
271
+ # serially after workers return so index updates are race-free.
272
+ #
273
+ def record_index_entry(result, check_duplicate: true)
274
+ if check_duplicate && @files.include?(result[:file])
275
+ Util.warn "File #{result[:file]} already exists. Document: #{result[:docnumber]}"
276
+ elsif check_duplicate
277
+ @files << result[:file]
278
+ end
279
+ index.add_or_update result[:index_id], result[:file]
207
280
  end
208
281
 
209
282
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi"
3
4
  require_relative "rfc_index_namespace"
4
5
  require_relative "is_also"
5
6
  require_relative "author"
@@ -212,7 +213,7 @@ module Relaton
212
213
  is_also.doc_id.map do |ref|
213
214
  rfc_entry = rfc_index&.[](ref)
214
215
  bibitem = rfc_entry ? rfc_entry.to_rfc_item(wg_names: wg_names) : build_minimal_bibitem(ref)
215
- Bib::Relation.new(type: "includes", bibitem: bibitem)
216
+ Relaton::Ietf::Relation.new(type: "includes", bibitem: bibitem)
216
217
  end.compact
217
218
  end
218
219
 
@@ -317,7 +318,7 @@ module Relaton
317
318
  def build_rfc_abstract
318
319
  return [] unless abstract&.p&.any?
319
320
 
320
- content = abstract.p.map { |para| "<p>#{para.strip}</p>" }.join
321
+ content = abstract.p.map { |para| "<p>#{CGI.escapeHTML(para.strip)}</p>" }.join
321
322
  [Bib::Abstract.new(content: content, language: "en", script: "Latn")]
322
323
  end
323
324
 
@@ -339,7 +340,7 @@ module Relaton
339
340
  def build_rfc_doc_relation(ref, type)
340
341
  docid = Bib::Docidentifier.new(type: "IETF", content: ref, primary: true)
341
342
  bibitem = ItemData.new(formattedref: Bib::Formattedref.new(content: ref), docidentifier: [docid])
342
- Bib::Relation.new(type: type, bibitem: bibitem)
343
+ Relaton::Ietf::Relation.new(type: type, bibitem: bibitem)
343
344
  end
344
345
 
345
346
  def build_rfc_status
@@ -1,5 +1,5 @@
1
1
  module Relaton
2
2
  module Ietf
3
- VERSION = "2.1.1".freeze
3
+ VERSION = "2.1.3".freeze
4
4
  end
5
5
  end
data/relaton-ietf.gemspec CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
 
19
19
  Formerly known as rfcbib.
20
20
  DESCRIPTION
21
- spec.homepage = "https://github.com/metanorma/relaton-ietf"
21
+ spec.homepage = "https://github.com/relaton/relaton-ietf"
22
22
  spec.license = "BSD-2-Clause"
23
23
 
24
24
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
31
31
 
32
32
  spec.add_dependency "base64"
33
+ spec.add_dependency "parallel", "~> 1.26"
33
34
  spec.add_dependency "relaton-bib", "~> 2.1.0"
34
35
  spec.add_dependency "relaton-core", "~> 0.0.13"
35
36
  spec.add_dependency "relaton-index", "~> 0.2.3"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaton-ietf
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-06 00:00:00.000000000 Z
11
+ date: 2026-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: parallel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.26'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.26'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: relaton-bib
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -91,11 +105,6 @@ files:
91
105
  - bin/console
92
106
  - bin/rspec
93
107
  - bin/setup
94
- - grammars/basicdoc.rng
95
- - grammars/biblio-standoc.rng
96
- - grammars/biblio.rng
97
- - grammars/relaton-ietf-compile.rng
98
- - grammars/relaton-ietf.rng
99
108
  - lib/relaton/ietf.rb
100
109
  - lib/relaton/ietf/bibdata.rb
101
110
  - lib/relaton/ietf/bibitem.rb
@@ -124,7 +133,7 @@ files:
124
133
  - lib/relaton/ietf/version.rb
125
134
  - lib/relaton/ietf/wg_name_resolver.rb
126
135
  - relaton-ietf.gemspec
127
- homepage: https://github.com/metanorma/relaton-ietf
136
+ homepage: https://github.com/relaton/relaton-ietf
128
137
  licenses:
129
138
  - BSD-2-Clause
130
139
  metadata: {}