Module: Featurevisor

Defined in:
lib/featurevisor.rb,
lib/featurevisor/hooks.rb,
lib/featurevisor/events.rb,
lib/featurevisor/logger.rb,
lib/featurevisor/emitter.rb,
lib/featurevisor/version.rb,
lib/featurevisor/bucketer.rb,
lib/featurevisor/evaluate.rb,
lib/featurevisor/instance.rb,
lib/featurevisor/conditions.rb,
lib/featurevisor/murmurhash.rb,
lib/featurevisor/child_instance.rb,
lib/featurevisor/datafile_reader.rb,
lib/featurevisor/compare_versions.rb

Defined Under Namespace

Modules: Bucketer, Conditions, Evaluate, EvaluationReason, Events, Hooks Classes: ChildInstance, DatafileReader, Emitter, Error, Instance, Logger

Constant Summary collapse

LOG_LEVELS =

Log levels for the logger

%w[fatal error warn info debug].freeze
DEFAULT_LOG_LEVEL =
"info".freeze
LOGGER_PREFIX =
"[Featurevisor]".freeze
EVENT_NAMES =

Event names for the emitter

%w[datafile_set context_set sticky_set].freeze
VERSION =
"0.2.0"
EVALUATION_TYPES =

Evaluation types

%w[flag variation variable].freeze
SEMVER_REGEX =

Regular expression for semantic version parsing

/^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i

Class Method Summary collapse

Class Method Details

.compare_segments(a, b) ⇒ Integer

Compares version segments

Parameters:

  • a (Array<String>, MatchData)

    First version segments

  • b (Array<String>, MatchData)

    Second version segments

Returns:

  • (Integer)

    -1 if a < b, 0 if equal, 1 if a > b



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/featurevisor/compare_versions.rb', line 80

def self.compare_segments(a, b)
  # Convert to arrays if needed
  a_array = a.is_a?(MatchData) ? a.to_a[1..-1] : a.to_a
  b_array = b.is_a?(MatchData) ? b.to_a[1..-1] : b.to_a

  max_length = [a_array.length, b_array.length].max

  (0...max_length).each do |i|
    a_val = a_array[i] || "0"
    b_val = b_array[i] || "0"

    result = compare_strings(a_val, b_val)
    return result unless result == 0
  end

  0
end

.compare_strings(a, b) ⇒ Integer

Compares two strings for version comparison

Parameters:

  • a (String)

    First string

  • b (String)

    Second string

Returns:

  • (Integer)

    -1 if a < b, 0 if equal, 1 if a > b



62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/featurevisor/compare_versions.rb', line 62

def self.compare_strings(a, b)
  return 0 if wildcard?(a) || wildcard?(b)

  ap, bp = force_type(try_parse(a), try_parse(b))

  if ap > bp
    1
  elsif ap < bp
    -1
  else
    0
  end
end

.compare_versions(v1, v2) ⇒ Integer

Compares two version strings

Parameters:

  • v1 (String)

    First version string

  • v2 (String)

    Second version string

Returns:

  • (Integer)

    -1 if v1 < v2, 0 if equal, 1 if v1 > v2

Raises:

  • (TypeError)

    If either version is not a string

  • (ArgumentError)

    If either version is not valid semver



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/featurevisor/compare_versions.rb', line 104

def self.compare_versions(v1, v2)
  # Validate input and split into segments
  n1 = validate_and_parse(v1)
  n2 = validate_and_parse(v2)

  # Pop off the patch
  p1 = n1.pop
  p2 = n2.pop

  # Validate numbers
  r = compare_segments(n1, n2)
  return r unless r == 0

  # Validate pre-release
  if p1 && p2
    compare_segments(p1.split("."), p2.split("."))
  elsif p1 || p2
    p1 ? -1 : 1
  else
    0
  end
end

.create_instance(options = {}) ⇒ Instance

Create a new Featurevisor instance

Parameters:

  • options (Hash) (defaults to: {})

    Instance options

Returns:



460
461
462
# File 'lib/featurevisor/instance.rb', line 460

def self.create_instance(options = {})
  Instance.new(options)
end

.create_logger(options = {}) ⇒ Logger

Create a new logger instance

Parameters:

  • options (Hash) (defaults to: {})

    Logger options

Returns:

  • (Logger)

    New logger instance



119
120
121
# File 'lib/featurevisor/logger.rb', line 119

def self.create_logger(options = {})
  Logger.new(options)
end

.default_log_handler(level, message, details = nil) ⇒ Object

Default log handler function

Parameters:

  • level (String)

    Log level

  • message (String)

    Log message

  • details (Hash, nil) (defaults to: nil)

    Additional details



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/featurevisor/logger.rb', line 127

def self.default_log_handler(level, message, details = nil)
  method_name = case level
               when "info" then "puts"
               when "warn" then "warn"
               when "error", "fatal" then "warn"
               else "puts"
               end

  case method_name
  when "puts"
    if details && !details.empty?
      Kernel.puts("#{LOGGER_PREFIX} #{message} #{details.inspect}")
    else
      Kernel.puts("#{LOGGER_PREFIX} #{message}")
    end
  when "warn"
    if details && !details.empty?
      Kernel.warn("#{LOGGER_PREFIX} #{message} #{details.inspect}")
    else
      Kernel.warn("#{LOGGER_PREFIX} #{message}")
    end
  end
end

.force_type(a, b) ⇒ Array

Forces types to be the same for comparison

Parameters:

  • a (String, Integer)

    First value

  • b (String, Integer)

    Second value

Returns:

  • (Array)

    Array with both values converted to same type



41
42
43
44
45
46
47
# File 'lib/featurevisor/compare_versions.rb', line 41

def self.force_type(a, b)
  if a.is_a?(Integer) != b.is_a?(Integer)
    [a.to_s, b.to_s]
  else
    [a, b]
  end
end

.murmur_hash_v3(key, seed) ⇒ Integer

MurmurHash v3 implementation ported from TypeScript Original: github.com/perezd/node-murmurhash

Parameters:

  • key (String, Array<Integer>)

    Input key to hash

  • seed (Integer)

    Seed value for the hash

Returns:

  • (Integer)

    32-bit hash value



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/featurevisor/murmurhash.rb', line 9

def self.murmur_hash_v3(key, seed)
  # Convert string to bytes if needed
  key = key.bytes if key.is_a?(String)

  remainder = key.length & 3  # key.length % 4
  bytes = key.length - remainder
  h1 = seed
  c1 = 0xcc9e2d51
  c2 = 0x1b873593
  i = 0

  # Process 4-byte chunks
  while i < bytes
    k1 = (key[i] & 0xff) |
          ((key[i + 1] & 0xff) << 8) |
          ((key[i + 2] & 0xff) << 16) |
          ((key[i + 3] & 0xff) << 24)
    i += 4

    k1 = ((k1 & 0xffff) * c1 + ((((k1 >> 16) * c1) & 0xffff) << 16)) & 0xffffffff
    k1 = (k1 << 15) | (k1 >> 17)
    k1 = ((k1 & 0xffff) * c2 + ((((k1 >> 16) * c2) & 0xffff) << 16)) & 0xffffffff

    h1 ^= k1
    h1 = (h1 << 13) | (h1 >> 19)
    h1b = ((h1 & 0xffff) * 5 + ((((h1 >> 16) * 5) & 0xffff) << 16)) & 0xffffffff
    h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >> 16) + 0xe654) & 0xffff) << 16)
  end

  # Process remaining bytes
  k1 = 0

  # Handle remainder processing with fall-through behavior like TypeScript switch
  if remainder >= 3
    k1 ^= (key[i + 2] & 0xff) << 16
  end
  if remainder >= 2
    k1 ^= (key[i + 1] & 0xff) << 8
  end
  if remainder >= 1
    k1 ^= key[i] & 0xff

    k1 = ((k1 & 0xffff) * c1 + ((((k1 >> 16) * c1) & 0xffff) << 16)) & 0xffffffff
    k1 = (k1 << 15) | (k1 >> 17)
    k1 = ((k1 & 0xffff) * c2 + ((((k1 >> 16) * c2) & 0xffff) << 16)) & 0xffffffff
    h1 ^= k1
  end

  h1 ^= key.length

  # Final mixing - use unsigned right shift equivalent
  h1 ^= h1 >> 16
  h1 = ((h1 & 0xffff) * 0x85ebca6b + ((((h1 >> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff
  h1 ^= h1 >> 13
  h1 = ((h1 & 0xffff) * 0xc2b2ae35 + ((((h1 >> 16) * 0xc2b2ae35) & 0xffff) << 16)) & 0xffffffff
  h1 ^= h1 >> 16

  # Convert to unsigned 32-bit integer (equivalent to >>> 0 in TypeScript)
  h1 & 0xffffffff
end

.try_parse(v) ⇒ Integer, String

Tries to parse a string as an integer

Parameters:

  • v (String)

    String to parse

Returns:

  • (Integer, String)

    Parsed integer or original string



52
53
54
55
56
# File 'lib/featurevisor/compare_versions.rb', line 52

def self.try_parse(v)
  Integer(v, 10)
rescue ArgumentError
  v
end

.validate_and_parse(version) ⇒ Array<String>

Validates and parses a version string

Parameters:

  • version (String)

    Version string to validate and parse

Returns:

  • (Array<String>)

    Array of version segments

Raises:

  • (TypeError)

    If version is not a string

  • (ArgumentError)

    If version is not valid semver



16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/featurevisor/compare_versions.rb', line 16

def self.validate_and_parse(version)
  unless version.is_a?(String)
    raise TypeError, "Invalid argument expected string"
  end

  match = version.match(SEMVER_REGEX)
  unless match
    raise ArgumentError, "Invalid argument not valid semver ('#{version}' received)"
  end

  # Remove the first element (full match) and return the rest
  match.to_a[1..-1]
end

.wildcard?(s) ⇒ Boolean

Checks if a string is a wildcard

Parameters:

  • s (String)

    String to check

Returns:

  • (Boolean)

    True if wildcard



33
34
35
# File 'lib/featurevisor/compare_versions.rb', line 33

def self.wildcard?(s)
  s == "*" || s.downcase == "x"
end