flagkit 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +196 -0
- data/lib/flagkit/client.rb +443 -0
- data/lib/flagkit/core/cache.rb +162 -0
- data/lib/flagkit/core/encrypted_cache.rb +227 -0
- data/lib/flagkit/core/event_persistence.rb +513 -0
- data/lib/flagkit/core/event_queue.rb +190 -0
- data/lib/flagkit/core/polling_manager.rb +112 -0
- data/lib/flagkit/core/streaming_manager.rb +469 -0
- data/lib/flagkit/error/error_code.rb +98 -0
- data/lib/flagkit/error/error_sanitizer.rb +48 -0
- data/lib/flagkit/error/flagkit_error.rb +95 -0
- data/lib/flagkit/http/circuit_breaker.rb +145 -0
- data/lib/flagkit/http/http_client.rb +312 -0
- data/lib/flagkit/options.rb +222 -0
- data/lib/flagkit/types/evaluation_context.rb +121 -0
- data/lib/flagkit/types/evaluation_reason.rb +22 -0
- data/lib/flagkit/types/evaluation_result.rb +77 -0
- data/lib/flagkit/types/flag_state.rb +100 -0
- data/lib/flagkit/types/flag_type.rb +46 -0
- data/lib/flagkit/utils/security.rb +528 -0
- data/lib/flagkit/utils/version.rb +116 -0
- data/lib/flagkit/version.rb +5 -0
- data/lib/flagkit.rb +166 -0
- metadata +200 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlagKit
|
|
4
|
+
# Configuration options for the FlagKit SDK.
|
|
5
|
+
class Options
|
|
6
|
+
DEFAULT_POLLING_INTERVAL = 30
|
|
7
|
+
DEFAULT_CACHE_TTL = 300
|
|
8
|
+
DEFAULT_MAX_CACHE_SIZE = 1000
|
|
9
|
+
DEFAULT_EVENT_BATCH_SIZE = 10
|
|
10
|
+
DEFAULT_EVENT_FLUSH_INTERVAL = 30
|
|
11
|
+
DEFAULT_TIMEOUT = 10
|
|
12
|
+
DEFAULT_RETRY_ATTEMPTS = 3
|
|
13
|
+
DEFAULT_CIRCUIT_BREAKER_THRESHOLD = 5
|
|
14
|
+
DEFAULT_CIRCUIT_BREAKER_RESET_TIMEOUT = 30
|
|
15
|
+
DEFAULT_KEY_ROTATION_GRACE_PERIOD = 300
|
|
16
|
+
DEFAULT_MAX_PERSISTED_EVENTS = 10_000
|
|
17
|
+
DEFAULT_PERSISTENCE_FLUSH_INTERVAL = 1000
|
|
18
|
+
DEFAULT_EVALUATION_JITTER_ENABLED = false
|
|
19
|
+
DEFAULT_EVALUATION_JITTER_MIN_MS = 5
|
|
20
|
+
DEFAULT_EVALUATION_JITTER_MAX_MS = 15
|
|
21
|
+
DEFAULT_BOOTSTRAP_VERIFICATION_ENABLED = true
|
|
22
|
+
DEFAULT_BOOTSTRAP_VERIFICATION_MAX_AGE = 86_400_000 # 24 hours in milliseconds
|
|
23
|
+
DEFAULT_BOOTSTRAP_VERIFICATION_ON_FAILURE = "warn"
|
|
24
|
+
DEFAULT_ERROR_SANITIZATION_ENABLED = true
|
|
25
|
+
DEFAULT_ERROR_SANITIZATION_PRESERVE_ORIGINAL = false
|
|
26
|
+
|
|
27
|
+
attr_reader :api_key,
|
|
28
|
+
:polling_interval,
|
|
29
|
+
:cache_ttl,
|
|
30
|
+
:max_cache_size,
|
|
31
|
+
:cache_enabled,
|
|
32
|
+
:event_batch_size,
|
|
33
|
+
:event_flush_interval,
|
|
34
|
+
:events_enabled,
|
|
35
|
+
:timeout,
|
|
36
|
+
:retry_attempts,
|
|
37
|
+
:circuit_breaker_threshold,
|
|
38
|
+
:circuit_breaker_reset_timeout,
|
|
39
|
+
:bootstrap,
|
|
40
|
+
:logger,
|
|
41
|
+
:storage,
|
|
42
|
+
:local_port,
|
|
43
|
+
:secondary_api_key,
|
|
44
|
+
:key_rotation_grace_period,
|
|
45
|
+
:strict_pii_mode,
|
|
46
|
+
:enable_request_signing,
|
|
47
|
+
:encrypt_cache,
|
|
48
|
+
:persist_events,
|
|
49
|
+
:event_storage_path,
|
|
50
|
+
:max_persisted_events,
|
|
51
|
+
:persistence_flush_interval,
|
|
52
|
+
:evaluation_jitter_enabled,
|
|
53
|
+
:evaluation_jitter_min_ms,
|
|
54
|
+
:evaluation_jitter_max_ms,
|
|
55
|
+
:bootstrap_verification_enabled,
|
|
56
|
+
:bootstrap_verification_max_age,
|
|
57
|
+
:bootstrap_verification_on_failure,
|
|
58
|
+
:error_sanitization_enabled,
|
|
59
|
+
:error_sanitization_preserve_original,
|
|
60
|
+
:on_usage_update,
|
|
61
|
+
:on_subscription_error,
|
|
62
|
+
:on_connection_limit_error
|
|
63
|
+
|
|
64
|
+
# @param api_key [String] The API key
|
|
65
|
+
# @param polling_interval [Integer] Polling interval in seconds
|
|
66
|
+
# @param cache_ttl [Integer] Cache TTL in seconds
|
|
67
|
+
# @param max_cache_size [Integer] Maximum cache size
|
|
68
|
+
# @param cache_enabled [Boolean] Whether caching is enabled
|
|
69
|
+
# @param event_batch_size [Integer] Event batch size
|
|
70
|
+
# @param event_flush_interval [Integer] Event flush interval in seconds
|
|
71
|
+
# @param events_enabled [Boolean] Whether events are enabled
|
|
72
|
+
# @param timeout [Integer] Request timeout in seconds
|
|
73
|
+
# @param retry_attempts [Integer] Number of retry attempts
|
|
74
|
+
# @param circuit_breaker_threshold [Integer] Circuit breaker failure threshold
|
|
75
|
+
# @param circuit_breaker_reset_timeout [Integer] Circuit breaker reset timeout in seconds
|
|
76
|
+
# @param bootstrap [Hash, nil] Bootstrap data
|
|
77
|
+
# @param logger [Object, nil] Logger instance
|
|
78
|
+
# @param storage [Object, nil] Storage adapter
|
|
79
|
+
# @param local_port [Integer, nil] Local development server port (uses http://localhost:{port}/api/v1)
|
|
80
|
+
# @param secondary_api_key [String, nil] Secondary API key for key rotation
|
|
81
|
+
# @param key_rotation_grace_period [Integer] Grace period in seconds during key rotation
|
|
82
|
+
# @param strict_pii_mode [Boolean] Raise SecurityError instead of warning when PII detected
|
|
83
|
+
# @param enable_request_signing [Boolean] Enable HMAC-SHA256 request signing for POST requests
|
|
84
|
+
# @param encrypt_cache [Boolean] Enable AES-256-GCM encryption for cached data
|
|
85
|
+
# @param persist_events [Boolean] Enable crash-resilient event persistence
|
|
86
|
+
# @param event_storage_path [String, nil] Directory for event storage (defaults to OS temp dir)
|
|
87
|
+
# @param max_persisted_events [Integer] Maximum events to persist
|
|
88
|
+
# @param persistence_flush_interval [Integer] Milliseconds between disk writes
|
|
89
|
+
# @param evaluation_jitter_enabled [Boolean] Enable timing jitter for cache timing attack protection
|
|
90
|
+
# @param evaluation_jitter_min_ms [Integer] Minimum jitter delay in milliseconds
|
|
91
|
+
# @param evaluation_jitter_max_ms [Integer] Maximum jitter delay in milliseconds
|
|
92
|
+
# @param bootstrap_verification_enabled [Boolean] Enable HMAC-SHA256 signature verification for bootstrap data
|
|
93
|
+
# @param bootstrap_verification_max_age [Integer] Maximum age in milliseconds for bootstrap data
|
|
94
|
+
# @param bootstrap_verification_on_failure [String] Action on verification failure: 'warn', 'error', or 'ignore'
|
|
95
|
+
# @param error_sanitization_enabled [Boolean] Enable sanitization of sensitive info from error messages
|
|
96
|
+
# @param error_sanitization_preserve_original [Boolean] Preserve original message in addition to sanitized
|
|
97
|
+
# @param on_usage_update [Proc, nil] Callback for usage metrics updates (receives UsageMetrics)
|
|
98
|
+
# @param on_subscription_error [Proc, nil] Callback when subscription error occurs in streaming (receives message)
|
|
99
|
+
# @param on_connection_limit_error [Proc, nil] Callback when connection limit is reached in streaming
|
|
100
|
+
def initialize(
|
|
101
|
+
api_key:,
|
|
102
|
+
polling_interval: DEFAULT_POLLING_INTERVAL,
|
|
103
|
+
cache_ttl: DEFAULT_CACHE_TTL,
|
|
104
|
+
max_cache_size: DEFAULT_MAX_CACHE_SIZE,
|
|
105
|
+
cache_enabled: true,
|
|
106
|
+
event_batch_size: DEFAULT_EVENT_BATCH_SIZE,
|
|
107
|
+
event_flush_interval: DEFAULT_EVENT_FLUSH_INTERVAL,
|
|
108
|
+
events_enabled: true,
|
|
109
|
+
timeout: DEFAULT_TIMEOUT,
|
|
110
|
+
retry_attempts: DEFAULT_RETRY_ATTEMPTS,
|
|
111
|
+
circuit_breaker_threshold: DEFAULT_CIRCUIT_BREAKER_THRESHOLD,
|
|
112
|
+
circuit_breaker_reset_timeout: DEFAULT_CIRCUIT_BREAKER_RESET_TIMEOUT,
|
|
113
|
+
bootstrap: nil,
|
|
114
|
+
logger: nil,
|
|
115
|
+
storage: nil,
|
|
116
|
+
local_port: nil,
|
|
117
|
+
secondary_api_key: nil,
|
|
118
|
+
key_rotation_grace_period: DEFAULT_KEY_ROTATION_GRACE_PERIOD,
|
|
119
|
+
strict_pii_mode: false,
|
|
120
|
+
enable_request_signing: true,
|
|
121
|
+
encrypt_cache: false,
|
|
122
|
+
persist_events: false,
|
|
123
|
+
event_storage_path: nil,
|
|
124
|
+
max_persisted_events: DEFAULT_MAX_PERSISTED_EVENTS,
|
|
125
|
+
persistence_flush_interval: DEFAULT_PERSISTENCE_FLUSH_INTERVAL,
|
|
126
|
+
evaluation_jitter_enabled: DEFAULT_EVALUATION_JITTER_ENABLED,
|
|
127
|
+
evaluation_jitter_min_ms: DEFAULT_EVALUATION_JITTER_MIN_MS,
|
|
128
|
+
evaluation_jitter_max_ms: DEFAULT_EVALUATION_JITTER_MAX_MS,
|
|
129
|
+
bootstrap_verification_enabled: DEFAULT_BOOTSTRAP_VERIFICATION_ENABLED,
|
|
130
|
+
bootstrap_verification_max_age: DEFAULT_BOOTSTRAP_VERIFICATION_MAX_AGE,
|
|
131
|
+
bootstrap_verification_on_failure: DEFAULT_BOOTSTRAP_VERIFICATION_ON_FAILURE,
|
|
132
|
+
error_sanitization_enabled: DEFAULT_ERROR_SANITIZATION_ENABLED,
|
|
133
|
+
error_sanitization_preserve_original: DEFAULT_ERROR_SANITIZATION_PRESERVE_ORIGINAL,
|
|
134
|
+
on_usage_update: nil,
|
|
135
|
+
on_subscription_error: nil,
|
|
136
|
+
on_connection_limit_error: nil
|
|
137
|
+
)
|
|
138
|
+
@api_key = api_key
|
|
139
|
+
@polling_interval = polling_interval
|
|
140
|
+
@cache_ttl = cache_ttl
|
|
141
|
+
@max_cache_size = max_cache_size
|
|
142
|
+
@cache_enabled = cache_enabled
|
|
143
|
+
@event_batch_size = event_batch_size
|
|
144
|
+
@event_flush_interval = event_flush_interval
|
|
145
|
+
@events_enabled = events_enabled
|
|
146
|
+
@timeout = timeout
|
|
147
|
+
@retry_attempts = retry_attempts
|
|
148
|
+
@circuit_breaker_threshold = circuit_breaker_threshold
|
|
149
|
+
@circuit_breaker_reset_timeout = circuit_breaker_reset_timeout
|
|
150
|
+
@bootstrap = bootstrap
|
|
151
|
+
@logger = logger
|
|
152
|
+
@storage = storage
|
|
153
|
+
@local_port = local_port
|
|
154
|
+
@secondary_api_key = secondary_api_key
|
|
155
|
+
@key_rotation_grace_period = key_rotation_grace_period
|
|
156
|
+
@strict_pii_mode = strict_pii_mode
|
|
157
|
+
@enable_request_signing = enable_request_signing
|
|
158
|
+
@encrypt_cache = encrypt_cache
|
|
159
|
+
@persist_events = persist_events
|
|
160
|
+
@event_storage_path = event_storage_path || default_event_storage_path
|
|
161
|
+
@max_persisted_events = max_persisted_events
|
|
162
|
+
@persistence_flush_interval = persistence_flush_interval
|
|
163
|
+
@evaluation_jitter_enabled = evaluation_jitter_enabled
|
|
164
|
+
@evaluation_jitter_min_ms = evaluation_jitter_min_ms
|
|
165
|
+
@evaluation_jitter_max_ms = evaluation_jitter_max_ms
|
|
166
|
+
@bootstrap_verification_enabled = bootstrap_verification_enabled
|
|
167
|
+
@bootstrap_verification_max_age = bootstrap_verification_max_age
|
|
168
|
+
@bootstrap_verification_on_failure = bootstrap_verification_on_failure
|
|
169
|
+
@error_sanitization_enabled = error_sanitization_enabled
|
|
170
|
+
@error_sanitization_preserve_original = error_sanitization_preserve_original
|
|
171
|
+
@on_usage_update = on_usage_update
|
|
172
|
+
@on_subscription_error = on_subscription_error
|
|
173
|
+
@on_connection_limit_error = on_connection_limit_error
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Validates the options.
|
|
177
|
+
#
|
|
178
|
+
# @raise [Error] If validation fails
|
|
179
|
+
# @raise [SecurityError] If local_port is used in production
|
|
180
|
+
def validate!
|
|
181
|
+
validate_api_key!
|
|
182
|
+
validate_positive_integers!
|
|
183
|
+
validate_local_port_restriction!
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def validate_local_port_restriction!
|
|
189
|
+
return unless local_port
|
|
190
|
+
|
|
191
|
+
env = ENV.fetch("RACK_ENV", ENV.fetch("RAILS_ENV", nil))
|
|
192
|
+
return unless env == "production"
|
|
193
|
+
|
|
194
|
+
raise SecurityError.new(
|
|
195
|
+
ErrorCode::SECURITY_LOCAL_PORT_IN_PRODUCTION,
|
|
196
|
+
"local_port cannot be used in production environment. " \
|
|
197
|
+
"This is a security risk as it bypasses HTTPS and may expose traffic to interception."
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def validate_api_key!
|
|
202
|
+
raise Error.config_error(ErrorCode::CONFIG_INVALID_API_KEY, "API key is required") if api_key.nil? || api_key.empty?
|
|
203
|
+
|
|
204
|
+
unless api_key.start_with?("sdk_", "srv_", "cli_")
|
|
205
|
+
raise Error.config_error(ErrorCode::CONFIG_INVALID_API_KEY, "Invalid API key format")
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def validate_positive_integers!
|
|
210
|
+
if polling_interval <= 0
|
|
211
|
+
raise Error.config_error(ErrorCode::CONFIG_INVALID_POLLING_INTERVAL, "Polling interval must be positive")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
raise Error.config_error(ErrorCode::CONFIG_INVALID_CACHE_TTL, "Cache TTL must be positive") if cache_ttl <= 0
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def default_event_storage_path
|
|
218
|
+
require "tmpdir"
|
|
219
|
+
File.join(Dir.tmpdir, "flagkit", "events")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlagKit
|
|
4
|
+
module Types
|
|
5
|
+
# Context for flag evaluation containing user and custom attributes.
|
|
6
|
+
class EvaluationContext
|
|
7
|
+
PRIVATE_ATTRIBUTE_PREFIX = "_"
|
|
8
|
+
|
|
9
|
+
attr_reader :user_id, :attributes
|
|
10
|
+
|
|
11
|
+
# @param user_id [String, nil] The user identifier
|
|
12
|
+
# @param attributes [Hash] Custom attributes
|
|
13
|
+
def initialize(user_id: nil, **attributes)
|
|
14
|
+
@user_id = user_id
|
|
15
|
+
@attributes = attributes.transform_keys(&:to_s)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Creates a new context with the given user ID.
|
|
19
|
+
#
|
|
20
|
+
# @param user_id [String] The user ID
|
|
21
|
+
# @return [EvaluationContext]
|
|
22
|
+
def with_user_id(user_id)
|
|
23
|
+
self.class.new(user_id: user_id, **attributes)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Creates a new context with the given attribute added.
|
|
27
|
+
#
|
|
28
|
+
# @param key [String, Symbol] The attribute key
|
|
29
|
+
# @param value [Object] The attribute value
|
|
30
|
+
# @return [EvaluationContext]
|
|
31
|
+
def with_attribute(key, value)
|
|
32
|
+
new_attrs = attributes.merge(key.to_s => value)
|
|
33
|
+
self.class.new(user_id: user_id, **new_attrs)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Creates a new context with multiple attributes added.
|
|
37
|
+
#
|
|
38
|
+
# @param attrs [Hash] The attributes to add
|
|
39
|
+
# @return [EvaluationContext]
|
|
40
|
+
def with_attributes(attrs)
|
|
41
|
+
new_attrs = attributes.merge(attrs.transform_keys(&:to_s))
|
|
42
|
+
self.class.new(user_id: user_id, **new_attrs)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Merges another context into this one.
|
|
46
|
+
# The other context takes precedence.
|
|
47
|
+
#
|
|
48
|
+
# @param other [EvaluationContext, nil] The other context
|
|
49
|
+
# @return [EvaluationContext]
|
|
50
|
+
def merge(other)
|
|
51
|
+
return self unless other
|
|
52
|
+
|
|
53
|
+
new_user_id = other.user_id || user_id
|
|
54
|
+
new_attrs = attributes.merge(other.attributes)
|
|
55
|
+
self.class.new(user_id: new_user_id, **new_attrs)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Creates a copy with private attributes stripped.
|
|
59
|
+
#
|
|
60
|
+
# @return [EvaluationContext]
|
|
61
|
+
def strip_private_attributes
|
|
62
|
+
public_attrs = attributes.reject { |key, _| key.start_with?(PRIVATE_ATTRIBUTE_PREFIX) }
|
|
63
|
+
self.class.new(user_id: user_id, **public_attrs)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Checks if the context is empty.
|
|
67
|
+
#
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def empty?
|
|
70
|
+
user_id.nil? && attributes.empty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Gets an attribute value.
|
|
74
|
+
#
|
|
75
|
+
# @param key [String, Symbol] The attribute key
|
|
76
|
+
# @return [Object, nil]
|
|
77
|
+
def [](key)
|
|
78
|
+
attributes[key.to_s]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Converts the context to a hash for API requests.
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash]
|
|
84
|
+
def to_h
|
|
85
|
+
result = {}
|
|
86
|
+
result["userId"] = user_id if user_id
|
|
87
|
+
result["attributes"] = attributes unless attributes.empty?
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Creates a context from a hash.
|
|
92
|
+
#
|
|
93
|
+
# @param data [Hash] The data hash
|
|
94
|
+
# @return [EvaluationContext]
|
|
95
|
+
def self.from_hash(data)
|
|
96
|
+
return new if data.nil?
|
|
97
|
+
|
|
98
|
+
user_id = data["userId"] || data["user_id"] || data[:user_id]
|
|
99
|
+
attrs = data["attributes"] || data[:attributes] || {}
|
|
100
|
+
new(user_id: user_id, **attrs)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def ==(other)
|
|
104
|
+
return false unless other.is_a?(EvaluationContext)
|
|
105
|
+
|
|
106
|
+
user_id == other.user_id && attributes == other.attributes
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def eql?(other)
|
|
110
|
+
self == other
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def hash
|
|
114
|
+
[user_id, attributes].hash
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Alias for backward compatibility
|
|
120
|
+
EvaluationContext = Types::EvaluationContext
|
|
121
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlagKit
|
|
4
|
+
module Types
|
|
5
|
+
# Reasons for flag evaluation results.
|
|
6
|
+
module EvaluationReason
|
|
7
|
+
CACHED = "CACHED"
|
|
8
|
+
DEFAULT = "DEFAULT"
|
|
9
|
+
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
|
|
10
|
+
BOOTSTRAP = "BOOTSTRAP"
|
|
11
|
+
SERVER = "SERVER"
|
|
12
|
+
STALE_CACHE = "STALE_CACHE"
|
|
13
|
+
ERROR = "ERROR"
|
|
14
|
+
DISABLED = "DISABLED"
|
|
15
|
+
TYPE_MISMATCH = "TYPE_MISMATCH"
|
|
16
|
+
OFFLINE = "OFFLINE"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Alias for backward compatibility
|
|
21
|
+
EvaluationReason = Types::EvaluationReason
|
|
22
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlagKit
|
|
4
|
+
module Types
|
|
5
|
+
# Result of evaluating a feature flag.
|
|
6
|
+
class EvaluationResult
|
|
7
|
+
attr_reader :flag_key, :value, :enabled, :reason, :version, :timestamp
|
|
8
|
+
|
|
9
|
+
# @param flag_key [String] The flag key
|
|
10
|
+
# @param value [Object] The evaluated value
|
|
11
|
+
# @param enabled [Boolean] Whether the flag is enabled
|
|
12
|
+
# @param reason [String] The evaluation reason
|
|
13
|
+
# @param version [Integer] The flag version
|
|
14
|
+
# @param timestamp [Time] When the evaluation occurred
|
|
15
|
+
def initialize(flag_key:, value:, enabled: false, reason: EvaluationReason::DEFAULT, version: 0, timestamp: nil)
|
|
16
|
+
@flag_key = flag_key
|
|
17
|
+
@value = value
|
|
18
|
+
@enabled = enabled
|
|
19
|
+
@reason = reason
|
|
20
|
+
@version = version
|
|
21
|
+
@timestamp = timestamp || Time.now
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Boolean] The value as a boolean
|
|
25
|
+
def boolean_value
|
|
26
|
+
value == true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [String, nil] The value as a string
|
|
30
|
+
def string_value
|
|
31
|
+
value&.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Float] The value as a float
|
|
35
|
+
def number_value
|
|
36
|
+
value.is_a?(Numeric) ? value.to_f : 0.0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Integer] The value as an integer
|
|
40
|
+
def int_value
|
|
41
|
+
value.is_a?(Numeric) ? value.to_i : 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Hash, nil] The value as a hash
|
|
45
|
+
def json_value
|
|
46
|
+
value.is_a?(Hash) ? value : nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Creates a default result.
|
|
50
|
+
#
|
|
51
|
+
# @param key [String] The flag key
|
|
52
|
+
# @param default_value [Object] The default value
|
|
53
|
+
# @param reason [String] The reason
|
|
54
|
+
# @return [EvaluationResult]
|
|
55
|
+
def self.default_result(key, default_value, reason)
|
|
56
|
+
new(flag_key: key, value: default_value, enabled: false, reason: reason)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Converts the result to a hash.
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash]
|
|
62
|
+
def to_h
|
|
63
|
+
{
|
|
64
|
+
flag_key: flag_key,
|
|
65
|
+
value: value,
|
|
66
|
+
enabled: enabled,
|
|
67
|
+
reason: reason,
|
|
68
|
+
version: version,
|
|
69
|
+
timestamp: timestamp.iso8601
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Alias for backward compatibility
|
|
76
|
+
EvaluationResult = Types::EvaluationResult
|
|
77
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlagKit
|
|
4
|
+
module Types
|
|
5
|
+
# Represents the state of a feature flag.
|
|
6
|
+
class FlagState
|
|
7
|
+
attr_reader :key, :value, :enabled, :version, :flag_type, :last_modified, :metadata
|
|
8
|
+
|
|
9
|
+
# @param key [String] The flag key
|
|
10
|
+
# @param value [Object] The flag value
|
|
11
|
+
# @param enabled [Boolean] Whether the flag is enabled
|
|
12
|
+
# @param version [Integer] The flag version
|
|
13
|
+
# @param flag_type [String] The flag type
|
|
14
|
+
# @param last_modified [String, nil] ISO 8601 timestamp
|
|
15
|
+
# @param metadata [Hash, nil] Additional metadata
|
|
16
|
+
def initialize(key:, value:, enabled: true, version: 0, flag_type: nil, last_modified: nil, metadata: nil)
|
|
17
|
+
@key = key
|
|
18
|
+
@value = value
|
|
19
|
+
@enabled = enabled
|
|
20
|
+
@version = version
|
|
21
|
+
@flag_type = flag_type || FlagType.infer(value)
|
|
22
|
+
@last_modified = last_modified || Time.now.utc.iso8601
|
|
23
|
+
@metadata = metadata || {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Boolean] The value as a boolean
|
|
27
|
+
def boolean_value
|
|
28
|
+
value == true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [String, nil] The value as a string
|
|
32
|
+
def string_value
|
|
33
|
+
value&.to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Float] The value as a float
|
|
37
|
+
def number_value
|
|
38
|
+
value.is_a?(Numeric) ? value.to_f : 0.0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Integer] The value as an integer
|
|
42
|
+
def int_value
|
|
43
|
+
value.is_a?(Numeric) ? value.to_i : 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Hash, nil] The value as a hash
|
|
47
|
+
def json_value
|
|
48
|
+
value.is_a?(Hash) ? value : nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Creates a FlagState from a hash.
|
|
52
|
+
#
|
|
53
|
+
# @param data [Hash] The data hash
|
|
54
|
+
# @return [FlagState]
|
|
55
|
+
def self.from_hash(data)
|
|
56
|
+
new(
|
|
57
|
+
key: data["key"] || data[:key],
|
|
58
|
+
value: data["value"] || data[:value],
|
|
59
|
+
enabled: data.fetch("enabled", data.fetch(:enabled, true)),
|
|
60
|
+
version: data["version"] || data[:version] || 0,
|
|
61
|
+
flag_type: data["flagType"] || data[:flag_type],
|
|
62
|
+
last_modified: data["lastModified"] || data[:last_modified],
|
|
63
|
+
metadata: data["metadata"] || data[:metadata]
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Converts the flag state to a hash.
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash]
|
|
70
|
+
def to_h
|
|
71
|
+
{
|
|
72
|
+
key: key,
|
|
73
|
+
value: value,
|
|
74
|
+
enabled: enabled,
|
|
75
|
+
version: version,
|
|
76
|
+
flag_type: flag_type,
|
|
77
|
+
last_modified: last_modified,
|
|
78
|
+
metadata: metadata
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def ==(other)
|
|
83
|
+
return false unless other.is_a?(FlagState)
|
|
84
|
+
|
|
85
|
+
key == other.key && value == other.value && enabled == other.enabled && version == other.version
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def eql?(other)
|
|
89
|
+
self == other
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def hash
|
|
93
|
+
[key, value, enabled, version].hash
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Alias for backward compatibility
|
|
99
|
+
FlagState = Types::FlagState
|
|
100
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlagKit
|
|
4
|
+
module Types
|
|
5
|
+
# Types of feature flags.
|
|
6
|
+
module FlagType
|
|
7
|
+
BOOLEAN = "boolean"
|
|
8
|
+
STRING = "string"
|
|
9
|
+
NUMBER = "number"
|
|
10
|
+
JSON = "json"
|
|
11
|
+
|
|
12
|
+
ALL = [BOOLEAN, STRING, NUMBER, JSON].freeze
|
|
13
|
+
|
|
14
|
+
# Infers the flag type from a value.
|
|
15
|
+
#
|
|
16
|
+
# @param value [Object] The value to infer type from
|
|
17
|
+
# @return [String] The inferred flag type
|
|
18
|
+
def self.infer(value)
|
|
19
|
+
case value
|
|
20
|
+
when TrueClass, FalseClass
|
|
21
|
+
BOOLEAN
|
|
22
|
+
when String
|
|
23
|
+
STRING
|
|
24
|
+
when Numeric
|
|
25
|
+
NUMBER
|
|
26
|
+
else
|
|
27
|
+
JSON
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Parses a flag type string.
|
|
32
|
+
#
|
|
33
|
+
# @param value [String] The type string
|
|
34
|
+
# @return [String] The normalized flag type
|
|
35
|
+
def self.parse(value)
|
|
36
|
+
return JSON unless value
|
|
37
|
+
|
|
38
|
+
normalized = value.to_s.downcase
|
|
39
|
+
ALL.include?(normalized) ? normalized : JSON
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Alias for backward compatibility
|
|
45
|
+
FlagType = Types::FlagType
|
|
46
|
+
end
|