prefab-cloud-ruby 0.24.5 → 1.0.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.
- checksums.yaml +4 -4
 - data/CHANGELOG.md +15 -0
 - data/VERSION +1 -1
 - data/compile_protos.sh +7 -0
 - data/lib/prefab/client.rb +20 -46
 - data/lib/prefab/config_client.rb +9 -12
 - data/lib/prefab/config_resolver.rb +2 -1
 - data/lib/prefab/config_value_unwrapper.rb +20 -9
 - data/lib/prefab/context.rb +43 -7
 - data/lib/prefab/context_shape_aggregator.rb +1 -1
 - data/lib/prefab/criteria_evaluator.rb +24 -16
 - data/lib/prefab/evaluation.rb +48 -0
 - data/lib/prefab/evaluation_summary_aggregator.rb +85 -0
 - data/lib/prefab/example_contexts_aggregator.rb +76 -0
 - data/lib/prefab/exponential_backoff.rb +5 -0
 - data/lib/prefab/feature_flag_client.rb +0 -2
 - data/lib/prefab/log_path_aggregator.rb +1 -1
 - data/lib/prefab/logger_client.rb +12 -13
 - data/lib/prefab/options.rb +52 -43
 - data/lib/prefab/periodic_sync.rb +30 -13
 - data/lib/prefab/rate_limit_cache.rb +41 -0
 - data/lib/prefab/resolved_config_presenter.rb +2 -4
 - data/lib/prefab/weighted_value_resolver.rb +1 -1
 - data/lib/prefab-cloud-ruby.rb +5 -5
 - data/lib/prefab_pb.rb +11 -1
 - data/prefab-cloud-ruby.gemspec +14 -9
 - data/test/integration_test.rb +1 -3
 - data/test/integration_test_helpers.rb +0 -1
 - data/test/support/common_helpers.rb +174 -0
 - data/test/support/mock_base_client.rb +44 -0
 - data/test/support/mock_config_client.rb +19 -0
 - data/test/support/mock_config_loader.rb +1 -0
 - data/test/test_client.rb +354 -40
 - data/test/test_config_client.rb +1 -0
 - data/test/test_config_loader.rb +1 -0
 - data/test/test_config_resolver.rb +25 -24
 - data/test/test_config_value_unwrapper.rb +22 -32
 - data/test/test_context.rb +1 -0
 - data/test/test_context_shape_aggregator.rb +11 -1
 - data/test/test_criteria_evaluator.rb +180 -133
 - data/test/test_evaluation_summary_aggregator.rb +162 -0
 - data/test/test_example_contexts_aggregator.rb +238 -0
 - data/test/test_helper.rb +5 -131
 - data/test/test_integration.rb +6 -4
 - data/test/test_local_config_parser.rb +2 -2
 - data/test/test_log_path_aggregator.rb +9 -1
 - data/test/test_logger.rb +6 -5
 - data/test/test_options.rb +33 -2
 - data/test/test_rate_limit_cache.rb +44 -0
 - data/test/test_weighted_value_resolver.rb +13 -7
 - metadata +13 -8
 - data/lib/prefab/evaluated_configs_aggregator.rb +0 -60
 - data/lib/prefab/evaluated_keys_aggregator.rb +0 -41
 - data/lib/prefab/noop_cache.rb +0 -15
 - data/lib/prefab/noop_stats.rb +0 -8
 - data/test/test_evaluated_configs_aggregator.rb +0 -254
 - data/test/test_evaluated_keys_aggregator.rb +0 -54
 
| 
         @@ -0,0 +1,174 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module CommonHelpers
         
     | 
| 
      
 4 
     | 
    
         
            +
              require 'timecop'
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              def setup
         
     | 
| 
      
 7 
     | 
    
         
            +
                $oldstderr, $stderr = $stderr, StringIO.new
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                $logs = nil
         
     | 
| 
      
 10 
     | 
    
         
            +
                Timecop.freeze('2023-08-09 15:18:12 -0400')
         
     | 
| 
      
 11 
     | 
    
         
            +
              end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
              def teardown
         
     | 
| 
      
 14 
     | 
    
         
            +
                if $logs && !$logs.string.empty?
         
     | 
| 
      
 15 
     | 
    
         
            +
                  raise "Unexpected logs. Handle logs with assert_only_expected_logs or assert_logged\n\n#{$logs.string}"
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                if $stderr != $oldstderr && !$stderr.string.empty?
         
     | 
| 
      
 19 
     | 
    
         
            +
                  # we ignore 2.X because of the number of `instance variable @xyz not initialized` warnings
         
     | 
| 
      
 20 
     | 
    
         
            +
                  if !RUBY_VERSION.start_with?('2.')
         
     | 
| 
      
 21 
     | 
    
         
            +
                    raise "Unexpected stderr. Handle stderr with assert_stderr\n\n#{$stderr.string}"
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                $stderr = $oldstderr
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                Timecop.return
         
     | 
| 
      
 28 
     | 
    
         
            +
              end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
              def with_env(key, value, &block)
         
     | 
| 
      
 31 
     | 
    
         
            +
                old_value = ENV.fetch(key, nil)
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                ENV[key] = value
         
     | 
| 
      
 34 
     | 
    
         
            +
                block.call
         
     | 
| 
      
 35 
     | 
    
         
            +
              ensure
         
     | 
| 
      
 36 
     | 
    
         
            +
                ENV[key] = old_value
         
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
              DEFAULT_NEW_CLIENT_OPTIONS = {
         
     | 
| 
      
 40 
     | 
    
         
            +
                prefab_config_override_dir: 'none',
         
     | 
| 
      
 41 
     | 
    
         
            +
                prefab_config_classpath_dir: 'test',
         
     | 
| 
      
 42 
     | 
    
         
            +
                prefab_envs: ['unit_tests'],
         
     | 
| 
      
 43 
     | 
    
         
            +
                prefab_datasources: Prefab::Options::DATASOURCES::LOCAL_ONLY
         
     | 
| 
      
 44 
     | 
    
         
            +
              }.freeze
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
              def new_client(overrides = {})
         
     | 
| 
      
 47 
     | 
    
         
            +
                $logs ||= StringIO.new
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                config = overrides.delete(:config)
         
     | 
| 
      
 50 
     | 
    
         
            +
                project_env_id = overrides.delete(:project_env_id)
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                options = Prefab::Options.new(
         
     | 
| 
      
 53 
     | 
    
         
            +
                  **DEFAULT_NEW_CLIENT_OPTIONS.merge(
         
     | 
| 
      
 54 
     | 
    
         
            +
                    overrides.merge(logdev: $logs)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  )
         
     | 
| 
      
 56 
     | 
    
         
            +
                )
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                Prefab::Client.new(options).tap do |client|
         
     | 
| 
      
 59 
     | 
    
         
            +
                  inject_config(client, config) if config
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  client.resolver.project_env_id = project_env_id if project_env_id
         
     | 
| 
      
 62 
     | 
    
         
            +
                end
         
     | 
| 
      
 63 
     | 
    
         
            +
              end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
              def string_list(values)
         
     | 
| 
      
 66 
     | 
    
         
            +
                PrefabProto::ConfigValue.new(string_list: PrefabProto::StringList.new(values: values))
         
     | 
| 
      
 67 
     | 
    
         
            +
              end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
              def inject_config(client, config)
         
     | 
| 
      
 70 
     | 
    
         
            +
                resolver = client.config_client.instance_variable_get('@config_resolver')
         
     | 
| 
      
 71 
     | 
    
         
            +
                store = resolver.instance_variable_get('@local_store')
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                Array(config).each do |c|
         
     | 
| 
      
 74 
     | 
    
         
            +
                  store[c.key] = { config: c }
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
              end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
              def inject_project_env_id(client, project_env_id)
         
     | 
| 
      
 79 
     | 
    
         
            +
                resolver = client.config_client.instance_variable_get('@config_resolver')
         
     | 
| 
      
 80 
     | 
    
         
            +
                resolver.project_env_id = project_env_id
         
     | 
| 
      
 81 
     | 
    
         
            +
              end
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
              FakeResponse = Struct.new(:status, :body)
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
              def wait_for_post_requests(client, max_wait: 2, sleep_time: 0.01)
         
     | 
| 
      
 86 
     | 
    
         
            +
                # we use ivars to avoid re-mocking the post method on subsequent calls
         
     | 
| 
      
 87 
     | 
    
         
            +
                client.instance_variable_set("@_requests", [])
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                if !client.instance_variable_get("@_already_faked_post")
         
     | 
| 
      
 90 
     | 
    
         
            +
                  client.define_singleton_method(:post) do |*params|
         
     | 
| 
      
 91 
     | 
    
         
            +
                    @_requests.push(params)
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                    FakeResponse.new(200, '')
         
     | 
| 
      
 94 
     | 
    
         
            +
                  end
         
     | 
| 
      
 95 
     | 
    
         
            +
                end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                client.instance_variable_set("@_already_faked_post", true)
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                yield
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                # let the flush thread run
         
     | 
| 
      
 102 
     | 
    
         
            +
                wait_time = 0
         
     | 
| 
      
 103 
     | 
    
         
            +
                while client.instance_variable_get("@_requests").empty?
         
     | 
| 
      
 104 
     | 
    
         
            +
                  wait_time += sleep_time
         
     | 
| 
      
 105 
     | 
    
         
            +
                  sleep sleep_time
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                  raise "Waited #{max_wait} seconds for the flush thread to run, but it never did" if wait_time > max_wait
         
     | 
| 
      
 108 
     | 
    
         
            +
                end
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                client.instance_variable_get("@_requests")
         
     | 
| 
      
 111 
     | 
    
         
            +
              end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
              def assert_summary(client, data)
         
     | 
| 
      
 114 
     | 
    
         
            +
                raise 'Evaluation summary aggregator not enabled' unless client.evaluation_summary_aggregator
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                assert_equal data, client.evaluation_summary_aggregator.data
         
     | 
| 
      
 117 
     | 
    
         
            +
              end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
              def assert_example_contexts(client, data)
         
     | 
| 
      
 120 
     | 
    
         
            +
                raise 'Example contexts aggregator not enabled' unless client.example_contexts_aggregator
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
                assert_equal data, client.example_contexts_aggregator.data
         
     | 
| 
      
 123 
     | 
    
         
            +
              end
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
              def weighted_values(values_and_weights, hash_by_property_name: 'user.key')
         
     | 
| 
      
 126 
     | 
    
         
            +
                values = values_and_weights.map do |value, weight|
         
     | 
| 
      
 127 
     | 
    
         
            +
                  weighted_value(value, weight)
         
     | 
| 
      
 128 
     | 
    
         
            +
                end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                PrefabProto::WeightedValues.new(weighted_values: values, hash_by_property_name: hash_by_property_name)
         
     | 
| 
      
 131 
     | 
    
         
            +
              end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
              def weighted_value(string, weight)
         
     | 
| 
      
 134 
     | 
    
         
            +
                PrefabProto::WeightedValue.new(
         
     | 
| 
      
 135 
     | 
    
         
            +
                  value: PrefabProto::ConfigValue.new(string: string), weight: weight
         
     | 
| 
      
 136 
     | 
    
         
            +
                )
         
     | 
| 
      
 137 
     | 
    
         
            +
              end
         
     | 
| 
      
 138 
     | 
    
         
            +
             
     | 
| 
      
 139 
     | 
    
         
            +
              def context(properties)
         
     | 
| 
      
 140 
     | 
    
         
            +
                Prefab::Context.new(properties)
         
     | 
| 
      
 141 
     | 
    
         
            +
              end
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
              def assert_only_expected_logs
         
     | 
| 
      
 144 
     | 
    
         
            +
                assert_equal "WARN  2023-08-09 15:18:12 -0400: cloud.prefab.client No success loading checkpoints\n", $logs.string
         
     | 
| 
      
 145 
     | 
    
         
            +
                # mark nil to indicate we handled it
         
     | 
| 
      
 146 
     | 
    
         
            +
                $logs = nil
         
     | 
| 
      
 147 
     | 
    
         
            +
              end
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
              def assert_logged(expected)
         
     | 
| 
      
 150 
     | 
    
         
            +
                # we do a uniq here because logging can happen in a separate thread so the
         
     | 
| 
      
 151 
     | 
    
         
            +
                # number of times a log might happen could be slightly variable.
         
     | 
| 
      
 152 
     | 
    
         
            +
                assert_equal expected, $logs.string.split("\n").uniq
         
     | 
| 
      
 153 
     | 
    
         
            +
                # mark nil to indicate we handled it
         
     | 
| 
      
 154 
     | 
    
         
            +
                $logs = nil
         
     | 
| 
      
 155 
     | 
    
         
            +
              end
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
              def assert_stderr(expected)
         
     | 
| 
      
 158 
     | 
    
         
            +
                assert ($stderr.string.split("\n").uniq & expected).size > 0
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
                # Ruby 2.X has a lot of warnings about instance variables not being
         
     | 
| 
      
 161 
     | 
    
         
            +
                # initialized so we don't try to assert on stderr for those versions.
         
     | 
| 
      
 162 
     | 
    
         
            +
                # Instead we just stop after asserting that our expected errors are
         
     | 
| 
      
 163 
     | 
    
         
            +
                # included in the output.
         
     | 
| 
      
 164 
     | 
    
         
            +
                if RUBY_VERSION.start_with?('2.')
         
     | 
| 
      
 165 
     | 
    
         
            +
                  puts $stderr.string
         
     | 
| 
      
 166 
     | 
    
         
            +
                  return
         
     | 
| 
      
 167 
     | 
    
         
            +
                end
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                assert_equal expected, $stderr.string.split("\n")
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                # restore since we've handled it
         
     | 
| 
      
 172 
     | 
    
         
            +
                $stderr = $oldstderr
         
     | 
| 
      
 173 
     | 
    
         
            +
              end
         
     | 
| 
      
 174 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,44 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            class MockBaseClient
         
     | 
| 
      
 4 
     | 
    
         
            +
              STAGING_ENV_ID = 1
         
     | 
| 
      
 5 
     | 
    
         
            +
              PRODUCTION_ENV_ID = 2
         
     | 
| 
      
 6 
     | 
    
         
            +
              TEST_ENV_ID = 3
         
     | 
| 
      
 7 
     | 
    
         
            +
              attr_reader :namespace, :logger, :config_client, :options, :posts
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              def initialize(options = Prefab::Options.new)
         
     | 
| 
      
 10 
     | 
    
         
            +
                @options = options
         
     | 
| 
      
 11 
     | 
    
         
            +
                @namespace = namespace
         
     | 
| 
      
 12 
     | 
    
         
            +
                @logger = Prefab::LoggerClient.new($stdout)
         
     | 
| 
      
 13 
     | 
    
         
            +
                @config_client = MockConfigClient.new
         
     | 
| 
      
 14 
     | 
    
         
            +
                @posts = []
         
     | 
| 
      
 15 
     | 
    
         
            +
              end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              def instance_hash
         
     | 
| 
      
 18 
     | 
    
         
            +
                'mock-base-client-instance-hash'
         
     | 
| 
      
 19 
     | 
    
         
            +
              end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              def project_id
         
     | 
| 
      
 22 
     | 
    
         
            +
                1
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
              def post(_, _)
         
     | 
| 
      
 26 
     | 
    
         
            +
                raise 'Use wait_for_post_requests'
         
     | 
| 
      
 27 
     | 
    
         
            +
              end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
              def log
         
     | 
| 
      
 30 
     | 
    
         
            +
                @logger
         
     | 
| 
      
 31 
     | 
    
         
            +
              end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
              def log_internal(level, message); end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
              def context_shape_aggregator; end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
              def evaluation_summary_aggregator; end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
              def example_contexts_aggregator; end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
              def config_value(key)
         
     | 
| 
      
 42 
     | 
    
         
            +
                @config_values[key]
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,19 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            class MockConfigClient
         
     | 
| 
      
 4 
     | 
    
         
            +
              def initialize(config_values = {})
         
     | 
| 
      
 5 
     | 
    
         
            +
                @config_values = config_values
         
     | 
| 
      
 6 
     | 
    
         
            +
              end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              def get(key, default = nil)
         
     | 
| 
      
 9 
     | 
    
         
            +
                @config_values.fetch(key, default)
         
     | 
| 
      
 10 
     | 
    
         
            +
              end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
              def get_config(key)
         
     | 
| 
      
 13 
     | 
    
         
            +
                PrefabProto::Config.new(value: @config_values[key], key: key)
         
     | 
| 
      
 14 
     | 
    
         
            +
              end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
              def mock_this_config(key, config_value)
         
     | 
| 
      
 17 
     | 
    
         
            +
                @config_values[key] = config_value
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     |