sbmt-strangler 0.9.1 → 0.14.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9bb8754c88d1b03e5d4a32bc244c6e14351d0e875dfd12bbf9011cb08c450ce
4
- data.tar.gz: 3ac98da59470beb7f2d023352072bc589867e5af9d2160d07a805529577e7482
3
+ metadata.gz: fb3b0530db252f7ac57631610a89b1f9750942275cba851f6d7fdb4ed2e2b825
4
+ data.tar.gz: 7478e5f4f6363858378ee48ad7eee94271133611f19351336885cec31d581928
5
5
  SHA512:
6
- metadata.gz: e1d13adacfa0630878515a07ae6619ddd23d2182a1fb17183be233ef7c10e0552c8917404d9728961e2fc14d0aa68131c328245b111465a56370a6d8e3d87086
7
- data.tar.gz: 405bf3d3146936259e97aeb52f6d0a5065d5080be4891b9cff97463fde3df32a6d998a5b1b023b630b1e1fc4c35dc353384dfcf09431f9549d7e641cd065ef69
6
+ metadata.gz: ce4c1ffc70a7286300e776e01dba508231c7dfac722d0f833eae264a68921f01d21a36d1031e507410c47642fd573912b9afc5cba16bce008c48644b0286ef29
7
+ data.tar.gz: 88236ec2185c89cc6eb8cbe1672439b6d4cf8ef617f0a098ed449470400378c58a753763f6653b768a8115f72f73fc542d26a26faf5475eef86fb6c6edae87ad
data/Appraisals CHANGED
@@ -4,7 +4,8 @@
4
4
 
5
5
  versions_map = {
6
6
  "7.0" => %w[3.1],
7
- "7.1" => %w[3.2 3.3]
7
+ "7.1" => %w[3.2 3.3],
8
+ "8.0" => %w[3.2 3.3]
8
9
  }
9
10
 
10
11
  current_ruby_version = RUBY_VERSION.split(".").first(2).join(".")
data/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
+ ## [0.14.1] - 2026-03-16
9
+
10
+ ### Fixed
11
+ - Use `params_tracking_allowlist` directly without permitting
12
+
13
+ ## [0.14.0] - 2025-06-05
14
+
15
+ ### Changed
16
+ - Support Rails v8
17
+
18
+ ## [0.11.0] - 2024-09-08
19
+
20
+ ### Changed
21
+ - `compose` receives two parameters now - `responses` and `rails_controller`
22
+
23
+ ## [0.10.1] - 2024-09-02
24
+
25
+ ### Fixed
26
+ - Use `Rails.application.executor.wrap` for application code running inside new thread
27
+
28
+ ## [0.10.0] - 2024-08-05
29
+
30
+ ### Added
31
+ - Add composition mode
32
+
33
+ ## [0.9.2] - 2024-07-12
34
+
35
+ ### Changed
36
+ - Now ONTIME flipper flags can work with any hours range
37
+ - Now hours range don't include last value to work
38
+
8
39
  ## [0.9.1] - 2024-07-02
9
40
 
10
41
  ### Added
data/Gemfile CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source "https://rubygems.org"
3
+ source ENV.fetch("NEXUS_PUBLIC_SOURCE_URL", "https://rubygems.org")
4
4
 
5
5
  gemspec
@@ -4,7 +4,9 @@ module Sbmt
4
4
  module Strangler
5
5
  module Metrics
6
6
  module Yabeda
7
- HTTP_BUCKETS = [0.01, 0.02, 0.04, 0.1, 0.2, 0.5, 0.8, 1, 1.5, 2, 5, 15, 30, 60].freeze
7
+ DEFAULT_BUCKETS = [0.01, 0.02, 0.04, 0.1, 0.2, 0.5, 0.8, 1, 1.5, 2, 5, 15, 30, 60].freeze
8
+ HTTP_BUCKETS = DEFAULT_BUCKETS
9
+ COMPOSITION_BUCKETS = DEFAULT_BUCKETS
8
10
 
9
11
  ::Yabeda.configure do
10
12
  group :sbmt_strangler do
@@ -38,3 +40,27 @@ module Sbmt
38
40
  end
39
41
  end
40
42
  end
43
+
44
+ # Declaring composition step duration metric in an `after_initialize` block
45
+ # allows user to customize buckets in his app-level configuration file:
46
+ #
47
+ # # config/initializers/strangler.rb
48
+ # Sbmt::Strangler.configure do |strangler|
49
+ # strangler.composition_step_duration_metric_buckets = [0.1, 0.2, 0.3]
50
+ # end
51
+ #
52
+ Rails.application.config.after_initialize do
53
+ ::Yabeda.configure do
54
+ group :sbmt_strangler do
55
+ composition_buckets =
56
+ ::Sbmt::Strangler.configuration.composition_step_duration_metric_buckets ||
57
+ ::Sbmt::Strangler::Metrics::Yabeda::COMPOSITION_BUCKETS
58
+
59
+ histogram :composition_step_duration,
60
+ tags: %i[step part type level parent controller action],
61
+ unit: :seconds,
62
+ buckets: composition_buckets,
63
+ comment: "Composition step duration"
64
+ end
65
+ end
66
+ end
data/dip.yml CHANGED
@@ -48,6 +48,8 @@ interaction:
48
48
  command: bundle exec appraisal rails-7.0 rspec
49
49
  rails-7.1:
50
50
  command: bundle exec appraisal rails-7.1 rspec
51
+ rails-8.0:
52
+ command: bundle exec appraisal rails-8.0 rspec
51
53
 
52
54
  rubocop:
53
55
  description: Run Ruby linter
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class Action
6
+ module Composition
7
+ module Errors
8
+ class ConfigurationError < StandardError; end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class Action
6
+ module Composition
7
+ module Errors
8
+ class MaxLevelError < StandardError; end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class Action
6
+ module Composition
7
+ module Metrics
8
+ private
9
+
10
+ def with_metrics(rails_controller:, part: nil)
11
+ result = nil
12
+ with_yabeda_duration_measurement(rails_controller: rails_controller, part: part) do
13
+ with_open_telemetry_tracing(part: part) do
14
+ result = yield
15
+ end
16
+ end
17
+ result
18
+ end
19
+
20
+ def with_yabeda_duration_measurement(rails_controller:, part: nil)
21
+ result = nil
22
+ yabeda_tags = {
23
+ step: name.to_s,
24
+ part: part&.to_s,
25
+ type: type.to_s,
26
+ level: level.to_s,
27
+ parent: parent&.name&.to_s,
28
+ controller: rails_controller.controller_path,
29
+ action: rails_controller.action_name
30
+ }
31
+ Yabeda.sbmt_strangler.composition_step_duration.measure(yabeda_tags) do
32
+ result = yield
33
+ end
34
+ result
35
+ end
36
+
37
+ def with_open_telemetry_tracing(part: nil)
38
+ return yield unless Object.const_defined?(:OpenTelemetry)
39
+
40
+ span_name = "Composition step: #{name} (#{type})"
41
+ span_name += " / #{part}" unless part.nil?
42
+
43
+ span_attrs = {
44
+ "step" => name.to_s,
45
+ "type" => type.to_s,
46
+ "level" => level
47
+ }
48
+ span_attrs["part"] = part.to_s unless part.nil?
49
+ span_attrs["parent"] = parent.name.to_s unless parent.nil?
50
+
51
+ result = nil
52
+ ::OpenTelemetry.tracer_provider.tracer("Sbmt::Strangler")
53
+ .in_span(span_name, attributes: span_attrs, kind: :internal) do |_span|
54
+ result = yield
55
+ end
56
+ result
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "metrics"
4
+ require_relative "errors/configuration_error"
5
+ require_relative "errors/max_level_error"
6
+
7
+ module Sbmt
8
+ module Strangler
9
+ class Action
10
+ module Composition
11
+ class Step
12
+ include Metrics
13
+
14
+ TYPES = %i[sync async].freeze
15
+ MAX_LEVEL = 2
16
+
17
+ attr_reader :name, :type, :level, :parent
18
+
19
+ def initialize(name:, type: :sync, level: 0, parent: nil)
20
+ if name.nil? || name.to_s == ""
21
+ raise Errors::ConfigurationError, "Composition step name must be a non-empty string or symbol"
22
+ end
23
+
24
+ if TYPES.exclude?(type)
25
+ raise Errors::ConfigurationError, "Composition step type must be a symbol from #{TYPES}"
26
+ end
27
+
28
+ if !parent.nil? && !parent.is_a?(self.class)
29
+ raise Errors::ConfigurationError, "Composition step parent must be either #{self.class} or nil"
30
+ end
31
+
32
+ if !level.is_a?(Integer) && level >= 0
33
+ raise Errors::ConfigurationError, "Composition step level must be a non-negative integer"
34
+ end
35
+
36
+ if level > MAX_LEVEL
37
+ raise Errors::MaxLevelError, "Composition step is too deeply nested"
38
+ end
39
+
40
+ @name = name
41
+ @type = type
42
+ @level = level
43
+ @parent = parent
44
+ @sync_steps = {}
45
+ @async_steps = {}
46
+ end
47
+
48
+ def call(rails_controller, previous_responses: {})
49
+ with_metrics(rails_controller: rails_controller) do
50
+ result = begin
51
+ with_metrics(part: :process, rails_controller: rails_controller) do
52
+ process_lambda.call(rails_controller, previous_responses)
53
+ end
54
+ rescue => error
55
+ handle_error(error)
56
+
57
+ {} # TODO: Better error handling in composition.
58
+ end
59
+
60
+ # process composition if there are nested steps
61
+ if composite?
62
+ result = call_composition(rails_controller, previous_responses: previous_responses.merge(name => result))
63
+ end
64
+
65
+ result
66
+ end
67
+ end
68
+
69
+ def composite?
70
+ @sync_steps.any? || @async_steps.any?
71
+ end
72
+
73
+ def sync(name, &)
74
+ if @async_steps.key?(name)
75
+ raise Errors::ConfigurationError, "Composition step #{name.inspect} has been already defined as async"
76
+ end
77
+
78
+ @sync_steps[name] ||=
79
+ Sbmt::Strangler::Action::Composition::Step.new(
80
+ name: name,
81
+ type: :sync,
82
+ parent: self,
83
+ level: level + 1
84
+ )
85
+ yield(@sync_steps[name]) if block_given?
86
+ self
87
+ end
88
+
89
+ def async(name, &)
90
+ if @sync_steps.key?(name)
91
+ raise Errors::ConfigurationError, "Composition step #{name.inspect} has been already defined as sync"
92
+ end
93
+
94
+ @async_steps[name] ||=
95
+ Sbmt::Strangler::Action::Composition::Step.new(
96
+ name: name,
97
+ type: :async,
98
+ parent: self,
99
+ level: level + 1
100
+ )
101
+ yield(@async_steps[name]) if block_given?
102
+ self
103
+ end
104
+
105
+ def process(&block)
106
+ @process_lambda = block
107
+ self
108
+ end
109
+
110
+ def compose(&block)
111
+ @compose_lambda = block
112
+ self
113
+ end
114
+
115
+ private
116
+
117
+ attr_reader :sync_steps, :async_steps, :process_lambda, :compose_lambda
118
+
119
+ def call_composition(rails_controller, previous_responses: {})
120
+ responses = nil
121
+
122
+ with_metrics(part: :substeps, rails_controller: rails_controller) do
123
+ async_responses = async_steps.map do |name, step|
124
+ Concurrent::Promises.future do
125
+ Rails.application.executor.wrap do
126
+ result = step.call(rails_controller, previous_responses: previous_responses)
127
+ {name => result}
128
+ end
129
+ end
130
+ end
131
+
132
+ sync_responses = sync_steps.reduce(previous_responses) do |result, (name, step)|
133
+ result.merge(name => step.call(rails_controller, previous_responses: result))
134
+ end
135
+
136
+ responses = async_responses.map(&:value).reduce(sync_responses) do |result, step_result|
137
+ result.merge(step_result)
138
+ end
139
+ end
140
+
141
+ with_metrics(part: :compose, rails_controller: rails_controller) do
142
+ compose_lambda.call(responses, rails_controller)
143
+ end
144
+ rescue => error
145
+ handle_error(error)
146
+
147
+ {} # TODO: Better error handling in composition.
148
+ end
149
+
150
+ def handle_error(error)
151
+ Sbmt::Strangler.logger.error(error.message)
152
+ Sbmt::Strangler.error_tracker.error(error)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "action/composition/step"
4
+
3
5
  module Sbmt
4
6
  module Strangler
5
7
  class Action
@@ -32,6 +34,18 @@ module Sbmt
32
34
  def http_client
33
35
  @http_client ||= Sbmt::Strangler::Http::Client.new(http_options: http)
34
36
  end
37
+
38
+ def composition(&)
39
+ if block_given?
40
+ @composition ||= Composition::Step.new(name: :root)
41
+ yield(@composition)
42
+ end
43
+ @composition
44
+ end
45
+
46
+ def composition?
47
+ !composition.nil?
48
+ end
35
49
  end
36
50
  end
37
51
  end
@@ -9,6 +9,7 @@ module Sbmt
9
9
  option :action_controller_base_class, default: "ActionController::Base"
10
10
  option :error_tracker, default: "Sbmt::Strangler::ErrorTracker"
11
11
  option :flipper_actor, default: ->(_http_params, _headers) {}
12
+ option :composition_step_duration_metric_buckets, default: nil
12
13
 
13
14
  attr_reader :controllers, :http
14
15
 
@@ -30,7 +30,13 @@ module Sbmt
30
30
  .compact
31
31
 
32
32
  hour_now = DateTime.now.in_time_zone.hour
33
- hours_ranges.any? { |range| (range.first..range.last).cover?(hour_now) }
33
+ hours_ranges.any? do |range|
34
+ return (range.first...range.last).cover?(hour_now) if range.last > range.first
35
+
36
+ range_last = range.last + 24
37
+ (range.first...range_last).cover?(hour_now + 24) ||
38
+ (range.first...range_last).cover?(hour_now)
39
+ end
34
40
  end
35
41
  end
36
42
  end
@@ -14,7 +14,7 @@ module Sbmt
14
14
  end
15
15
 
16
16
  def log_unallowed_params
17
- unallowed_params = all_request_params - allowed_request_params
17
+ unallowed_params = all_request_params_keys - allowed_request_params_keys
18
18
  Sbmt::Strangler.logger.log_warn(<<~WARN.strip) if unallowed_params.any?
19
19
  Not allowed parameters in #{controller_path}##{action_name}: #{unallowed_params}
20
20
  WARN
@@ -47,21 +47,25 @@ module Sbmt
47
47
 
48
48
  private
49
49
 
50
- delegate :http_params, :allowed_params, :controller_path, :action_name, to: :rails_controller
50
+ delegate :http_params, :strangler_action, :controller_path, :action_name, to: :rails_controller
51
51
 
52
52
  def common_tags
53
53
  {
54
- params: allowed_request_params.join(","),
54
+ params: allowed_request_params_keys.join(","),
55
55
  controller: controller_path,
56
56
  action: action_name
57
57
  }
58
58
  end
59
59
 
60
- def allowed_request_params
61
- @allowed_request_params ||= allowed_params.keys.map(&:to_s).sort.uniq
60
+ def allowed_request_params_keys
61
+ @allowed_request_params ||= if strangler_action.params_tracking_allowlist.blank?
62
+ all_request_params_keys
63
+ else
64
+ (all_request_params_keys & strangler_action.params_tracking_allowlist).sort
65
+ end
62
66
  end
63
67
 
64
- def all_request_params
68
+ def all_request_params_keys
65
69
  @all_request_params ||= http_params.keys.map(&:to_s).sort.uniq
66
70
  end
67
71
  end
@@ -9,12 +9,6 @@ module Sbmt
9
9
  params.to_unsafe_h.except(:action, :controller, :format)
10
10
  end
11
11
 
12
- def allowed_params
13
- return http_params if strangler_action.params_tracking_allowlist.blank?
14
-
15
- params.permit(*strangler_action.params_tracking_allowlist).to_h
16
- end
17
-
18
12
  def allowed_headers
19
13
  if strangler_action.headers_allowlist.blank?
20
14
  return request.headers.select { |name, _| name.starts_with?("HTTP_") }.to_h
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sbmt
4
4
  module Strangler
5
- VERSION = "0.9.1"
5
+ VERSION = "0.14.1"
6
6
  end
7
7
  end
@@ -9,8 +9,17 @@ module Sbmt
9
9
  include Dry::Monads::Result::Mixin
10
10
 
11
11
  def call
12
- mirror_task = Concurrent::Promises.future { mirror_call }
13
- proxy_task = Concurrent::Promises.future { http_request(http_params) }
12
+ mirror_task = Concurrent::Promises.future do
13
+ Rails.application.executor.wrap do
14
+ mirror_call
15
+ end
16
+ end
17
+
18
+ proxy_task = Concurrent::Promises.future do
19
+ Rails.application.executor.wrap do
20
+ http_request(http_params)
21
+ end
22
+ end
14
23
 
15
24
  mirror_call_result = mirror_task.value!
16
25
  origin_response = proxy_task.value!
@@ -45,7 +54,12 @@ module Sbmt
45
54
  end
46
55
 
47
56
  def mirror_call
48
- value = strangler_action.mirror.call(rails_controller)
57
+ value = if strangler_action.composition?
58
+ strangler_action.composition.call(rails_controller)
59
+ else
60
+ strangler_action.mirror.call(rails_controller)
61
+ end
62
+
49
63
  Success(value)
50
64
  rescue => err
51
65
  handle_error(err)
@@ -40,7 +40,12 @@ module Sbmt
40
40
  delegate :track_mirror_call, :track_render_call, to: :metric_tracker
41
41
 
42
42
  def mirror_call
43
- value = strangler_action.mirror.call(rails_controller)
43
+ value = if strangler_action.composition?
44
+ strangler_action.composition.call(rails_controller)
45
+ else
46
+ strangler_action.mirror.call(rails_controller)
47
+ end
48
+
44
49
  Success(value)
45
50
  rescue => err
46
51
  handle_error(err)
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.homepage = "https://github.com/SberMarket-Tech/sbmt-strangler"
13
13
  spec.required_ruby_version = ">= 3.1.0"
14
14
 
15
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
15
+ spec.metadata["allowed_push_host"] = ENV.fetch("NEXUS_URL", "https://rubygems.org")
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = spec.homepage
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency "faraday", "> 2.0"
32
32
  spec.add_dependency "faraday-net_http_persistent", "~> 2.0"
33
33
  spec.add_dependency "net-http-persistent", ">= 4.0.1"
34
- spec.add_dependency "rails", ">= 6.1", "< 8"
34
+ spec.add_dependency "rails", ">= 6.1", "< 8.1"
35
35
  spec.add_dependency "yabeda", ">= 0.11"
36
36
  spec.add_dependency "flipper", ">= 1.2.2"
37
37
  spec.add_dependency "concurrent-ruby", ">= 1.2.3"
@@ -54,6 +54,6 @@ Gem::Specification.new do |spec|
54
54
  spec.add_development_dependency "vcr"
55
55
  spec.add_development_dependency "standard", ">= 1.7"
56
56
  spec.add_development_dependency "zeitwerk"
57
- spec.add_development_dependency "sentry-rails", "> 5.2.0"
57
+ spec.add_development_dependency "sentry-rails", "> 6.0.0"
58
58
  spec.add_development_dependency "debug"
59
59
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sbmt-strangler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - sbermarket team
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-07-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: faraday
@@ -61,7 +60,7 @@ dependencies:
61
60
  version: '6.1'
62
61
  - - "<"
63
62
  - !ruby/object:Gem::Version
64
- version: '8'
63
+ version: '8.1'
65
64
  type: :runtime
66
65
  prerelease: false
67
66
  version_requirements: !ruby/object:Gem::Requirement
@@ -71,7 +70,7 @@ dependencies:
71
70
  version: '6.1'
72
71
  - - "<"
73
72
  - !ruby/object:Gem::Version
74
- version: '8'
73
+ version: '8.1'
75
74
  - !ruby/object:Gem::Dependency
76
75
  name: yabeda
77
76
  requirement: !ruby/object:Gem::Requirement
@@ -372,14 +371,14 @@ dependencies:
372
371
  requirements:
373
372
  - - ">"
374
373
  - !ruby/object:Gem::Version
375
- version: 5.2.0
374
+ version: 6.0.0
376
375
  type: :development
377
376
  prerelease: false
378
377
  version_requirements: !ruby/object:Gem::Requirement
379
378
  requirements:
380
379
  - - ">"
381
380
  - !ruby/object:Gem::Version
382
- version: 5.2.0
381
+ version: 6.0.0
383
382
  - !ruby/object:Gem::Dependency
384
383
  name: debug
385
384
  requirement: !ruby/object:Gem::Requirement
@@ -395,7 +394,6 @@ dependencies:
395
394
  - !ruby/object:Gem::Version
396
395
  version: '0'
397
396
  description: Utility for strangler pattern
398
- email:
399
397
  executables: []
400
398
  extensions: []
401
399
  extra_rdoc_files: []
@@ -420,6 +418,10 @@ files:
420
418
  - lefthook.yml
421
419
  - lib/sbmt/strangler.rb
422
420
  - lib/sbmt/strangler/action.rb
421
+ - lib/sbmt/strangler/action/composition/errors/configuration_error.rb
422
+ - lib/sbmt/strangler/action/composition/errors/max_level_error.rb
423
+ - lib/sbmt/strangler/action/composition/metrics.rb
424
+ - lib/sbmt/strangler/action/composition/step.rb
423
425
  - lib/sbmt/strangler/action_invoker.rb
424
426
  - lib/sbmt/strangler/builder.rb
425
427
  - lib/sbmt/strangler/configurable.rb
@@ -451,7 +453,6 @@ metadata:
451
453
  source_code_uri: https://github.com/SberMarket-Tech/sbmt-strangler
452
454
  changelog_uri: https://github.com/SberMarket-Tech/sbmt-strangler/-/blob/master/CHANGELOG.md
453
455
  rubygems_mfa_required: 'false'
454
- post_install_message:
455
456
  rdoc_options: []
456
457
  require_paths:
457
458
  - lib
@@ -466,8 +467,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
466
467
  - !ruby/object:Gem::Version
467
468
  version: '0'
468
469
  requirements: []
469
- rubygems_version: 3.5.11
470
- signing_key:
470
+ rubygems_version: 3.6.9
471
471
  specification_version: 4
472
472
  summary: Utility for strangler pattern
473
473
  test_files: []