logstash-filter-translate 3.2.2 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,33 @@
1
1
  # encoding: utf-8
2
2
  require "logstash/devutils/rspec/spec_helper"
3
+ require 'logstash/plugin_mixins/ecs_compatibility_support/spec_helper'
3
4
  require "logstash/filters/translate"
4
5
 
6
+ module TranslateUtil
7
+ def self.build_fixture_path(filename)
8
+ File.join(File.dirname(__FILE__), "..", "fixtures", filename)
9
+ end
10
+ end
11
+
5
12
  describe LogStash::Filters::Translate do
6
13
 
7
14
  let(:config) { Hash.new }
8
15
  subject { described_class.new(config) }
9
16
 
17
+ let(:logger) { double('Logger').as_null_object }
18
+ let(:deprecation_logger) { double('DeprecationLogger').as_null_object }
19
+
20
+ before(:each) do
21
+ allow_any_instance_of(described_class).to receive(:logger).and_return(logger)
22
+ allow_any_instance_of(described_class).to receive(:deprecation_logger).and_return(deprecation_logger)
23
+ end
24
+
10
25
  describe "exact translation" do
11
26
 
12
27
  let(:config) do
13
28
  {
14
- "field" => "status",
15
- "destination" => "translation",
29
+ "source" => "status",
30
+ "target" => "translation",
16
31
  "dictionary" => [ "200", "OK",
17
32
  "300", "Redirect",
18
33
  "400", "Client Error",
@@ -24,20 +39,43 @@ describe LogStash::Filters::Translate do
24
39
 
25
40
  let(:event) { LogStash::Event.new("status" => 200) }
26
41
 
27
- it "return the exact translation" do
42
+ it "coerces field to a string then returns the exact translation" do
28
43
  subject.register
29
44
  subject.filter(event)
30
45
  expect(event.get("translation")).to eq("OK")
31
46
  end
32
47
  end
33
48
 
49
+ describe "translation fails when regex setting is false but keys are regex based" do
50
+
51
+ let(:config) do
52
+ {
53
+ "source" => "status",
54
+ "target" => "translation",
55
+ "dictionary" => [ "^2\\d\\d", "OK",
56
+ "^3\\d\\d", "Redirect",
57
+ "^4\\d\\d", "Client Error",
58
+ "^5\\d\\d", "Server Error" ],
59
+ "exact" => true,
60
+ "regex" => false
61
+ }
62
+ end
63
+
64
+ let(:event) { LogStash::Event.new("status" => 200) }
65
+
66
+ it "does not return the exact translation" do
67
+ subject.register
68
+ subject.filter(event)
69
+ expect(event.get("translation")).to be_nil
70
+ end
71
+ end
34
72
 
35
73
  describe "multi translation" do
36
74
  context "when using an inline dictionary" do
37
75
  let(:config) do
38
76
  {
39
- "field" => "status",
40
- "destination" => "translation",
77
+ "source" => "status",
78
+ "target" => "translation",
41
79
  "dictionary" => [ "200", "OK",
42
80
  "300", "Redirect",
43
81
  "400", "Client Error",
@@ -57,11 +95,11 @@ describe LogStash::Filters::Translate do
57
95
  end
58
96
 
59
97
  context "when using a file based dictionary" do
60
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "regex_union_dict.csv") }
98
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("regex_union_dict.csv") }
61
99
  let(:config) do
62
100
  {
63
- "field" => "status",
64
- "destination" => "translation",
101
+ "source" => "status",
102
+ "target" => "translation",
65
103
  "dictionary_path" => dictionary_path,
66
104
  "refresh_interval" => 0,
67
105
  "exact" => false,
@@ -83,8 +121,8 @@ describe LogStash::Filters::Translate do
83
121
  context "when using an inline dictionary" do
84
122
  let(:config) do
85
123
  {
86
- "field" => "status",
87
- "destination" => "translation",
124
+ "source" => "status",
125
+ "target" => "translation",
88
126
  "dictionary" => [ "^2[0-9][0-9]$", "OK",
89
127
  "^3[0-9][0-9]$", "Redirect",
90
128
  "^4[0-9][0-9]$", "Client Error",
@@ -104,11 +142,11 @@ describe LogStash::Filters::Translate do
104
142
  end
105
143
 
106
144
  context "when using a file based dictionary" do
107
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "regex_dict.csv") }
145
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("regex_dict.csv") }
108
146
  let(:config) do
109
147
  {
110
- "field" => "status",
111
- "destination" => "translation",
148
+ "source" => "status",
149
+ "target" => "translation",
112
150
  "dictionary_path" => dictionary_path,
113
151
  "refresh_interval" => 0,
114
152
  "exact" => true,
@@ -126,53 +164,59 @@ describe LogStash::Filters::Translate do
126
164
  end
127
165
  end
128
166
 
129
- describe "fallback value" do
130
-
131
- context "static configuration" do
132
- let(:config) do
133
- {
134
- "field" => "status",
135
- "destination" => "translation",
136
- "fallback" => "no match"
137
- }
167
+ describe "fallback value", :ecs_compatibility_support do
168
+ ecs_compatibility_matrix(:disabled, :v1) do
169
+ before(:each) do
170
+ allow_any_instance_of(described_class).to receive(:ecs_compatibility).and_return(ecs_compatibility)
138
171
  end
139
172
 
140
- let(:event) { LogStash::Event.new("status" => "200") }
141
-
142
- it "return the exact translation" do
143
- subject.register
144
- subject.filter(event)
145
- expect(event.get("translation")).to eq("no match")
173
+ context "static configuration" do
174
+ let(:config) do
175
+ {
176
+ "source" => "status",
177
+ "target" => "translation",
178
+ "fallback" => "no match"
179
+ }
180
+ end
181
+
182
+ let(:event) { LogStash::Event.new("status" => "200") }
183
+
184
+ it "return the exact translation" do
185
+ subject.register
186
+ subject.filter(event)
187
+ expect(event.get("translation")).to eq("no match")
188
+ end
146
189
  end
147
- end
148
190
 
149
- context "allow sprintf" do
150
- let(:config) do
151
- {
152
- "field" => "status",
153
- "destination" => "translation",
154
- "fallback" => "%{missing_translation}"
155
- }
191
+ context "allow sprintf" do
192
+ let(:config) do
193
+ {
194
+ "source" => "status",
195
+ "target" => "translation",
196
+ "fallback" => "%{missing_translation}"
197
+ }
198
+ end
199
+
200
+ let(:event) { LogStash::Event.new("status" => "200", "missing_translation" => "missing no match") }
201
+
202
+ it "return the exact translation" do
203
+ subject.register
204
+ subject.filter(event)
205
+ expect(event.get("translation")).to eq("missing no match")
206
+ end
156
207
  end
157
208
 
158
- let(:event) { LogStash::Event.new("status" => "200", "missing_translation" => "missing no match") }
159
-
160
- it "return the exact translation" do
161
- subject.register
162
- subject.filter(event)
163
- expect(event.get("translation")).to eq("missing no match")
164
- end
165
209
  end
166
210
  end
167
211
 
168
212
  describe "loading a dictionary" do
169
213
 
170
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict-wrong.yml") }
214
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict-wrong.yml") }
171
215
 
172
216
  let(:config) do
173
217
  {
174
- "field" => "status",
175
- "destination" => "translation",
218
+ "source" => "status",
219
+ "target" => "translation",
176
220
  "dictionary_path" => dictionary_path,
177
221
  "refresh_interval" => -1,
178
222
  "exact" => true,
@@ -186,7 +230,7 @@ describe LogStash::Filters::Translate do
186
230
  end
187
231
 
188
232
  context "when using a yml file" do
189
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict.yml") }
233
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.yml") }
190
234
  let(:event) { LogStash::Event.new("status" => "a") }
191
235
 
192
236
  it "return the exact translation" do
@@ -197,7 +241,7 @@ describe LogStash::Filters::Translate do
197
241
  end
198
242
 
199
243
  context "when using a map tagged yml file" do
200
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "tag-map-dict.yml") }
244
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("tag-map-dict.yml") }
201
245
  let(:event) { LogStash::Event.new("status" => "six") }
202
246
 
203
247
  it "return the exact translation" do
@@ -208,7 +252,7 @@ describe LogStash::Filters::Translate do
208
252
  end
209
253
 
210
254
  context "when using a omap tagged yml file" do
211
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "tag-omap-dict.yml") }
255
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("tag-omap-dict.yml") }
212
256
  let(:event) { LogStash::Event.new("status" => "nine") }
213
257
 
214
258
  it "return the exact translation" do
@@ -219,7 +263,7 @@ describe LogStash::Filters::Translate do
219
263
  end
220
264
 
221
265
  context "when using a json file" do
222
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict.json") }
266
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.json") }
223
267
  let(:event) { LogStash::Event.new("status" => "b") }
224
268
 
225
269
  it "return the exact translation" do
@@ -230,7 +274,7 @@ describe LogStash::Filters::Translate do
230
274
  end
231
275
 
232
276
  context "when using a csv file" do
233
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict.csv") }
277
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.csv") }
234
278
  let(:event) { LogStash::Event.new("status" => "c") }
235
279
 
236
280
  it "return the exact translation" do
@@ -241,7 +285,7 @@ describe LogStash::Filters::Translate do
241
285
  end
242
286
 
243
287
  context "when using an unknown file" do
244
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict.other") }
288
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.other") }
245
289
 
246
290
  it "raises error" do
247
291
  expect { subject.register }.to raise_error(RuntimeError, /Dictionary #{dictionary_path} has a non valid format/)
@@ -250,42 +294,42 @@ describe LogStash::Filters::Translate do
250
294
  end
251
295
 
252
296
  describe "iterate_on functionality" do
253
- describe "when iterate_on is the same as field, AKA array of values" do
254
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "tag-map-dict.yml") }
255
- let(:config) do
256
- {
257
- "iterate_on" => "foo",
258
- "field" => "foo",
259
- "destination" => "baz",
260
- "fallback" => "nooo",
261
- "dictionary_path" => dictionary_path,
262
- # "override" => true,
263
- "refresh_interval" => 0
264
- }
265
- end
297
+ let(:config) do
298
+ {
299
+ "iterate_on" => "foo",
300
+ "source" => iterate_on_field,
301
+ "target" => "baz",
302
+ "fallback" => "nooo",
303
+ "dictionary_path" => dictionary_path,
304
+ # "override" => true,
305
+ "refresh_interval" => 0
306
+ }
307
+ end
308
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("tag-map-dict.yml") }
266
309
 
310
+ describe "when iterate_on is the same as field, AKA array of values" do
311
+ let(:iterate_on_field) { "foo" }
267
312
  let(:event) { LogStash::Event.new("foo" => ["nine","eight", "seven"]) }
268
- it "adds a translation to destination array for each value in field array" do
313
+ it "adds a translation to target array for each value in field array" do
269
314
  subject.register
270
315
  subject.filter(event)
271
316
  expect(event.get("baz")).to eq(["val-9-1|val-9-2", "val-8-1|val-8-2", "val-7-1|val-7-2"])
272
317
  end
273
318
  end
274
319
 
275
- describe "when iterate_on is not the same as field, AKA array of objects" do
276
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "tag-map-dict.yml") }
277
- let(:config) do
278
- {
279
- "iterate_on" => "foo",
280
- "field" => "bar",
281
- "destination" => "baz",
282
- "fallback" => "nooo",
283
- "dictionary_path" => dictionary_path,
284
- # "override" => true,
285
- "refresh_interval" => 0
286
- }
320
+ describe "when iterate_on is the same as field, AKA array of values, coerces integer elements to strings" do
321
+ let(:iterate_on_field) { "foo" }
322
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("regex_union_dict.csv") }
323
+ let(:event) { LogStash::Event.new("foo" => [200, 300, 400]) }
324
+ it "adds a translation to target array for each value in field array" do
325
+ subject.register
326
+ subject.filter(event)
327
+ expect(event.get("baz")).to eq(["OK","Redirect","Client Error"])
287
328
  end
329
+ end
288
330
 
331
+ describe "when iterate_on is not the same as field, AKA array of objects" do
332
+ let(:iterate_on_field) { "bar" }
289
333
  let(:event) { LogStash::Event.new("foo" => [{"bar"=>"two"},{"bar"=>"one"}, {"bar"=>"six"}]) }
290
334
  it "adds a translation to each map" do
291
335
  subject.register
@@ -295,17 +339,31 @@ describe LogStash::Filters::Translate do
295
339
  expect(event.get("[foo][2][baz]")).to eq("val-6-1|val-6-2")
296
340
  end
297
341
  end
342
+
343
+ describe "when iterate_on is not the same as field, AKA array of objects, coerces integer values to strings" do
344
+ let(:iterate_on_field) { "bar" }
345
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("regex_union_dict.csv") }
346
+ let(:event) { LogStash::Event.new("foo" => [{"bar"=>200},{"bar"=>300}, {"bar"=>400}]) }
347
+ it "adds a translation to each map" do
348
+ subject.register
349
+ subject.filter(event)
350
+ expect(event.get("[foo][0][baz]")).to eq("OK")
351
+ expect(event.get("[foo][1][baz]")).to eq("Redirect")
352
+ expect(event.get("[foo][2][baz]")).to eq("Client Error")
353
+ end
354
+ end
298
355
  end
299
356
 
300
- describe "field and destination are the same (needs override)" do
301
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "tag-map-dict.yml") }
357
+ describe "field and destination are the same (explicit override)" do
358
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("tag-map-dict.yml") }
302
359
  let(:config) do
303
360
  {
304
361
  "field" => "foo",
305
362
  "destination" => "foo",
306
363
  "dictionary_path" => dictionary_path,
307
364
  "override" => true,
308
- "refresh_interval" => -1
365
+ "refresh_interval" => -1,
366
+ "ecs_compatibility" => 'disabled'
309
367
  }
310
368
  end
311
369
 
@@ -318,11 +376,11 @@ describe LogStash::Filters::Translate do
318
376
  end
319
377
  end
320
378
 
321
- describe "general configuration" do
322
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict.yml") }
379
+ context "invalid dictionary configuration" do
380
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.yml") }
323
381
  let(:config) do
324
382
  {
325
- "field" => "random field",
383
+ "source" => "random field",
326
384
  "dictionary" => { "a" => "b" },
327
385
  "dictionary_path" => dictionary_path,
328
386
  }
@@ -333,6 +391,77 @@ describe LogStash::Filters::Translate do
333
391
  end
334
392
  end
335
393
 
394
+ context "invalid target+destination configuration" do
395
+ let(:config) do
396
+ {
397
+ "source" => "message",
398
+ "target" => 'foo',
399
+ "destination" => 'bar',
400
+ }
401
+ end
402
+
403
+ it "raises an exception if both 'target' and 'destination' are set" do
404
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /remove .*?destination => /)
405
+ end
406
+ end
407
+
408
+ context "invalid source+field configuration" do
409
+ let(:config) do
410
+ {
411
+ "source" => "message",
412
+ "field" => 'foo'
413
+ }
414
+ end
415
+
416
+ it "raises an exception if both 'source' and 'field' are set" do
417
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /remove .*?field => /)
418
+ end
419
+ end
420
+
421
+ context "destination option" do
422
+ let(:config) do
423
+ {
424
+ "source" => "message", "destination" => 'bar', "ecs_compatibility" => 'v1'
425
+ }
426
+ end
427
+
428
+ it "sets the target" do
429
+ subject.register
430
+ expect( subject.target ).to eql 'bar'
431
+
432
+ expect(logger).to have_received(:debug).with(a_string_including "intercepting `destination`")
433
+ expect(deprecation_logger).to have_received(:deprecated).with(a_string_including "`destination` option is deprecated; use `target` instead.")
434
+ end
435
+ end
436
+
437
+ context "field option" do
438
+ let(:config) do
439
+ {
440
+ "field" => "message", "target" => 'bar'
441
+ }
442
+ end
443
+
444
+ it "sets the source" do
445
+ subject.register # does not raise
446
+ expect( subject.source ).to eql 'message'
447
+
448
+ expect(logger).to have_received(:debug).with(a_string_including "intercepting `field`")
449
+ expect(deprecation_logger).to have_received(:deprecated).with(a_string_including "`field` option is deprecated; use `source` instead.")
450
+ end
451
+ end
452
+
453
+ context "source option" do
454
+ let(:config) do
455
+ {
456
+ "target" => 'bar'
457
+ }
458
+ end
459
+
460
+ it "is required to be set" do
461
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /provide .*?source => /)
462
+ end
463
+ end
464
+
336
465
  describe "refresh_behaviour" do
337
466
  let(:dictionary_content) { "a : 1\nb : 2\nc : 3" }
338
467
  let(:modified_content) { "a : 1\nb : 4" }
@@ -340,8 +469,8 @@ describe LogStash::Filters::Translate do
340
469
  let(:refresh_behaviour) { "merge" }
341
470
  let(:config) do
342
471
  {
343
- "field" => "status",
344
- "destination" => "translation",
472
+ "source" => "status",
473
+ "target" => "translation",
345
474
  "dictionary_path" => dictionary_path,
346
475
  "refresh_interval" => -1, # we're controlling this manually
347
476
  "exact" => true,
@@ -407,8 +536,8 @@ describe LogStash::Filters::Translate do
407
536
 
408
537
  let(:config) do
409
538
  {
410
- "field" => "status",
411
- "destination" => "translation",
539
+ "source" => "status",
540
+ "target" => "translation",
412
541
  "dictionary_path" => dictionary_path.to_path,
413
542
  "refresh_interval" => -1,
414
543
  "fallback" => "no match",
@@ -457,4 +586,72 @@ describe LogStash::Filters::Translate do
457
586
  end
458
587
  end
459
588
  end
589
+
590
+ describe "default target" do
591
+
592
+ let(:config) do
593
+ {
594
+ "source" => "message",
595
+ "dictionary" => { "foo" => "bar" }
596
+ }
597
+ end
598
+
599
+ let(:event) { LogStash::Event.new("message" => "foo") }
600
+
601
+ before { subject.register }
602
+
603
+ context "legacy mode" do
604
+
605
+ let(:config) { super().merge('ecs_compatibility' => 'disabled') }
606
+
607
+ it "uses the translation target" do
608
+ subject.filter(event)
609
+ expect(event.get("translation")).to eq("bar")
610
+ expect(event.get("message")).to eq("foo")
611
+ end
612
+
613
+ end
614
+
615
+ context "ECS mode" do
616
+
617
+ let(:config) { super().merge('ecs_compatibility' => 'v1') }
618
+
619
+ it "does in place translation" do
620
+ subject.filter(event)
621
+ expect(event.include?("translation")).to be false
622
+ expect(event.get("message")).to eq("bar")
623
+ end
624
+
625
+ end
626
+
627
+ end
628
+
629
+
630
+ describe "error handling" do
631
+
632
+ let(:config) do
633
+ {
634
+ "source" => "message",
635
+ "dictionary" => { "foo" => "bar" }
636
+ }
637
+ end
638
+
639
+ let(:event) { LogStash::Event.new("message" => "foo") }
640
+
641
+ before { subject.register }
642
+
643
+ it "handles unexpected error within filter" do
644
+ expect(subject.updater).to receive(:update).and_raise RuntimeError.new('TEST')
645
+
646
+ expect { subject.filter(event) }.to_not raise_error
647
+ end
648
+
649
+ it "propagates Java errors" do
650
+ expect(subject.updater).to receive(:update).and_raise java.lang.OutOfMemoryError.new('FAKE-OUT!')
651
+
652
+ expect { subject.filter(event) }.to raise_error(java.lang.OutOfMemoryError)
653
+ end
654
+
655
+ end
656
+
460
657
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-filter-translate
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.2
4
+ version: 3.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-28 00:00:00.000000000 Z
11
+ date: 2022-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -30,6 +30,48 @@ dependencies:
30
30
  - - "<="
31
31
  - !ruby/object:Gem::Version
32
32
  version: '2.99'
33
+ - !ruby/object:Gem::Dependency
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.2'
39
+ name: logstash-mixin-ecs_compatibility_support
40
+ prerelease: false
41
+ type: :runtime
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.2'
47
+ - !ruby/object:Gem::Dependency
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '1.0'
53
+ name: logstash-mixin-validator_support
54
+ prerelease: false
55
+ type: :runtime
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ - !ruby/object:Gem::Dependency
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.0'
67
+ name: logstash-mixin-deprecation_logger_support
68
+ prerelease: false
69
+ type: :runtime
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.0'
33
75
  - !ruby/object:Gem::Dependency
34
76
  requirement: !ruby/object:Gem::Requirement
35
77
  requirements:
@@ -164,8 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
206
  - !ruby/object:Gem::Version
165
207
  version: '0'
166
208
  requirements: []
167
- rubyforge_project:
168
- rubygems_version: 2.6.13
209
+ rubygems_version: 3.1.6
169
210
  signing_key:
170
211
  specification_version: 4
171
212
  summary: Replaces field contents based on a hash or YAML file