prefab-cloud-ruby 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 (91) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/workflows/ruby.yml +46 -0
  4. data/.gitmodules +3 -0
  5. data/.rubocop.yml +13 -0
  6. data/.tool-versions +1 -0
  7. data/CHANGELOG.md +169 -0
  8. data/CODEOWNERS +1 -0
  9. data/Gemfile +26 -0
  10. data/Gemfile.lock +188 -0
  11. data/LICENSE.txt +20 -0
  12. data/README.md +94 -0
  13. data/Rakefile +50 -0
  14. data/VERSION +1 -0
  15. data/bin/console +21 -0
  16. data/compile_protos.sh +18 -0
  17. data/lib/prefab/client.rb +153 -0
  18. data/lib/prefab/config_client.rb +292 -0
  19. data/lib/prefab/config_client_presenter.rb +18 -0
  20. data/lib/prefab/config_loader.rb +84 -0
  21. data/lib/prefab/config_resolver.rb +77 -0
  22. data/lib/prefab/config_value_unwrapper.rb +115 -0
  23. data/lib/prefab/config_value_wrapper.rb +18 -0
  24. data/lib/prefab/context.rb +179 -0
  25. data/lib/prefab/context_shape.rb +20 -0
  26. data/lib/prefab/context_shape_aggregator.rb +65 -0
  27. data/lib/prefab/criteria_evaluator.rb +136 -0
  28. data/lib/prefab/encryption.rb +65 -0
  29. data/lib/prefab/error.rb +6 -0
  30. data/lib/prefab/errors/env_var_parse_error.rb +11 -0
  31. data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
  32. data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
  33. data/lib/prefab/errors/missing_default_error.rb +13 -0
  34. data/lib/prefab/errors/missing_env_var_error.rb +11 -0
  35. data/lib/prefab/errors/uninitialized_error.rb +13 -0
  36. data/lib/prefab/evaluation.rb +52 -0
  37. data/lib/prefab/evaluation_summary_aggregator.rb +87 -0
  38. data/lib/prefab/example_contexts_aggregator.rb +78 -0
  39. data/lib/prefab/exponential_backoff.rb +21 -0
  40. data/lib/prefab/feature_flag_client.rb +42 -0
  41. data/lib/prefab/http_connection.rb +41 -0
  42. data/lib/prefab/internal_logger.rb +16 -0
  43. data/lib/prefab/local_config_parser.rb +151 -0
  44. data/lib/prefab/log_path_aggregator.rb +69 -0
  45. data/lib/prefab/logger_client.rb +264 -0
  46. data/lib/prefab/murmer3.rb +50 -0
  47. data/lib/prefab/options.rb +208 -0
  48. data/lib/prefab/periodic_sync.rb +69 -0
  49. data/lib/prefab/prefab.rb +56 -0
  50. data/lib/prefab/rate_limit_cache.rb +41 -0
  51. data/lib/prefab/resolved_config_presenter.rb +86 -0
  52. data/lib/prefab/time_helpers.rb +7 -0
  53. data/lib/prefab/weighted_value_resolver.rb +42 -0
  54. data/lib/prefab/yaml_config_parser.rb +34 -0
  55. data/lib/prefab-cloud-ruby.rb +57 -0
  56. data/lib/prefab_pb.rb +93 -0
  57. data/prefab-cloud-ruby.gemspec +155 -0
  58. data/test/.prefab.default.config.yaml +2 -0
  59. data/test/.prefab.unit_tests.config.yaml +28 -0
  60. data/test/integration_test.rb +150 -0
  61. data/test/integration_test_helpers.rb +151 -0
  62. data/test/support/common_helpers.rb +180 -0
  63. data/test/support/mock_base_client.rb +42 -0
  64. data/test/support/mock_config_client.rb +19 -0
  65. data/test/support/mock_config_loader.rb +1 -0
  66. data/test/test_client.rb +444 -0
  67. data/test/test_config_client.rb +109 -0
  68. data/test/test_config_loader.rb +117 -0
  69. data/test/test_config_resolver.rb +430 -0
  70. data/test/test_config_value_unwrapper.rb +224 -0
  71. data/test/test_config_value_wrapper.rb +42 -0
  72. data/test/test_context.rb +203 -0
  73. data/test/test_context_shape.rb +50 -0
  74. data/test/test_context_shape_aggregator.rb +147 -0
  75. data/test/test_criteria_evaluator.rb +726 -0
  76. data/test/test_encryption.rb +16 -0
  77. data/test/test_evaluation_summary_aggregator.rb +162 -0
  78. data/test/test_example_contexts_aggregator.rb +238 -0
  79. data/test/test_exponential_backoff.rb +18 -0
  80. data/test/test_feature_flag_client.rb +48 -0
  81. data/test/test_helper.rb +17 -0
  82. data/test/test_integration.rb +58 -0
  83. data/test/test_local_config_parser.rb +147 -0
  84. data/test/test_log_path_aggregator.rb +62 -0
  85. data/test/test_logger.rb +621 -0
  86. data/test/test_logger_initialization.rb +12 -0
  87. data/test/test_options.rb +75 -0
  88. data/test/test_prefab.rb +12 -0
  89. data/test/test_rate_limit_cache.rb +44 -0
  90. data/test/test_weighted_value_resolver.rb +71 -0
  91. metadata +337 -0
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ class ConfigValueUnwrapper
5
+ LOG = Prefab::InternalLogger.new(ConfigValueUnwrapper)
6
+ CONFIDENTIAL_PREFIX = "*****"
7
+ attr_reader :weighted_value_index
8
+
9
+ def initialize(config_value, resolver, weighted_value_index = nil)
10
+ @config_value = config_value
11
+ @resolver = resolver
12
+ @weighted_value_index = weighted_value_index
13
+ end
14
+
15
+ def reportable_wrapped_value
16
+ if @config_value.confidential || @config_value.decrypt_with&.length&.positive?
17
+ # Unique hash for differentiation
18
+ Prefab::ConfigValueWrapper.wrap("#{CONFIDENTIAL_PREFIX}#{Digest::MD5.hexdigest(unwrap)[0, 5]}")
19
+ else
20
+ @config_value
21
+ end
22
+ end
23
+
24
+ def reportable_value
25
+ Prefab::ConfigValueUnwrapper.new(reportable_wrapped_value, @resolver, @weighted_value_index).unwrap
26
+ end
27
+
28
+ def raw_config_value
29
+ @config_value
30
+ end
31
+
32
+ # this will return the actual value of confidential, use reportable_value unless you need it
33
+ def unwrap
34
+ raw = case @config_value.type
35
+ when :int, :string, :double, :bool, :log_level
36
+ @config_value.public_send(@config_value.type)
37
+ when :string_list
38
+ @config_value.string_list.values
39
+ else
40
+ LOG.error "Unknown type: #{@config_value.type}"
41
+ raise "Unknown type: #{@config_value.type}"
42
+ end
43
+ if @config_value.has_decrypt_with?
44
+ decryption_key = @resolver.get(@config_value.decrypt_with)&.unwrapped_value
45
+ if decryption_key.nil?
46
+ LOG.warn "No value for decryption key #{@config_value.decrypt_with} found."
47
+ return ""
48
+ else
49
+ unencrypted = Prefab::Encryption.new(decryption_key).decrypt(raw)
50
+ return unencrypted
51
+ end
52
+ end
53
+
54
+ raw
55
+ end
56
+
57
+ def self.deepest_value(config_value, config, context, resolver)
58
+ if config_value&.type == :weighted_values
59
+ value, index = Prefab::WeightedValueResolver.new(
60
+ config_value.weighted_values.weighted_values,
61
+ config.key,
62
+ context.get(config_value.weighted_values.hash_by_property_name)
63
+ ).resolve
64
+
65
+ new(deepest_value(value.value, config, context, resolver).raw_config_value, resolver, index)
66
+
67
+ elsif config_value&.type == :provided
68
+ if :ENV_VAR == config_value.provided.source
69
+ raw = ENV[config_value.provided.lookup]
70
+ if raw.nil?
71
+ raise Prefab::Errors::MissingEnvVarError.new("Missing environment variable #{config_value.provided.lookup}")
72
+ else
73
+ coerced = coerce_into_type(raw, config, config_value.provided.lookup)
74
+ new(Prefab::ConfigValueWrapper.wrap(coerced, confidential: config_value.confidential), resolver)
75
+ end
76
+ else
77
+ raise "Unknown Provided Source #{config_value.provided.source}"
78
+ end
79
+ else
80
+ new(config_value, resolver)
81
+ end
82
+ end
83
+
84
+ # Don't allow env vars to resolve to a value_type other than the config's value_type
85
+ def self.coerce_into_type(value_string, config, env_var_name)
86
+ case config.value_type
87
+ when :INT then Integer(value_string)
88
+ when :DOUBLE then Float(value_string)
89
+ when :STRING then String(value_string)
90
+ when :STRING_LIST then
91
+ maybe_string_list = YAML.load(value_string)
92
+ case maybe_string_list
93
+ when Array
94
+ maybe_string_list
95
+ else
96
+ raise raise Prefab::Errors::EnvVarParseError.new(value_string, config, env_var_name)
97
+ end
98
+ when :BOOL then
99
+ maybe_bool = YAML.load(value_string)
100
+ case maybe_bool
101
+ when TrueClass, FalseClass
102
+ maybe_bool
103
+ else
104
+ raise Prefab::Errors::EnvVarParseError.new(value_string, config, env_var_name)
105
+ end
106
+ when :NOT_SET_VALUE_TYPE
107
+ YAML.load(value_string)
108
+ else
109
+ raise Prefab::Errors::EnvVarParseError.new(value_string, config, env_var_name)
110
+ end
111
+ rescue ArgumentError
112
+ raise Prefab::Errors::EnvVarParseError.new(value_string, config, env_var_name)
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,18 @@
1
+ module Prefab
2
+ class ConfigValueWrapper
3
+ def self.wrap(value, confidential: nil)
4
+ case value
5
+ when Integer
6
+ PrefabProto::ConfigValue.new(int: value, confidential: confidential)
7
+ when Float
8
+ PrefabProto::ConfigValue.new(double: value, confidential: confidential)
9
+ when TrueClass, FalseClass
10
+ PrefabProto::ConfigValue.new(bool: value, confidential: confidential)
11
+ when Array
12
+ PrefabProto::ConfigValue.new(string_list: PrefabProto::StringList.new(values: value.map(&:to_s)), confidential: confidential)
13
+ else
14
+ PrefabProto::ConfigValue.new(string: value.to_s, confidential: confidential)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ class Context
5
+ BLANK_CONTEXT_NAME = ''
6
+
7
+ class NamedContext
8
+ attr_reader :name
9
+
10
+ def initialize(name, hash)
11
+ @hash = {}
12
+ @name = name.to_s
13
+
14
+ merge!(hash)
15
+ end
16
+
17
+ def get(parts)
18
+ @hash[parts]
19
+ end
20
+
21
+ def merge!(other)
22
+ @hash = @hash.merge(other.transform_keys(&:to_s))
23
+ end
24
+
25
+ def to_h
26
+ @hash
27
+ end
28
+
29
+ def key
30
+ "#{@name}:#{get('key')}"
31
+ end
32
+
33
+ def to_proto
34
+ PrefabProto::Context.new(
35
+ type: name,
36
+ values: to_h.transform_values do |value|
37
+ ConfigValueWrapper.wrap(value)
38
+ end
39
+ )
40
+ end
41
+ end
42
+
43
+ THREAD_KEY = :prefab_context
44
+ attr_reader :contexts, :seen_at
45
+
46
+ class << self
47
+ def current=(context)
48
+ Thread.current[THREAD_KEY] = context
49
+ end
50
+
51
+ def current
52
+ Thread.current[THREAD_KEY] ||= new
53
+ end
54
+
55
+ def with_context(context)
56
+ old_context = Thread.current[THREAD_KEY]
57
+ Thread.current[THREAD_KEY] = new(context)
58
+ yield
59
+ ensure
60
+ Thread.current[THREAD_KEY] = old_context
61
+ end
62
+
63
+ def with_merged_context(context)
64
+ old_context = Thread.current[THREAD_KEY]
65
+ Thread.current[THREAD_KEY] = merge_with_current(context)
66
+ yield
67
+ ensure
68
+ Thread.current[THREAD_KEY] = old_context
69
+ end
70
+
71
+ def clear_current
72
+ Thread.current[THREAD_KEY] = nil
73
+ end
74
+
75
+ def merge_with_current(new_context_properties = {})
76
+ new(current.to_h.merge(new_context_properties.to_h))
77
+ end
78
+ end
79
+
80
+ def initialize(context = {})
81
+ @contexts = {}
82
+ @seen_at = Time.now.utc.to_i
83
+
84
+ if context.is_a?(NamedContext)
85
+ @contexts[context.name] = context
86
+ elsif context.is_a?(Hash)
87
+ context.map do |name, values|
88
+ if values.is_a?(Hash)
89
+ @contexts[name.to_s] = NamedContext.new(name, values)
90
+ else
91
+ warn '[DEPRECATION] Prefab contexts should be a hash with a key of the context name and a value of a hash.'
92
+
93
+ @contexts[BLANK_CONTEXT_NAME] ||= NamedContext.new(BLANK_CONTEXT_NAME, {})
94
+ @contexts[BLANK_CONTEXT_NAME].merge!({ name => values })
95
+ end
96
+ end
97
+ else
98
+ raise ArgumentError, 'must be a Hash or a NamedContext'
99
+ end
100
+ end
101
+
102
+ def blank?
103
+ contexts.empty?
104
+ end
105
+
106
+ def set(name, hash)
107
+ @contexts[name.to_s] = NamedContext.new(name, hash)
108
+ end
109
+
110
+ def get(property_key)
111
+ name, key = property_key.split('.', 2)
112
+
113
+ if key.nil?
114
+ name = BLANK_CONTEXT_NAME
115
+ key = property_key
116
+ end
117
+
118
+ contexts[name]&.get(key)
119
+ end
120
+
121
+ def to_h
122
+ contexts.transform_values(&:to_h)
123
+ end
124
+
125
+ def clear
126
+ @contexts = {}
127
+ end
128
+
129
+ def context(name)
130
+ contexts[name.to_s] || NamedContext.new(name, {})
131
+ end
132
+
133
+ def merge_default(defaults)
134
+ defaults.keys.each do |name|
135
+ set(name, context(name).merge!(defaults[name]))
136
+ end
137
+
138
+ self
139
+ end
140
+
141
+ def to_proto(namespace)
142
+ prefab_context = {
143
+ 'current-time' => ConfigValueWrapper.wrap(Prefab::TimeHelpers.now_in_ms)
144
+ }
145
+
146
+ prefab_context['namespace'] = ConfigValueWrapper.wrap(namespace) if namespace&.length&.positive?
147
+
148
+ PrefabProto::ContextSet.new(
149
+ contexts: contexts.map do |name, context|
150
+ context.to_proto
151
+ end.concat([PrefabProto::Context.new(type: 'prefab',
152
+ values: prefab_context)])
153
+ )
154
+ end
155
+
156
+ def slim_proto
157
+ PrefabProto::ContextSet.new(
158
+ contexts: contexts.map do |_, context|
159
+ context.to_proto
160
+ end
161
+ )
162
+ end
163
+
164
+ def grouped_key
165
+ contexts.map do |_, context|
166
+ context.key
167
+ end.sort.join('|')
168
+ end
169
+
170
+ include Comparable
171
+ def <=>(other)
172
+ if other.is_a?(Prefab::Context)
173
+ to_h <=> other.to_h
174
+ else
175
+ super
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,20 @@
1
+ module Prefab
2
+ class ContextShape
3
+ MAPPING = {
4
+ Integer => 1,
5
+ String => 2,
6
+ Float => 4,
7
+ TrueClass => 5,
8
+ FalseClass => 5,
9
+ Array => 10,
10
+ }.freeze
11
+
12
+ # We default to String if the type isn't a primitive we support.
13
+ # This is because we do a `to_s` in the CriteriaEvaluator.
14
+ DEFAULT = MAPPING[String]
15
+
16
+ def self.field_type_number(value)
17
+ MAPPING.fetch(value.class, DEFAULT)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'periodic_sync'
4
+
5
+ module Prefab
6
+ class ContextShapeAggregator
7
+ include Prefab::PeriodicSync
8
+
9
+ LOG = Prefab::InternalLogger.new(ContextShapeAggregator)
10
+
11
+ attr_reader :data
12
+
13
+ def initialize(client:, max_shapes:, sync_interval:)
14
+ @max_shapes = max_shapes
15
+ @client = client
16
+ @name = 'context_shape_aggregator'
17
+
18
+ @data = Concurrent::Set.new
19
+
20
+ start_periodic_sync(sync_interval)
21
+ end
22
+
23
+ def push(context)
24
+ return if @data.size >= @max_shapes
25
+
26
+ context.contexts.each_pair do |name, name_context|
27
+ name_context.to_h.each_pair do |key, value|
28
+ @data.add [name, key, Prefab::ContextShape.field_type_number(value)]
29
+ end
30
+ end
31
+ end
32
+
33
+ def prepare_data
34
+ duped = @data.dup
35
+ @data.clear
36
+
37
+ duped.inject({}) do |acc, (name, key, type)|
38
+ acc[name] ||= {}
39
+ acc[name][key] = type
40
+ acc
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def flush(to_ship, _)
47
+ pool.post do
48
+ LOG.debug "Uploading context shapes for #{to_ship.values.size}"
49
+
50
+ shapes = PrefabProto::ContextShapes.new(
51
+ shapes: to_ship.map do |name, shape|
52
+ PrefabProto::ContextShape.new(
53
+ name: name,
54
+ field_types: shape
55
+ )
56
+ end
57
+ )
58
+
59
+ result = post('/api/v1/context-shapes', shapes)
60
+
61
+ LOG.debug "Uploaded #{to_ship.values.size} shapes: #{result.status}"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Naming/MethodName
4
+ # We're intentionally keeping the UPCASED method names to match the protobuf
5
+ # and avoid wasting CPU cycles lowercasing things
6
+ module Prefab
7
+ # This class evaluates a config's criteria. `evaluate` returns the value of
8
+ # the first match based on the provided properties.
9
+ class CriteriaEvaluator
10
+ LOG = Prefab::InternalLogger.new(CriteriaEvaluator)
11
+ NAMESPACE_KEY = 'NAMESPACE'
12
+ NO_MATCHING_ROWS = [].freeze
13
+
14
+ def initialize(config, project_env_id:, resolver:, namespace:, base_client:)
15
+ @config = config
16
+ @project_env_id = project_env_id
17
+ @resolver = resolver
18
+ @namespace = namespace
19
+ @base_client = base_client
20
+ end
21
+
22
+ def evaluate(properties)
23
+ rtn = evaluate_for_env(@project_env_id, properties) ||
24
+ evaluate_for_env(0, properties)
25
+ LOG.debug "Eval Key #{@config.key} Result #{rtn&.reportable_value} with #{properties.to_h}" unless @config.config_type == :LOG_LEVEL
26
+ rtn
27
+ end
28
+
29
+ def all_criteria_match?(conditional_value, props)
30
+ conditional_value.criteria.all? do |criterion|
31
+ public_send(criterion.operator, criterion, props)
32
+ end
33
+ end
34
+
35
+ def IN_SEG(criterion, properties)
36
+ in_segment?(criterion, properties)
37
+ end
38
+
39
+ def NOT_IN_SEG(criterion, properties)
40
+ !in_segment?(criterion, properties)
41
+ end
42
+
43
+ def ALWAYS_TRUE(_criterion, _properties)
44
+ true
45
+ end
46
+
47
+ def PROP_IS_ONE_OF(criterion, properties)
48
+ matches?(criterion, value_from_properties(criterion, properties), properties)
49
+ end
50
+
51
+ def PROP_IS_NOT_ONE_OF(criterion, properties)
52
+ !matches?(criterion, value_from_properties(criterion, properties), properties)
53
+ end
54
+
55
+ def PROP_ENDS_WITH_ONE_OF(criterion, properties)
56
+ prop_ends_with_one_of?(criterion, value_from_properties(criterion, properties))
57
+ end
58
+
59
+ def PROP_DOES_NOT_END_WITH_ONE_OF(criterion, properties)
60
+ !prop_ends_with_one_of?(criterion, value_from_properties(criterion, properties))
61
+ end
62
+
63
+ def HIERARCHICAL_MATCH(criterion, properties)
64
+ value = value_from_properties(criterion, properties)
65
+ value&.start_with?(criterion.value_to_match.string)
66
+ end
67
+
68
+ def IN_INT_RANGE(criterion, properties)
69
+ value = if criterion.property_name == 'prefab.current-time'
70
+ Time.now.utc.to_i * 1000
71
+ else
72
+ value_from_properties(criterion, properties)
73
+ end
74
+
75
+ value && value >= criterion.value_to_match.int_range.start && value < criterion.value_to_match.int_range.end
76
+ end
77
+
78
+ def value_from_properties(criterion, properties)
79
+ criterion.property_name == NAMESPACE_KEY ? @namespace : properties.get(criterion.property_name)
80
+ end
81
+
82
+ private
83
+
84
+ def evaluate_for_env(env_id, properties)
85
+ @config.rows.each_with_index do |row, index|
86
+ next unless row.project_env_id == env_id
87
+
88
+ row.values.each_with_index do |conditional_value, value_index|
89
+ next unless all_criteria_match?(conditional_value, properties)
90
+
91
+ return Prefab::Evaluation.new(
92
+ config: @config,
93
+ value: conditional_value.value,
94
+ value_index: value_index,
95
+ config_row_index: index,
96
+ context: properties,
97
+ resolver: @resolver
98
+ )
99
+ end
100
+ end
101
+
102
+ nil
103
+ end
104
+
105
+ def in_segment?(criterion, properties)
106
+ segment = @resolver.get(criterion.value_to_match.string, properties)
107
+
108
+ @base_client.log.info("Segment #{criterion.value_to_match.string} not found") unless segment
109
+
110
+ segment&.report_and_return(@base_client.evaluation_summary_aggregator)
111
+ end
112
+
113
+ def matches?(criterion, value, properties)
114
+ criterion_value_or_values = Prefab::ConfigValueUnwrapper.deepest_value(criterion.value_to_match, @config.key,
115
+ properties, @resolver).unwrap
116
+
117
+ case criterion_value_or_values
118
+ when Google::Protobuf::RepeatedField
119
+ # we to_s the value from properties for comparison because the
120
+ # criterion_value_or_values is a list of strings
121
+ criterion_value_or_values.include?(value.to_s)
122
+ else
123
+ criterion_value_or_values == value
124
+ end
125
+ end
126
+
127
+ def prop_ends_with_one_of?(criterion, value)
128
+ return false unless value
129
+
130
+ criterion.value_to_match.string_list.values.any? do |ending|
131
+ value.end_with?(ending)
132
+ end
133
+ end
134
+ end
135
+ end
136
+ # rubocop:enable Naming/MethodName
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ class Encryption
5
+ CIPHER_TYPE = "aes-256-gcm" # 32/12
6
+ SEPARATOR = "--"
7
+
8
+ # Hexadecimal format ensures that generated keys are representable with
9
+ # plain text
10
+ #
11
+ # To convert back to the original string with the desired length:
12
+ # [ value ].pack("H*")
13
+ def self.generate_new_hex_key
14
+ generate_random_key.unpack("H*")[0]
15
+ end
16
+
17
+ def initialize(key_string_hex)
18
+ @key = [key_string_hex].pack("H*")
19
+ end
20
+
21
+ def encrypt(clear_text)
22
+ cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
23
+ cipher.encrypt
24
+ iv = cipher.random_iv
25
+
26
+ # load them into the cipher
27
+ cipher.key = @key
28
+ cipher.iv = iv
29
+ cipher.auth_data = ""
30
+
31
+ # encrypt the message
32
+ encrypted = cipher.update(clear_text)
33
+ encrypted << cipher.final
34
+ tag = cipher.auth_tag
35
+
36
+ # pack and join
37
+ [encrypted, iv, tag].map { |p| p.unpack("H*")[0] }.join(SEPARATOR)
38
+ end
39
+
40
+ def decrypt(encrypted_string)
41
+ unpacked_parts = encrypted_string.split(SEPARATOR).map { |p| [p].pack("H*") }
42
+
43
+ cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
44
+ cipher.decrypt
45
+ cipher.key = @key
46
+ cipher.iv = unpacked_parts[1]
47
+ cipher.auth_tag = unpacked_parts[2]
48
+
49
+ # and decrypt it
50
+ decrypted = cipher.update(unpacked_parts[0])
51
+ decrypted << cipher.final
52
+ decrypted
53
+ end
54
+
55
+ private
56
+
57
+ def self.generate_random_key
58
+ SecureRandom.random_bytes(key_length)
59
+ end
60
+
61
+ def self.key_length
62
+ OpenSSL::Cipher.new(CIPHER_TYPE).key_len
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ class Error < StandardError
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ module Errors
5
+ class EnvVarParseError < Prefab::Error
6
+ def initialize(env_var, config, env_var_name)
7
+ super("Evaluating #{config.key} couldn't coerce #{env_var_name} of #{env_var} to #{config.value_type}")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ module Errors
5
+ class InitializationTimeoutError < Prefab::Error
6
+ def initialize(timeout_sec, key)
7
+ message = "Prefab couldn't initialize in #{timeout_sec} second timeout. Trying to fetch key `#{key}`."
8
+
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ module Errors
5
+ class InvalidApiKeyError < Prefab::Error
6
+ def initialize(key)
7
+ if key.nil? || key.empty?
8
+ message = 'No API key. Set PREFAB_API_KEY env var or use PREFAB_DATASOURCES=LOCAL_ONLY'
9
+
10
+ super(message)
11
+ else
12
+ message = "Your API key format is invalid. Expecting something like 123-development-yourapikey-SDK. You provided `#{key}`"
13
+
14
+ super(message)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ module Errors
5
+ class MissingDefaultError < Prefab::Error
6
+ def initialize(key)
7
+ message = "No value found for key '#{key}' and no default was provided.\n\nIf you'd prefer returning `nil` rather than raising when this occurs, modify the `on_no_default` value you provide in your Prefab::Options."
8
+
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ module Errors
5
+ class MissingEnvVarError < Prefab::Error
6
+ def initialize(message)
7
+ super(message)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ module Errors
5
+ class UninitializedError < Prefab::Error
6
+ def initialize(key=nil)
7
+ message = "Use Prefab.initialize before calling Prefab.get #{key}"
8
+
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end