quonfig 0.0.2

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 (108) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/rules/constitution.md +81 -0
  3. data/.claude/rules/git-safety.md +11 -0
  4. data/.claude/rules/issue-tracking.md +13 -0
  5. data/.claude/rules/testing-workflow.md +28 -0
  6. data/.envrc.sample +3 -0
  7. data/.github/CODEOWNERS +2 -0
  8. data/.github/pull_request_template.md +8 -0
  9. data/.github/workflows/push_gem.yml +49 -0
  10. data/.github/workflows/ruby.yml +60 -0
  11. data/.github/workflows/test.yaml +40 -0
  12. data/.rubocop.yml +13 -0
  13. data/.tool-versions +1 -0
  14. data/CHANGELOG.md +301 -0
  15. data/CLAUDE.md +29 -0
  16. data/CODEOWNERS +1 -0
  17. data/Gemfile +26 -0
  18. data/Gemfile.lock +177 -0
  19. data/LICENSE.txt +20 -0
  20. data/README.md +213 -0
  21. data/Rakefile +64 -0
  22. data/VERSION +1 -0
  23. data/dev/allocation_stats +60 -0
  24. data/dev/benchmark +40 -0
  25. data/dev/console +12 -0
  26. data/dev/script_setup.rb +18 -0
  27. data/lib/quonfig/bound_client.rb +71 -0
  28. data/lib/quonfig/caching_http_connection.rb +95 -0
  29. data/lib/quonfig/client.rb +221 -0
  30. data/lib/quonfig/config_envelope.rb +5 -0
  31. data/lib/quonfig/config_loader.rb +103 -0
  32. data/lib/quonfig/config_store.rb +42 -0
  33. data/lib/quonfig/context.rb +101 -0
  34. data/lib/quonfig/datadir.rb +101 -0
  35. data/lib/quonfig/duration.rb +58 -0
  36. data/lib/quonfig/encryption.rb +74 -0
  37. data/lib/quonfig/error.rb +6 -0
  38. data/lib/quonfig/errors/env_var_parse_error.rb +11 -0
  39. data/lib/quonfig/errors/initialization_timeout_error.rb +12 -0
  40. data/lib/quonfig/errors/invalid_sdk_key_error.rb +19 -0
  41. data/lib/quonfig/errors/missing_default_error.rb +13 -0
  42. data/lib/quonfig/errors/missing_env_var_error.rb +11 -0
  43. data/lib/quonfig/errors/type_mismatch_error.rb +11 -0
  44. data/lib/quonfig/errors/uninitialized_error.rb +13 -0
  45. data/lib/quonfig/evaluation.rb +64 -0
  46. data/lib/quonfig/evaluator.rb +464 -0
  47. data/lib/quonfig/exponential_backoff.rb +21 -0
  48. data/lib/quonfig/fixed_size_hash.rb +14 -0
  49. data/lib/quonfig/http_connection.rb +46 -0
  50. data/lib/quonfig/internal_logger.rb +173 -0
  51. data/lib/quonfig/murmer3.rb +50 -0
  52. data/lib/quonfig/options.rb +194 -0
  53. data/lib/quonfig/periodic_sync.rb +74 -0
  54. data/lib/quonfig/quonfig.rb +58 -0
  55. data/lib/quonfig/rate_limit_cache.rb +41 -0
  56. data/lib/quonfig/reason.rb +39 -0
  57. data/lib/quonfig/resolver.rb +42 -0
  58. data/lib/quonfig/semantic_logger_filter.rb +90 -0
  59. data/lib/quonfig/semver.rb +132 -0
  60. data/lib/quonfig/sse_config_client.rb +135 -0
  61. data/lib/quonfig/time_helpers.rb +7 -0
  62. data/lib/quonfig/types.rb +56 -0
  63. data/lib/quonfig/weighted_value_resolver.rb +49 -0
  64. data/lib/quonfig.rb +57 -0
  65. data/quonfig.gemspec +149 -0
  66. data/scripts/generate_integration_tests.rb +362 -0
  67. data/test/fixtures/datafile.json +87 -0
  68. data/test/integration/test_context_precedence.rb +194 -0
  69. data/test/integration/test_datadir_environment.rb +76 -0
  70. data/test/integration/test_enabled.rb +784 -0
  71. data/test/integration/test_enabled_with_contexts.rb +94 -0
  72. data/test/integration/test_get.rb +224 -0
  73. data/test/integration/test_get_feature_flag.rb +34 -0
  74. data/test/integration/test_get_or_raise.rb +86 -0
  75. data/test/integration/test_get_weighted_values.rb +29 -0
  76. data/test/integration/test_helpers.rb +139 -0
  77. data/test/integration/test_helpers_test.rb +73 -0
  78. data/test/integration/test_post.rb +34 -0
  79. data/test/integration/test_telemetry.rb +114 -0
  80. data/test/support/common_helpers.rb +106 -0
  81. data/test/support/mock_base_client.rb +27 -0
  82. data/test/support/mock_config_loader.rb +1 -0
  83. data/test/test_bound_client.rb +109 -0
  84. data/test/test_caching_http_connection.rb +218 -0
  85. data/test/test_client.rb +255 -0
  86. data/test/test_config_loader.rb +70 -0
  87. data/test/test_context.rb +136 -0
  88. data/test/test_datadir.rb +199 -0
  89. data/test/test_duration.rb +37 -0
  90. data/test/test_encryption.rb +16 -0
  91. data/test/test_evaluator.rb +285 -0
  92. data/test/test_exponential_backoff.rb +44 -0
  93. data/test/test_fixed_size_hash.rb +119 -0
  94. data/test/test_helper.rb +17 -0
  95. data/test/test_http_connection.rb +79 -0
  96. data/test/test_internal_logger.rb +34 -0
  97. data/test/test_options.rb +167 -0
  98. data/test/test_rate_limit_cache.rb +44 -0
  99. data/test/test_reason.rb +79 -0
  100. data/test/test_rename.rb +65 -0
  101. data/test/test_resolver.rb +144 -0
  102. data/test/test_semantic_logger_filter.rb +123 -0
  103. data/test/test_semver.rb +108 -0
  104. data/test/test_sse_config_client.rb +297 -0
  105. data/test/test_typed_getters.rb +131 -0
  106. data/test/test_types.rb +141 -0
  107. data/test/test_weighted_value_resolver.rb +84 -0
  108. metadata +311 -0
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quonfig
4
+ # SemanticLogger filter that gates log output by a single Quonfig config
5
+ # whose rules target the logger via the +quonfig.logger-name+ context
6
+ # property.
7
+ #
8
+ # Usage:
9
+ # filter = client.semantic_logger_filter(config_key: 'log-levels.my-app')
10
+ # SemanticLogger.add_appender(io: $stdout, filter: filter)
11
+ #
12
+ # The filter normalizes the SemanticLogger logger name to dotted snake_case
13
+ # (e.g. +MyApp::Foo::Bar+ → +my_app.foo.bar+) and exposes it to the
14
+ # evaluator under +quonfig.logger-name+ so the customer's Quonfig config can
15
+ # discriminate per-logger via PROP_STARTS_WITH_ONE_OF / PROP_IS_ONE_OF
16
+ # rules. Lookup is O(1): one +client.get+ call per log line.
17
+ class SemanticLoggerFilter
18
+ LEVELS = {
19
+ trace: 0,
20
+ debug: 1,
21
+ info: 2,
22
+ warn: 3,
23
+ error: 4,
24
+ fatal: 5
25
+ }.freeze
26
+
27
+ LOGGER_NAME_CONTEXT_KEY = 'quonfig.logger-name'
28
+
29
+ def self.semantic_logger_loaded?
30
+ defined?(SemanticLogger)
31
+ end
32
+
33
+ def initialize(client, config_key:)
34
+ unless self.class.semantic_logger_loaded?
35
+ raise LoadError, "semantic_logger gem is required for Quonfig::SemanticLoggerFilter. Add `gem 'semantic_logger'` to your Gemfile."
36
+ end
37
+
38
+ @client = client
39
+ @config_key = config_key
40
+ end
41
+
42
+ # SemanticLogger filter contract: return true to emit, false to suppress.
43
+ # Missing config key → return true so SemanticLogger's static level decides.
44
+ def call(log)
45
+ configured = @client.get(@config_key, nil, context_for(log))
46
+ return true if configured.nil?
47
+
48
+ log_severity = LEVELS[log.level] || LEVELS[:debug]
49
+ min_severity = LEVELS[normalize_level(configured)] || LEVELS[:debug]
50
+ log_severity >= min_severity
51
+ end
52
+
53
+ # Normalize a SemanticLogger logger name to the dotted snake_case form
54
+ # the customer writes targeting rules against.
55
+ # MyApp::Foo::Bar → my_app.foo.bar
56
+ # HTMLParser → html_parser
57
+ def normalize(name)
58
+ name.to_s
59
+ .gsub('::', '.')
60
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
61
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
62
+ .downcase
63
+ end
64
+
65
+ private
66
+
67
+ def context_for(log)
68
+ { 'quonfig' => { 'logger-name' => normalize(log.name) } }
69
+ end
70
+
71
+ def normalize_level(level)
72
+ case level
73
+ when Symbol then level.downcase
74
+ when String then level.downcase.to_sym
75
+ when Integer
76
+ # LogLevel ints from old proto: 1=trace … 9=fatal. Best-effort map.
77
+ case level
78
+ when 1 then :trace
79
+ when 2 then :debug
80
+ when 3 then :info
81
+ when 5 then :warn
82
+ when 6 then :error
83
+ when 9 then :fatal
84
+ else :debug
85
+ end
86
+ else :debug
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SemanticVersion
4
+ include Comparable
5
+
6
+ SEMVER_PATTERN = /
7
+ ^
8
+ (?<major>0|[1-9]\d*)
9
+ \.
10
+ (?<minor>0|[1-9]\d*)
11
+ \.
12
+ (?<patch>0|[1-9]\d*)
13
+ (?:-(?<prerelease>
14
+ (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
15
+ (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
16
+ ))?
17
+ (?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
18
+ $
19
+ /x
20
+
21
+ attr_reader :major, :minor, :patch, :prerelease, :build_metadata
22
+
23
+ def self.parse(version_string)
24
+ raise ArgumentError, "version string cannot be nil" if version_string.nil?
25
+ raise ArgumentError, "version string cannot be empty" if version_string.empty?
26
+
27
+ match = SEMVER_PATTERN.match(version_string)
28
+ raise ArgumentError, "invalid semantic version format: #{version_string}" unless match
29
+
30
+ new(
31
+ major: match[:major].to_i,
32
+ minor: match[:minor].to_i,
33
+ patch: match[:patch].to_i,
34
+ prerelease: match[:prerelease],
35
+ build_metadata: match[:buildmetadata]
36
+ )
37
+ end
38
+
39
+ def self.parse_quietly(version_string)
40
+ parse(version_string)
41
+ rescue ArgumentError
42
+ nil
43
+ end
44
+
45
+ def initialize(major:, minor:, patch:, prerelease: nil, build_metadata: nil)
46
+ @major = major
47
+ @minor = minor
48
+ @patch = patch
49
+ @prerelease = prerelease
50
+ @build_metadata = build_metadata
51
+ end
52
+
53
+ def <=>(other)
54
+ return nil unless other.is_a?(SemanticVersion)
55
+
56
+ # Compare major.minor.patch
57
+ return major <=> other.major if major != other.major
58
+ return minor <=> other.minor if minor != other.minor
59
+ return patch <=> other.patch if patch != other.patch
60
+
61
+ # Compare pre-release versions
62
+ compare_prerelease(prerelease, other.prerelease)
63
+ end
64
+
65
+ def ==(other)
66
+ return false unless other.is_a?(SemanticVersion)
67
+
68
+ major == other.major &&
69
+ minor == other.minor &&
70
+ patch == other.patch &&
71
+ prerelease == other.prerelease
72
+ # Build metadata is ignored in equality checks
73
+ end
74
+
75
+ def eql?(other)
76
+ self == other
77
+ end
78
+
79
+ def hash
80
+ [major, minor, patch, prerelease].hash
81
+ end
82
+
83
+ def to_s
84
+ result = "#{major}.#{minor}.#{patch}"
85
+ result += "-#{prerelease}" if prerelease
86
+ result += "+#{build_metadata}" if build_metadata
87
+ result
88
+ end
89
+
90
+ private
91
+
92
+ def self.numeric?(str)
93
+ str.to_i.to_s == str
94
+ end
95
+
96
+ def compare_prerelease(pre1, pre2)
97
+ # If both are empty, they're equal
98
+ return 0 if pre1.nil? && pre2.nil?
99
+
100
+ # A version without prerelease has higher precedence
101
+ return 1 if pre1.nil?
102
+ return -1 if pre2.nil?
103
+
104
+ # Split into identifiers
105
+ ids1 = pre1.split('.')
106
+ ids2 = pre2.split('.')
107
+
108
+ # Compare each identifier until we find a difference
109
+ [ids1.length, ids2.length].min.times do |i|
110
+ cmp = compare_prerelease_identifiers(ids1[i], ids2[i])
111
+ return cmp if cmp != 0
112
+ end
113
+
114
+ # If all identifiers match up to the length of the shorter one,
115
+ # the longer one has higher precedence
116
+ ids1.length <=> ids2.length
117
+ end
118
+
119
+ def compare_prerelease_identifiers(id1, id2)
120
+ # If both are numeric, compare numerically
121
+ if self.class.numeric?(id1) && self.class.numeric?(id2)
122
+ return id1.to_i <=> id2.to_i
123
+ end
124
+
125
+ # If only one is numeric, numeric ones have lower precedence
126
+ return -1 if self.class.numeric?(id1)
127
+ return 1 if self.class.numeric?(id2)
128
+
129
+ # Neither is numeric, compare as strings
130
+ id1 <=> id2
131
+ end
132
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+ require 'base64'
3
+ require 'json'
4
+
5
+ module Quonfig
6
+ class SSEConfigClient
7
+ class Options
8
+ attr_reader :sse_read_timeout, :seconds_between_new_connection,
9
+ :sse_default_reconnect_time, :sleep_delay_for_new_connection_check,
10
+ :errors_to_close_connection
11
+
12
+ def initialize(sse_read_timeout: 300,
13
+ seconds_between_new_connection: 5,
14
+ sleep_delay_for_new_connection_check: 1,
15
+ sse_default_reconnect_time: SSE::Client::DEFAULT_RECONNECT_TIME,
16
+ errors_to_close_connection: [HTTP::ConnectionError])
17
+ @sse_read_timeout = sse_read_timeout
18
+ @seconds_between_new_connection = seconds_between_new_connection
19
+ @sse_default_reconnect_time = sse_default_reconnect_time
20
+ @sleep_delay_for_new_connection_check = sleep_delay_for_new_connection_check
21
+ @errors_to_close_connection = errors_to_close_connection
22
+ end
23
+ end
24
+
25
+ LOG = Quonfig::InternalLogger.new(self)
26
+
27
+ def initialize(prefab_options, config_loader, options = nil, logger = nil)
28
+ @prefab_options = prefab_options
29
+ @options = options || Options.new
30
+ @config_loader = config_loader
31
+ @connected = false
32
+ @logger = logger || LOG
33
+ end
34
+
35
+ def close
36
+ @retry_thread&.kill
37
+ @client&.close
38
+ end
39
+
40
+ def start(&load_configs)
41
+ if @prefab_options.sse_api_urls.empty?
42
+ @logger.debug 'No SSE api_urls configured'
43
+ return
44
+ end
45
+
46
+ @client = connect(&load_configs)
47
+
48
+ closed_count = 0
49
+
50
+ @retry_thread = Thread.new do
51
+ loop do
52
+ sleep @options.sleep_delay_for_new_connection_check
53
+
54
+ if @client.closed?
55
+ closed_count += @options.sleep_delay_for_new_connection_check
56
+
57
+ if closed_count > @options.seconds_between_new_connection
58
+ closed_count = 0
59
+ @logger.debug 'Reconnecting SSE client'
60
+ @client = connect(&load_configs)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def connect(&load_configs)
68
+ url = "#{source}/api/v2/sse/config"
69
+ @logger.debug "SSE Streaming Connect to #{url} start_at #{@config_loader.highwater_mark}"
70
+
71
+ SSE::Client.new(url,
72
+ headers: headers,
73
+ read_timeout: @options.sse_read_timeout,
74
+ reconnect_time: @options.sse_default_reconnect_time,
75
+ last_event_id: (@config_loader.highwater_mark&.positive? ? @config_loader.highwater_mark.to_s : nil),
76
+ logger: Quonfig::InternalLogger.new(SSE::Client)) do |client|
77
+ client.on_event do |event|
78
+ if event.data.nil? || event.data.empty?
79
+ @logger.error "SSE Streaming Error: Received empty data for url #{url}"
80
+ client.close
81
+ return
82
+ end
83
+
84
+ begin
85
+ parsed = JSON.parse(event.data)
86
+ rescue JSON::ParserError => e
87
+ @logger.error "SSE Streaming Error: Failed to parse JSON for url #{url}: #{e.message}"
88
+ client.close
89
+ return
90
+ end
91
+
92
+ envelope = Quonfig::ConfigEnvelope.new(
93
+ configs: parsed['configs'] || [],
94
+ meta: parsed['meta'] || {}
95
+ )
96
+ load_configs.call(envelope, event, :sse)
97
+ end
98
+
99
+ client.on_error do |error|
100
+ # SSL "unexpected eof" is expected when SSE sessions timeout normally
101
+ if error.is_a?(OpenSSL::SSL::SSLError) && error.message.include?('unexpected eof')
102
+ @logger.debug "SSE Streaming: Connection closed (expected timeout) for url #{url}"
103
+ else
104
+ @logger.error "SSE Streaming Error: #{error.inspect} for url #{url}"
105
+ end
106
+
107
+ if @options.errors_to_close_connection.any? { |klass| error.is_a?(klass) }
108
+ @logger.debug "Closing SSE connection for url #{url}"
109
+ client.close
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def headers
116
+ auth = "1:#{@prefab_options.sdk_key}"
117
+ auth_string = Base64.strict_encode64(auth)
118
+ return {
119
+ 'Authorization' => "Basic #{auth_string}",
120
+ 'Accept' => 'text/event-stream',
121
+ 'X-Quonfig-SDK-Version' => "sdk-ruby-#{Quonfig::VERSION}"
122
+ }
123
+ end
124
+
125
+ def source
126
+ @source_index = @source_index.nil? ? 0 : @source_index + 1
127
+
128
+ if @source_index >= @prefab_options.sse_api_urls.size
129
+ @source_index = 0
130
+ end
131
+
132
+ @prefab_options.sse_api_urls[@source_index]
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,7 @@
1
+ module Quonfig
2
+ module TimeHelpers
3
+ def self.now_in_ms
4
+ ::Time.now.utc.to_i * 1000
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby types mirroring the JSON delivery protocol defined in
4
+ # sdk-node/src/types.ts. Field names are snake_case per Ruby convention;
5
+ # callers parsing JSON fixtures (camelCase) are responsible for the mapping.
6
+ # All Structs use keyword_init so omitted fields default to nil.
7
+ module Quonfig
8
+ Value = Struct.new(:type, :value, :confidential, :decrypt_with, keyword_init: true)
9
+
10
+ Criterion = Struct.new(:property_name, :operator, :value_to_match, keyword_init: true)
11
+
12
+ Rule = Struct.new(:criteria, :value, keyword_init: true)
13
+
14
+ RuleSet = Struct.new(:rules, keyword_init: true)
15
+
16
+ Environment = Struct.new(:id, :rules, keyword_init: true)
17
+
18
+ Meta = Struct.new(:version, :environment, :workspace_id, keyword_init: true)
19
+
20
+ ConfigResponse = Struct.new(
21
+ :id,
22
+ :key,
23
+ :type,
24
+ :value_type,
25
+ :send_to_client_sdk,
26
+ :default,
27
+ :environment,
28
+ keyword_init: true
29
+ )
30
+
31
+ WeightedValue = Struct.new(:weight, :value, keyword_init: true)
32
+
33
+ WeightedValuesData = Struct.new(:weighted_values, :hash_by_property_name, keyword_init: true)
34
+
35
+ SchemaData = Struct.new(:schema_type, :schema, keyword_init: true)
36
+
37
+ ProvidedData = Struct.new(:source, :lookup, keyword_init: true)
38
+
39
+ WorkspaceEnvironment = Struct.new(:id, :rules, keyword_init: true)
40
+
41
+ WorkspaceConfigDocument = Struct.new(
42
+ :id,
43
+ :key,
44
+ :type,
45
+ :value_type,
46
+ :send_to_client_sdk,
47
+ :default,
48
+ :environments,
49
+ keyword_init: true
50
+ )
51
+
52
+ QuonfigDatadirEnvironments = Struct.new(:environments, keyword_init: true)
53
+
54
+ # ConfigEnvelope is already defined in lib/quonfig/config_envelope.rb with
55
+ # the same members (:configs, :meta). It is required from lib/quonfig.rb.
56
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quonfig
4
+ class WeightedValueResolver
5
+ MAX_32_FLOAT = 4_294_967_294.0
6
+
7
+ def initialize(weights, config_key, context_hash_value)
8
+ @weights = weights
9
+ @config_key = config_key
10
+ @context_hash_value = context_hash_value
11
+ end
12
+
13
+ def resolve
14
+ percent = @context_hash_value ? user_percent : rand
15
+
16
+ index = variant_index(percent)
17
+
18
+ [@weights[index], index]
19
+ end
20
+
21
+ def user_percent
22
+ to_hash = "#{@config_key}#{@context_hash_value}"
23
+ int_value = Murmur3.murmur3_32(to_hash)
24
+ int_value / MAX_32_FLOAT
25
+ end
26
+
27
+ def variant_index(percent_through_distribution)
28
+ distribution_space = @weights.inject(0) { |sum, v| sum + weight_of(v) }
29
+ bucket = distribution_space * percent_through_distribution
30
+
31
+ sum = 0
32
+ @weights.each_with_index do |variant, index|
33
+ w = weight_of(variant)
34
+ return index if bucket < sum + w
35
+
36
+ sum += w
37
+ end
38
+
39
+ # In the event that all weights are zero, return the last variant
40
+ @weights.size - 1
41
+ end
42
+
43
+ private
44
+
45
+ def weight_of(variant)
46
+ variant[:weight] || variant['weight']
47
+ end
48
+ end
49
+ end
data/lib/quonfig.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quonfig
4
+ NO_DEFAULT_PROVIDED = :no_default_provided
5
+ VERSION = File.read(File.dirname(__FILE__) + '/../VERSION').strip
6
+ end
7
+
8
+ begin
9
+ require 'semantic_logger'
10
+ rescue LoadError
11
+ # semantic_logger is optional - only needed for dynamic log level filtering
12
+ end
13
+
14
+ require 'securerandom'
15
+ require 'concurrent/atomics'
16
+ require 'concurrent'
17
+ require 'faraday'
18
+ require 'openssl'
19
+ require 'ld-eventsource'
20
+
21
+ require 'quonfig/internal_logger'
22
+ require 'quonfig/time_helpers'
23
+ require 'quonfig/types'
24
+ require 'quonfig/error'
25
+ require 'quonfig/duration'
26
+ require 'quonfig/reason'
27
+ require 'quonfig/evaluation'
28
+ require 'quonfig/encryption'
29
+ require 'quonfig/exponential_backoff'
30
+ require 'quonfig/periodic_sync'
31
+ require 'quonfig/errors/initialization_timeout_error'
32
+ require 'quonfig/errors/invalid_sdk_key_error'
33
+ require 'quonfig/errors/missing_default_error'
34
+ require 'quonfig/errors/env_var_parse_error'
35
+ require 'quonfig/errors/missing_env_var_error'
36
+ require 'quonfig/errors/type_mismatch_error'
37
+ require 'quonfig/errors/uninitialized_error'
38
+ require 'quonfig/options'
39
+ require 'quonfig/rate_limit_cache'
40
+ require 'quonfig/weighted_value_resolver'
41
+ require 'quonfig/config_store'
42
+ require 'quonfig/evaluator'
43
+ require 'quonfig/resolver'
44
+ require 'quonfig/config_envelope'
45
+ require 'quonfig/config_loader'
46
+ require 'quonfig/datadir'
47
+ require 'quonfig/sse_config_client'
48
+ require 'quonfig/http_connection'
49
+ require 'quonfig/caching_http_connection'
50
+ require 'quonfig/context'
51
+ require 'quonfig/client'
52
+ require 'quonfig/bound_client'
53
+ require 'quonfig/semantic_logger_filter'
54
+ require 'quonfig/quonfig'
55
+ require 'quonfig/murmer3'
56
+ require 'quonfig/semver'
57
+ require 'quonfig/fixed_size_hash'
data/quonfig.gemspec ADDED
@@ -0,0 +1,149 @@
1
+ # Generated by juwelier
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: quonfig 0.0.2 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "quonfig".freeze
9
+ s.version = "0.0.2".freeze
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib".freeze]
13
+ s.authors = ["Jeff Dwyer".freeze]
14
+ s.date = "2026-04-22"
15
+ s.description = "Quonfig \u2014 feature flags and live config, stored as files in git.".freeze
16
+ s.email = "jeff@quonfig.com".freeze
17
+ s.extra_rdoc_files = [
18
+ "CHANGELOG.md",
19
+ "LICENSE.txt",
20
+ "README.md"
21
+ ]
22
+ s.files = [
23
+ ".claude/rules/constitution.md",
24
+ ".claude/rules/git-safety.md",
25
+ ".claude/rules/issue-tracking.md",
26
+ ".claude/rules/testing-workflow.md",
27
+ ".envrc.sample",
28
+ ".github/CODEOWNERS",
29
+ ".github/pull_request_template.md",
30
+ ".github/workflows/push_gem.yml",
31
+ ".github/workflows/ruby.yml",
32
+ ".github/workflows/test.yaml",
33
+ ".rubocop.yml",
34
+ ".tool-versions",
35
+ "CHANGELOG.md",
36
+ "CLAUDE.md",
37
+ "CODEOWNERS",
38
+ "Gemfile",
39
+ "Gemfile.lock",
40
+ "LICENSE.txt",
41
+ "README.md",
42
+ "Rakefile",
43
+ "VERSION",
44
+ "dev/allocation_stats",
45
+ "dev/benchmark",
46
+ "dev/console",
47
+ "dev/script_setup.rb",
48
+ "lib/quonfig.rb",
49
+ "lib/quonfig/bound_client.rb",
50
+ "lib/quonfig/caching_http_connection.rb",
51
+ "lib/quonfig/client.rb",
52
+ "lib/quonfig/config_envelope.rb",
53
+ "lib/quonfig/config_loader.rb",
54
+ "lib/quonfig/config_store.rb",
55
+ "lib/quonfig/context.rb",
56
+ "lib/quonfig/datadir.rb",
57
+ "lib/quonfig/duration.rb",
58
+ "lib/quonfig/encryption.rb",
59
+ "lib/quonfig/error.rb",
60
+ "lib/quonfig/errors/env_var_parse_error.rb",
61
+ "lib/quonfig/errors/initialization_timeout_error.rb",
62
+ "lib/quonfig/errors/invalid_sdk_key_error.rb",
63
+ "lib/quonfig/errors/missing_default_error.rb",
64
+ "lib/quonfig/errors/missing_env_var_error.rb",
65
+ "lib/quonfig/errors/type_mismatch_error.rb",
66
+ "lib/quonfig/errors/uninitialized_error.rb",
67
+ "lib/quonfig/evaluation.rb",
68
+ "lib/quonfig/evaluator.rb",
69
+ "lib/quonfig/exponential_backoff.rb",
70
+ "lib/quonfig/fixed_size_hash.rb",
71
+ "lib/quonfig/http_connection.rb",
72
+ "lib/quonfig/internal_logger.rb",
73
+ "lib/quonfig/murmer3.rb",
74
+ "lib/quonfig/options.rb",
75
+ "lib/quonfig/periodic_sync.rb",
76
+ "lib/quonfig/quonfig.rb",
77
+ "lib/quonfig/rate_limit_cache.rb",
78
+ "lib/quonfig/reason.rb",
79
+ "lib/quonfig/resolver.rb",
80
+ "lib/quonfig/semantic_logger_filter.rb",
81
+ "lib/quonfig/semver.rb",
82
+ "lib/quonfig/sse_config_client.rb",
83
+ "lib/quonfig/time_helpers.rb",
84
+ "lib/quonfig/types.rb",
85
+ "lib/quonfig/weighted_value_resolver.rb",
86
+ "quonfig.gemspec",
87
+ "scripts/generate_integration_tests.rb",
88
+ "test/fixtures/datafile.json",
89
+ "test/integration/test_context_precedence.rb",
90
+ "test/integration/test_datadir_environment.rb",
91
+ "test/integration/test_enabled.rb",
92
+ "test/integration/test_enabled_with_contexts.rb",
93
+ "test/integration/test_get.rb",
94
+ "test/integration/test_get_feature_flag.rb",
95
+ "test/integration/test_get_or_raise.rb",
96
+ "test/integration/test_get_weighted_values.rb",
97
+ "test/integration/test_helpers.rb",
98
+ "test/integration/test_helpers_test.rb",
99
+ "test/integration/test_post.rb",
100
+ "test/integration/test_telemetry.rb",
101
+ "test/support/common_helpers.rb",
102
+ "test/support/mock_base_client.rb",
103
+ "test/support/mock_config_loader.rb",
104
+ "test/test_bound_client.rb",
105
+ "test/test_caching_http_connection.rb",
106
+ "test/test_client.rb",
107
+ "test/test_config_loader.rb",
108
+ "test/test_context.rb",
109
+ "test/test_datadir.rb",
110
+ "test/test_duration.rb",
111
+ "test/test_encryption.rb",
112
+ "test/test_evaluator.rb",
113
+ "test/test_exponential_backoff.rb",
114
+ "test/test_fixed_size_hash.rb",
115
+ "test/test_helper.rb",
116
+ "test/test_http_connection.rb",
117
+ "test/test_internal_logger.rb",
118
+ "test/test_options.rb",
119
+ "test/test_rate_limit_cache.rb",
120
+ "test/test_reason.rb",
121
+ "test/test_rename.rb",
122
+ "test/test_resolver.rb",
123
+ "test/test_semantic_logger_filter.rb",
124
+ "test/test_semver.rb",
125
+ "test/test_sse_config_client.rb",
126
+ "test/test_typed_getters.rb",
127
+ "test/test_types.rb",
128
+ "test/test_weighted_value_resolver.rb"
129
+ ]
130
+ s.homepage = "https://github.com/quonfig/sdk-ruby".freeze
131
+ s.licenses = ["MIT".freeze]
132
+ s.rubygems_version = "3.5.22".freeze
133
+ s.summary = "Quonfig Ruby SDK".freeze
134
+
135
+ s.specification_version = 4
136
+
137
+ s.add_runtime_dependency(%q<concurrent-ruby>.freeze, ["~> 1.0".freeze, ">= 1.0.5".freeze])
138
+ s.add_runtime_dependency(%q<faraday>.freeze, [">= 0".freeze])
139
+ s.add_runtime_dependency(%q<ld-eventsource>.freeze, [">= 0".freeze])
140
+ s.add_runtime_dependency(%q<uuid>.freeze, [">= 0".freeze])
141
+ s.add_runtime_dependency(%q<activesupport>.freeze, [">= 4".freeze])
142
+ s.add_development_dependency(%q<allocation_stats>.freeze, [">= 0".freeze])
143
+ s.add_development_dependency(%q<benchmark-ips>.freeze, [">= 0".freeze])
144
+ s.add_development_dependency(%q<bundler>.freeze, [">= 0".freeze])
145
+ s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.4.9".freeze])
146
+ s.add_development_dependency(%q<rdoc>.freeze, [">= 0".freeze])
147
+ s.add_development_dependency(%q<simplecov>.freeze, [">= 0".freeze])
148
+ end
149
+