Skip to content

Commit 939691c

Browse files
committed
Featurevisor Ruby SDK
1 parent 5acc681 commit 939691c

17 files changed

Lines changed: 2906 additions & 0 deletions

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
source "https://rubygems.org"
2+
3+
# Specify your gem's dependencies in featurevisor.gemspec
4+
gemspec

Gemfile.lock

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
PATH
2+
remote: .
3+
specs:
4+
featurevisor (0.1.0)
5+
6+
GEM
7+
remote: https://rubygems.org/
8+
specs:
9+
diff-lcs (1.6.2)
10+
rake (13.3.0)
11+
rspec (3.13.1)
12+
rspec-core (~> 3.13.0)
13+
rspec-expectations (~> 3.13.0)
14+
rspec-mocks (~> 3.13.0)
15+
rspec-core (3.13.5)
16+
rspec-support (~> 3.13.0)
17+
rspec-expectations (3.13.5)
18+
diff-lcs (>= 1.2.0, < 2.0)
19+
rspec-support (~> 3.13.0)
20+
rspec-mocks (3.13.5)
21+
diff-lcs (>= 1.2.0, < 2.0)
22+
rspec-support (~> 3.13.0)
23+
rspec-support (3.13.4)
24+
25+
PLATFORMS
26+
ruby
27+
28+
DEPENDENCIES
29+
featurevisor!
30+
rake (~> 13.0)
31+
rspec (~> 3.12)
32+
33+
BUNDLED WITH
34+
1.17.2

featurevisor.gemspec

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Gem::Specification.new do |spec|
2+
spec.name = "featurevisor"
3+
spec.version = "0.1.0"
4+
spec.authors = ["Your Name"]
5+
spec.email = ["your.email@example.com"]
6+
spec.summary = "Featurevisor Ruby SDK with CLI tools for feature management"
7+
spec.description = "Featurevisor is a Ruby SDK that provides feature flag management, A/B testing, and progressive delivery capabilities. Includes CLI tools for testing, benchmarking, and distribution analysis."
8+
spec.homepage = "https://github.com/yourusername/featurevisor"
9+
spec.license = "MIT"
10+
spec.required_ruby_version = ">= 2.6.0"
11+
12+
spec.files = Dir.glob("lib/**/*") + Dir.glob("bin/**/*") + %w[README.md LICENSE]
13+
spec.bindir = "bin"
14+
spec.executables = ["featurevisor"]
15+
spec.require_paths = ["lib"]
16+
17+
spec.add_development_dependency "rspec", "~> 3.12"
18+
spec.add_development_dependency "rake", "~> 13.0"
19+
end

lib/featurevisor.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
require_relative "featurevisor/version"
2+
require_relative "featurevisor/murmurhash"
3+
require_relative "featurevisor/compare_versions"
4+
require_relative "featurevisor/logger"
5+
require_relative "featurevisor/emitter"
6+
require_relative "featurevisor/conditions"
7+
require_relative "featurevisor/datafile_reader"
8+
require_relative "featurevisor/bucketer"
9+
require_relative "featurevisor/hooks"
10+
require_relative "featurevisor/evaluate"
11+
require_relative "featurevisor/instance"
12+
require_relative "featurevisor/child_instance"
13+
require_relative "featurevisor/events"
14+
15+
module Featurevisor
16+
class Error < StandardError; end
17+
end

lib/featurevisor/bucketer.rb

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# frozen_string_literal: true
2+
3+
module Featurevisor
4+
# Bucketer module for handling feature flag bucketing
5+
module Bucketer
6+
# Maximum bucketed number (100% * 1000 to include three decimal places)
7+
MAX_BUCKETED_NUMBER = 100_000
8+
9+
# Hash seed for consistent bucketing
10+
HASH_SEED = 1
11+
12+
# Maximum hash value for 32-bit integers
13+
MAX_HASH_VALUE = 2**32
14+
15+
# Default separator for bucket keys
16+
DEFAULT_BUCKET_KEY_SEPARATOR = "."
17+
18+
# Get bucketed number from a bucket key
19+
# @param bucket_key [String] The bucket key to hash
20+
# @return [Integer] Bucket value between 0 and 100000
21+
def self.get_bucketed_number(bucket_key)
22+
hash_value = Featurevisor.murmur_hash_v3(bucket_key, HASH_SEED)
23+
ratio = hash_value.to_f / MAX_HASH_VALUE
24+
25+
(ratio * MAX_BUCKETED_NUMBER).floor
26+
end
27+
28+
# Get bucket key from feature configuration and context
29+
# @param options [Hash] Options hash containing:
30+
# - feature_key [String] The feature key
31+
# - bucket_by [String, Array<String>, Hash] Bucketing strategy
32+
# - context [Hash] User context
33+
# - logger [Logger] Logger instance
34+
# @return [String] The bucket key
35+
# @raise [StandardError] If bucket_by is invalid
36+
def self.get_bucket_key(options)
37+
feature_key = options[:feature_key]
38+
bucket_by = options[:bucket_by]
39+
context = options[:context]
40+
logger = options[:logger]
41+
42+
type, attribute_keys = parse_bucket_by(bucket_by, logger, feature_key)
43+
44+
bucket_key = build_bucket_key(attribute_keys, context, type, feature_key)
45+
46+
bucket_key.join(DEFAULT_BUCKET_KEY_SEPARATOR)
47+
end
48+
49+
private
50+
51+
# Parse bucket_by configuration to determine type and attribute keys
52+
# @param bucket_by [String, Array<String>, Hash] Bucketing strategy
53+
# @param logger [Logger] Logger instance
54+
# @param feature_key [String] Feature key for error logging
55+
# @return [Array] Tuple of [type, attribute_keys]
56+
def self.parse_bucket_by(bucket_by, logger, feature_key)
57+
if bucket_by.is_a?(String)
58+
["plain", [bucket_by]]
59+
elsif bucket_by.is_a?(Array)
60+
["and", bucket_by]
61+
elsif bucket_by.is_a?(Hash) && bucket_by[:or].is_a?(Array)
62+
["or", bucket_by[:or]]
63+
else
64+
logger.error("invalid bucketBy", { feature_key: feature_key, bucket_by: bucket_by })
65+
raise StandardError, "invalid bucketBy"
66+
end
67+
end
68+
69+
# Build bucket key array from attribute keys and context
70+
# @param attribute_keys [Array<String>] Array of attribute keys
71+
# @param context [Hash] User context
72+
# @param type [String] Bucketing type ("plain", "and", "or")
73+
# @param feature_key [String] Feature key to append
74+
# @return [Array] Array of bucket key components
75+
def self.build_bucket_key(attribute_keys, context, type, feature_key)
76+
bucket_key = []
77+
78+
attribute_keys.each do |attribute_key|
79+
attribute_value = Featurevisor::Conditions.get_value_from_context(context, attribute_key)
80+
81+
next if attribute_value.nil?
82+
83+
if type == "plain" || type == "and"
84+
bucket_key << attribute_value
85+
elsif type == "or" && bucket_key.empty?
86+
# For "or" type, only take the first available value
87+
bucket_key << attribute_value
88+
end
89+
end
90+
91+
bucket_key << feature_key
92+
bucket_key
93+
end
94+
end
95+
end

0 commit comments

Comments
 (0)