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.
- checksums.yaml +7 -0
- data/.claude/rules/constitution.md +81 -0
- data/.claude/rules/git-safety.md +11 -0
- data/.claude/rules/issue-tracking.md +13 -0
- data/.claude/rules/testing-workflow.md +28 -0
- data/.envrc.sample +3 -0
- data/.github/CODEOWNERS +2 -0
- data/.github/pull_request_template.md +8 -0
- data/.github/workflows/push_gem.yml +49 -0
- data/.github/workflows/ruby.yml +60 -0
- data/.github/workflows/test.yaml +40 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +301 -0
- data/CLAUDE.md +29 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +177 -0
- data/LICENSE.txt +20 -0
- data/README.md +213 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/dev/allocation_stats +60 -0
- data/dev/benchmark +40 -0
- data/dev/console +12 -0
- data/dev/script_setup.rb +18 -0
- data/lib/quonfig/bound_client.rb +71 -0
- data/lib/quonfig/caching_http_connection.rb +95 -0
- data/lib/quonfig/client.rb +221 -0
- data/lib/quonfig/config_envelope.rb +5 -0
- data/lib/quonfig/config_loader.rb +103 -0
- data/lib/quonfig/config_store.rb +42 -0
- data/lib/quonfig/context.rb +101 -0
- data/lib/quonfig/datadir.rb +101 -0
- data/lib/quonfig/duration.rb +58 -0
- data/lib/quonfig/encryption.rb +74 -0
- data/lib/quonfig/error.rb +6 -0
- data/lib/quonfig/errors/env_var_parse_error.rb +11 -0
- data/lib/quonfig/errors/initialization_timeout_error.rb +12 -0
- data/lib/quonfig/errors/invalid_sdk_key_error.rb +19 -0
- data/lib/quonfig/errors/missing_default_error.rb +13 -0
- data/lib/quonfig/errors/missing_env_var_error.rb +11 -0
- data/lib/quonfig/errors/type_mismatch_error.rb +11 -0
- data/lib/quonfig/errors/uninitialized_error.rb +13 -0
- data/lib/quonfig/evaluation.rb +64 -0
- data/lib/quonfig/evaluator.rb +464 -0
- data/lib/quonfig/exponential_backoff.rb +21 -0
- data/lib/quonfig/fixed_size_hash.rb +14 -0
- data/lib/quonfig/http_connection.rb +46 -0
- data/lib/quonfig/internal_logger.rb +173 -0
- data/lib/quonfig/murmer3.rb +50 -0
- data/lib/quonfig/options.rb +194 -0
- data/lib/quonfig/periodic_sync.rb +74 -0
- data/lib/quonfig/quonfig.rb +58 -0
- data/lib/quonfig/rate_limit_cache.rb +41 -0
- data/lib/quonfig/reason.rb +39 -0
- data/lib/quonfig/resolver.rb +42 -0
- data/lib/quonfig/semantic_logger_filter.rb +90 -0
- data/lib/quonfig/semver.rb +132 -0
- data/lib/quonfig/sse_config_client.rb +135 -0
- data/lib/quonfig/time_helpers.rb +7 -0
- data/lib/quonfig/types.rb +56 -0
- data/lib/quonfig/weighted_value_resolver.rb +49 -0
- data/lib/quonfig.rb +57 -0
- data/quonfig.gemspec +149 -0
- data/scripts/generate_integration_tests.rb +362 -0
- data/test/fixtures/datafile.json +87 -0
- data/test/integration/test_context_precedence.rb +194 -0
- data/test/integration/test_datadir_environment.rb +76 -0
- data/test/integration/test_enabled.rb +784 -0
- data/test/integration/test_enabled_with_contexts.rb +94 -0
- data/test/integration/test_get.rb +224 -0
- data/test/integration/test_get_feature_flag.rb +34 -0
- data/test/integration/test_get_or_raise.rb +86 -0
- data/test/integration/test_get_weighted_values.rb +29 -0
- data/test/integration/test_helpers.rb +139 -0
- data/test/integration/test_helpers_test.rb +73 -0
- data/test/integration/test_post.rb +34 -0
- data/test/integration/test_telemetry.rb +114 -0
- data/test/support/common_helpers.rb +106 -0
- data/test/support/mock_base_client.rb +27 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_bound_client.rb +109 -0
- data/test/test_caching_http_connection.rb +218 -0
- data/test/test_client.rb +255 -0
- data/test/test_config_loader.rb +70 -0
- data/test/test_context.rb +136 -0
- data/test/test_datadir.rb +199 -0
- data/test/test_duration.rb +37 -0
- data/test/test_encryption.rb +16 -0
- data/test/test_evaluator.rb +285 -0
- data/test/test_exponential_backoff.rb +44 -0
- data/test/test_fixed_size_hash.rb +119 -0
- data/test/test_helper.rb +17 -0
- data/test/test_http_connection.rb +79 -0
- data/test/test_internal_logger.rb +34 -0
- data/test/test_options.rb +167 -0
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_reason.rb +79 -0
- data/test/test_rename.rb +65 -0
- data/test/test_resolver.rb +144 -0
- data/test/test_semantic_logger_filter.rb +123 -0
- data/test/test_semver.rb +108 -0
- data/test/test_sse_config_client.rb +297 -0
- data/test/test_typed_getters.rb +131 -0
- data/test/test_types.rb +141 -0
- data/test/test_weighted_value_resolver.rb +84 -0
- metadata +311 -0
data/Gemfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
source 'https://rubygems.org'
|
|
2
|
+
|
|
3
|
+
gem 'concurrent-ruby', '~> 1.0', '>= 1.0.5'
|
|
4
|
+
gem 'faraday'
|
|
5
|
+
gem 'ld-eventsource'
|
|
6
|
+
gem 'uuid'
|
|
7
|
+
|
|
8
|
+
gem 'activesupport', '>= 4'
|
|
9
|
+
|
|
10
|
+
group :development do
|
|
11
|
+
gem 'allocation_stats'
|
|
12
|
+
gem 'benchmark-ips'
|
|
13
|
+
gem 'bundler'
|
|
14
|
+
gem 'juwelier', '~> 2.4.9'
|
|
15
|
+
gem 'rdoc'
|
|
16
|
+
gem 'simplecov', '>= 0'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
group :test do
|
|
20
|
+
gem 'semantic_logger', '!= 4.16.0', require: "semantic_logger/sync"
|
|
21
|
+
gem 'minitest'
|
|
22
|
+
gem 'minitest-focus'
|
|
23
|
+
gem 'minitest-reporters'
|
|
24
|
+
gem 'timecop'
|
|
25
|
+
gem 'webrick'
|
|
26
|
+
end
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
GEM
|
|
2
|
+
remote: https://rubygems.org/
|
|
3
|
+
specs:
|
|
4
|
+
activesupport (7.1.3.2)
|
|
5
|
+
base64
|
|
6
|
+
bigdecimal
|
|
7
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
8
|
+
connection_pool (>= 2.2.5)
|
|
9
|
+
drb
|
|
10
|
+
i18n (>= 1.6, < 2)
|
|
11
|
+
minitest (>= 5.1)
|
|
12
|
+
mutex_m
|
|
13
|
+
tzinfo (~> 2.0)
|
|
14
|
+
addressable (2.8.6)
|
|
15
|
+
public_suffix (>= 2.0.2, < 6.0)
|
|
16
|
+
allocation_stats (0.1.5)
|
|
17
|
+
ansi (1.5.0)
|
|
18
|
+
base64 (0.2.0)
|
|
19
|
+
benchmark-ips (2.13.0)
|
|
20
|
+
bigdecimal (3.1.7)
|
|
21
|
+
builder (3.2.4)
|
|
22
|
+
concurrent-ruby (1.2.3)
|
|
23
|
+
connection_pool (2.4.1)
|
|
24
|
+
descendants_tracker (0.0.4)
|
|
25
|
+
thread_safe (~> 0.3, >= 0.3.1)
|
|
26
|
+
docile (1.4.0)
|
|
27
|
+
domain_name (0.6.20240107)
|
|
28
|
+
drb (2.2.1)
|
|
29
|
+
faraday (1.10.3)
|
|
30
|
+
faraday-em_http (~> 1.0)
|
|
31
|
+
faraday-em_synchrony (~> 1.0)
|
|
32
|
+
faraday-excon (~> 1.1)
|
|
33
|
+
faraday-httpclient (~> 1.0)
|
|
34
|
+
faraday-multipart (~> 1.0)
|
|
35
|
+
faraday-net_http (~> 1.0)
|
|
36
|
+
faraday-net_http_persistent (~> 1.0)
|
|
37
|
+
faraday-patron (~> 1.0)
|
|
38
|
+
faraday-rack (~> 1.0)
|
|
39
|
+
faraday-retry (~> 1.0)
|
|
40
|
+
ruby2_keywords (>= 0.0.4)
|
|
41
|
+
faraday-em_http (1.0.0)
|
|
42
|
+
faraday-em_synchrony (1.0.0)
|
|
43
|
+
faraday-excon (1.1.0)
|
|
44
|
+
faraday-httpclient (1.0.1)
|
|
45
|
+
faraday-multipart (1.0.4)
|
|
46
|
+
multipart-post (~> 2)
|
|
47
|
+
faraday-net_http (1.0.1)
|
|
48
|
+
faraday-net_http_persistent (1.2.0)
|
|
49
|
+
faraday-patron (1.0.0)
|
|
50
|
+
faraday-rack (1.0.0)
|
|
51
|
+
faraday-retry (1.0.3)
|
|
52
|
+
ffi (1.17.4)
|
|
53
|
+
ffi-compiler (1.3.2)
|
|
54
|
+
ffi (>= 1.15.5)
|
|
55
|
+
rake
|
|
56
|
+
git (1.19.1)
|
|
57
|
+
addressable (~> 2.8)
|
|
58
|
+
rchardet (~> 1.8)
|
|
59
|
+
github_api (0.19.0)
|
|
60
|
+
addressable (~> 2.4)
|
|
61
|
+
descendants_tracker (~> 0.0.4)
|
|
62
|
+
faraday (>= 0.8, < 2)
|
|
63
|
+
hashie (~> 3.5, >= 3.5.2)
|
|
64
|
+
oauth2 (~> 1.0)
|
|
65
|
+
hashie (3.6.0)
|
|
66
|
+
highline (3.0.1)
|
|
67
|
+
http (5.2.0)
|
|
68
|
+
addressable (~> 2.8)
|
|
69
|
+
base64 (~> 0.1)
|
|
70
|
+
http-cookie (~> 1.0)
|
|
71
|
+
http-form_data (~> 2.2)
|
|
72
|
+
llhttp-ffi (~> 0.5.0)
|
|
73
|
+
http-cookie (1.0.5)
|
|
74
|
+
domain_name (~> 0.5)
|
|
75
|
+
http-form_data (2.3.0)
|
|
76
|
+
i18n (1.14.4)
|
|
77
|
+
concurrent-ruby (~> 1.0)
|
|
78
|
+
juwelier (2.4.9)
|
|
79
|
+
builder
|
|
80
|
+
bundler
|
|
81
|
+
git
|
|
82
|
+
github_api
|
|
83
|
+
highline
|
|
84
|
+
kamelcase (~> 0)
|
|
85
|
+
nokogiri
|
|
86
|
+
psych
|
|
87
|
+
rake
|
|
88
|
+
rdoc
|
|
89
|
+
semver2
|
|
90
|
+
jwt (2.8.1)
|
|
91
|
+
base64
|
|
92
|
+
kamelcase (0.0.2)
|
|
93
|
+
semver2 (~> 3)
|
|
94
|
+
ld-eventsource (2.2.2)
|
|
95
|
+
concurrent-ruby (~> 1.0)
|
|
96
|
+
http (>= 4.4.1, < 6.0.0)
|
|
97
|
+
llhttp-ffi (0.5.0)
|
|
98
|
+
ffi-compiler (~> 1.0)
|
|
99
|
+
rake (~> 13.0)
|
|
100
|
+
macaddr (1.7.2)
|
|
101
|
+
systemu (~> 2.6.5)
|
|
102
|
+
mini_portile2 (2.8.9)
|
|
103
|
+
minitest (5.22.3)
|
|
104
|
+
minitest-focus (1.4.0)
|
|
105
|
+
minitest (>= 4, < 6)
|
|
106
|
+
minitest-reporters (1.6.1)
|
|
107
|
+
ansi
|
|
108
|
+
builder
|
|
109
|
+
minitest (>= 5.0)
|
|
110
|
+
ruby-progressbar
|
|
111
|
+
multi_json (1.15.0)
|
|
112
|
+
multi_xml (0.6.0)
|
|
113
|
+
multipart-post (2.4.0)
|
|
114
|
+
mutex_m (0.2.0)
|
|
115
|
+
nokogiri (1.18.9)
|
|
116
|
+
mini_portile2 (~> 2.8.2)
|
|
117
|
+
racc (~> 1.4)
|
|
118
|
+
oauth2 (1.4.11)
|
|
119
|
+
faraday (>= 0.17.3, < 3.0)
|
|
120
|
+
jwt (>= 1.0, < 3.0)
|
|
121
|
+
multi_json (~> 1.3)
|
|
122
|
+
multi_xml (~> 0.5)
|
|
123
|
+
rack (>= 1.2, < 4)
|
|
124
|
+
psych (5.1.2)
|
|
125
|
+
stringio
|
|
126
|
+
public_suffix (5.0.4)
|
|
127
|
+
racc (1.8.1)
|
|
128
|
+
rack (3.1.18)
|
|
129
|
+
rake (13.1.0)
|
|
130
|
+
rchardet (1.8.0)
|
|
131
|
+
rdoc (6.6.3.1)
|
|
132
|
+
psych (>= 4.0.0)
|
|
133
|
+
ruby-progressbar (1.13.0)
|
|
134
|
+
ruby2_keywords (0.0.5)
|
|
135
|
+
semantic_logger (4.15.0)
|
|
136
|
+
concurrent-ruby (~> 1.0)
|
|
137
|
+
semver2 (3.4.2)
|
|
138
|
+
simplecov (0.22.0)
|
|
139
|
+
docile (~> 1.1)
|
|
140
|
+
simplecov-html (~> 0.11)
|
|
141
|
+
simplecov_json_formatter (~> 0.1)
|
|
142
|
+
simplecov-html (0.12.3)
|
|
143
|
+
simplecov_json_formatter (0.1.4)
|
|
144
|
+
stringio (3.1.0)
|
|
145
|
+
systemu (2.6.5)
|
|
146
|
+
thread_safe (0.3.6)
|
|
147
|
+
timecop (0.9.8)
|
|
148
|
+
tzinfo (2.0.6)
|
|
149
|
+
concurrent-ruby (~> 1.0)
|
|
150
|
+
uuid (2.3.9)
|
|
151
|
+
macaddr (~> 1.0)
|
|
152
|
+
webrick (1.8.2)
|
|
153
|
+
|
|
154
|
+
PLATFORMS
|
|
155
|
+
ruby
|
|
156
|
+
|
|
157
|
+
DEPENDENCIES
|
|
158
|
+
activesupport (>= 4)
|
|
159
|
+
allocation_stats
|
|
160
|
+
benchmark-ips
|
|
161
|
+
bundler
|
|
162
|
+
concurrent-ruby (~> 1.0, >= 1.0.5)
|
|
163
|
+
faraday
|
|
164
|
+
juwelier (~> 2.4.9)
|
|
165
|
+
ld-eventsource
|
|
166
|
+
minitest
|
|
167
|
+
minitest-focus
|
|
168
|
+
minitest-reporters
|
|
169
|
+
rdoc
|
|
170
|
+
semantic_logger (!= 4.16.0)
|
|
171
|
+
simplecov
|
|
172
|
+
timecop
|
|
173
|
+
uuid
|
|
174
|
+
webrick
|
|
175
|
+
|
|
176
|
+
BUNDLED WITH
|
|
177
|
+
2.3.5
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2023 Prefab, Inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# quonfig
|
|
2
|
+
|
|
3
|
+
Ruby SDK for [Quonfig](https://quonfig.com) — Feature Flags, Live Config, and Dynamic Log Levels.
|
|
4
|
+
|
|
5
|
+
> **Note:** This SDK is pre-1.0 and the API is not yet stable.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add the gem to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'quonfig'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gem install quonfig
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require 'quonfig'
|
|
25
|
+
|
|
26
|
+
client = Quonfig::Client.new(sdk_key: ENV['QUONFIG_BACKEND_SDK_KEY'])
|
|
27
|
+
|
|
28
|
+
# Feature flags
|
|
29
|
+
if client.enabled?('new-dashboard')
|
|
30
|
+
# show new dashboard
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Typed config values
|
|
34
|
+
limit = client.get_int('rate-limit')
|
|
35
|
+
name = client.get_string('app.display-name')
|
|
36
|
+
regions = client.get_string_list('allowed-regions')
|
|
37
|
+
|
|
38
|
+
# Context-aware evaluation — pass a context hash as the last argument
|
|
39
|
+
value = client.get_string('homepage-hero', user: { key: 'user-123', country: 'US' })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Context
|
|
43
|
+
|
|
44
|
+
Contexts are hashes grouped by scope (`user`, `team`, `device`, etc.). You can
|
|
45
|
+
attach a context in three ways:
|
|
46
|
+
|
|
47
|
+
### 1. Per-call context
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
client.get_bool('beta-feature', user: { key: 'user-123', plan: 'pro' })
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. `in_context` block
|
|
54
|
+
|
|
55
|
+
Everything evaluated inside the block sees the supplied context. The block's
|
|
56
|
+
return value is returned from `in_context`.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
result = client.in_context(user: { key: 'user-123', plan: 'pro' }) do |bound|
|
|
60
|
+
{
|
|
61
|
+
hero: bound.get_string('homepage-hero'),
|
|
62
|
+
limit: bound.get_int('rate-limit'),
|
|
63
|
+
beta?: bound.enabled?('beta-feature')
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3. `with_context` — BoundClient for repeated lookups
|
|
69
|
+
|
|
70
|
+
`with_context` returns an immutable `BoundClient` that carries the context on
|
|
71
|
+
every call. Useful when you want to pass a context-bound handle down the stack.
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
bound = client.with_context(user: { key: 'user-123', plan: 'pro' })
|
|
75
|
+
|
|
76
|
+
bound.get_string('homepage-hero')
|
|
77
|
+
bound.enabled?('beta-feature')
|
|
78
|
+
bound.get_int('rate-limit')
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Datadir / offline mode
|
|
82
|
+
|
|
83
|
+
For tests, CI, or air-gapped environments, point the client at a local workspace
|
|
84
|
+
directory instead of the Quonfig API. In datadir mode the SDK loads JSON config
|
|
85
|
+
files from disk and performs no network I/O.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
client = Quonfig::Client.new(
|
|
89
|
+
datadir: '/path/to/workspace',
|
|
90
|
+
environment: 'production'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
client.get_bool('feature-x')
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
You can also set `QUONFIG_DIR` in the environment and omit the `datadir:`
|
|
97
|
+
option; when `QUONFIG_DIR` is set the SDK switches to datadir mode
|
|
98
|
+
automatically. `environment` is required in datadir mode — it can be provided
|
|
99
|
+
via the option or via `QUONFIG_ENVIRONMENT`.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
export QUONFIG_DIR=/path/to/workspace
|
|
103
|
+
export QUONFIG_ENVIRONMENT=production
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
client = Quonfig::Client.new # reads QUONFIG_DIR + QUONFIG_ENVIRONMENT
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Environment variables
|
|
111
|
+
|
|
112
|
+
| Variable | Purpose |
|
|
113
|
+
|-----------------------------|------------------------------------------------------------------------------------------|
|
|
114
|
+
| `QUONFIG_BACKEND_SDK_KEY` | SDK key used to authenticate against the Quonfig API. Used when `sdk_key:` is omitted. |
|
|
115
|
+
| `QUONFIG_DIR` | Path to a workspace directory. When set, the SDK runs in datadir/offline mode. |
|
|
116
|
+
| `QUONFIG_ENVIRONMENT` | Environment name (`production`, `staging`, `development`) evaluated in datadir mode. |
|
|
117
|
+
| `QUONFIG_TELEMETRY_URL` | Overrides the telemetry endpoint. Defaults to `https://telemetry.quonfig.com`. |
|
|
118
|
+
|
|
119
|
+
## Constructor options
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
Quonfig::Client.new(
|
|
123
|
+
sdk_key: '...', # required unless QUONFIG_BACKEND_SDK_KEY is set
|
|
124
|
+
api_urls: ['https://primary.quonfig.com'],
|
|
125
|
+
telemetry_url: 'https://telemetry.quonfig.com',
|
|
126
|
+
enable_sse: true,
|
|
127
|
+
enable_polling: false,
|
|
128
|
+
poll_interval: 60,
|
|
129
|
+
init_timeout: 10,
|
|
130
|
+
on_no_default: :error,
|
|
131
|
+
global_context: {},
|
|
132
|
+
datadir: '/path/to/workspace',
|
|
133
|
+
environment: 'production'
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| Option | Type | Default | Description |
|
|
138
|
+
|-------------------|----------------------------|---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
|
|
139
|
+
| `sdk_key` | `String` | `ENV['QUONFIG_BACKEND_SDK_KEY']` | SDK key for API authentication. |
|
|
140
|
+
| `api_urls` | `Array<String>` | `['https://primary.quonfig.com']` | Ordered list of API base URLs to try. SSE stream URLs are derived by prepending `stream.` to each hostname. |
|
|
141
|
+
| `telemetry_url` | `String` | `https://telemetry.quonfig.com` (or `ENV['QUONFIG_TELEMETRY_URL']`) | Base URL for the telemetry service. |
|
|
142
|
+
| `enable_sse` | `Boolean` | `true` | Receive real-time updates over Server-Sent Events. |
|
|
143
|
+
| `enable_polling` | `Boolean` | `false` | Poll the API on an interval as a fallback. |
|
|
144
|
+
| `poll_interval` | `Integer` (seconds) | `60` | Polling interval when `enable_polling` is `true`. |
|
|
145
|
+
| `init_timeout` | `Integer` (seconds) | `10` | Maximum time to wait for the initial config load. |
|
|
146
|
+
| `on_no_default` | `Symbol` | `:error` | Behavior when a key has no value and no default: `:error`, `:warn`, or `:ignore`. |
|
|
147
|
+
| `global_context` | `Hash` | `{}` | Context applied to every evaluation. |
|
|
148
|
+
| `datadir` | `String` | `ENV['QUONFIG_DIR']` | Path to a local workspace. When set, the SDK runs offline from disk. |
|
|
149
|
+
| `environment` | `String` | `ENV['QUONFIG_ENVIRONMENT']` | Environment to evaluate in datadir mode. Required when `datadir` is set. |
|
|
150
|
+
|
|
151
|
+
## Typed getters
|
|
152
|
+
|
|
153
|
+
Each typed getter takes a config key and an optional context hash. If the key
|
|
154
|
+
is missing or the stored value does not match the requested type, the getter
|
|
155
|
+
returns `nil`.
|
|
156
|
+
|
|
157
|
+
| Method | Returns |
|
|
158
|
+
|-------------------------------------------------|-------------------------------|
|
|
159
|
+
| `get_string(key, contexts = nil)` | `String` or `nil` |
|
|
160
|
+
| `get_int(key, contexts = nil)` | `Integer` or `nil` |
|
|
161
|
+
| `get_float(key, contexts = nil)` | `Float` or `nil` |
|
|
162
|
+
| `get_bool(key, contexts = nil)` | `true`, `false`, or `nil` |
|
|
163
|
+
| `get_string_list(key, contexts = nil)` | `Array<String>` or `nil` |
|
|
164
|
+
| `get_duration(key, contexts = nil)` | `Float` (seconds) or `nil` |
|
|
165
|
+
| `get_json(key, contexts = nil)` | `Hash`, `Array`, or `nil` |
|
|
166
|
+
| `enabled?(feature_name, contexts = nil)` | `true` or `false` |
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
client.get_string('app.display-name')
|
|
172
|
+
client.get_int('rate-limit', user: { key: 'user-123' })
|
|
173
|
+
client.get_float('pricing.multiplier')
|
|
174
|
+
client.get_bool('flags.new-checkout')
|
|
175
|
+
client.get_string_list('allowed-regions')
|
|
176
|
+
client.get_duration('request-timeout')
|
|
177
|
+
client.get_json('homepage.layout')
|
|
178
|
+
client.enabled?('beta-feature', user: { key: 'user-123' })
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Dynamic log levels (SemanticLogger)
|
|
182
|
+
|
|
183
|
+
Quonfig can drive per-class log levels at runtime. Set config keys like
|
|
184
|
+
`log-levels.my_app.foo.bar` to one of `trace`, `debug`, `info`, `warn`, `error`,
|
|
185
|
+
`fatal` and wire the filter into SemanticLogger:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
require 'quonfig'
|
|
189
|
+
require 'semantic_logger'
|
|
190
|
+
|
|
191
|
+
client = Quonfig::Client.new(sdk_key: ENV['QUONFIG_BACKEND_SDK_KEY'])
|
|
192
|
+
SemanticLogger.add_appender(io: $stdout, filter: client.semantic_logger_filter)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Lookup is exact-match only: logger name `MyApp::Foo::Bar` normalizes to
|
|
196
|
+
`log-levels.my_app.foo.bar`. If no key is set the log is allowed through and
|
|
197
|
+
SemanticLogger's static level decides. There is no hierarchy walk — a value on
|
|
198
|
+
`log-levels.my_app` does not affect `log-levels.my_app.foo.bar`.
|
|
199
|
+
|
|
200
|
+
Pass `key_prefix:` to use a prefix other than `log-levels.`:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
client.semantic_logger_filter(key_prefix: 'debug.')
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Documentation
|
|
207
|
+
|
|
208
|
+
Full documentation, including SPEC, SDK reference, and operational guides, is
|
|
209
|
+
available at [https://quonfig.com/docs](https://quonfig.com/docs).
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'bundler'
|
|
5
|
+
begin
|
|
6
|
+
Bundler.setup(:default, :development)
|
|
7
|
+
rescue Bundler::BundlerError => e
|
|
8
|
+
warn e.message
|
|
9
|
+
warn 'Run `bundle install` to install missing gems'
|
|
10
|
+
exit e.status_code
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require 'rake'
|
|
14
|
+
|
|
15
|
+
require 'rake/testtask'
|
|
16
|
+
Rake::TestTask.new(:test) do |test|
|
|
17
|
+
test.libs << 'lib' << 'test'
|
|
18
|
+
test.pattern = 'test/**/test_*.rb'
|
|
19
|
+
test.verbose = true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
task default: :test
|
|
23
|
+
|
|
24
|
+
unless ENV['CI']
|
|
25
|
+
require 'juwelier'
|
|
26
|
+
Juwelier::Tasks.new do |gem|
|
|
27
|
+
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
|
28
|
+
gem.name = 'quonfig'
|
|
29
|
+
gem.homepage = 'https://github.com/quonfig/sdk-ruby'
|
|
30
|
+
gem.license = 'MIT'
|
|
31
|
+
gem.summary = %(Quonfig Ruby SDK)
|
|
32
|
+
gem.description = %(Quonfig — feature flags and live config, stored as files in git.)
|
|
33
|
+
gem.email = 'jeff@quonfig.com'
|
|
34
|
+
gem.authors = ['Jeff Dwyer']
|
|
35
|
+
|
|
36
|
+
# dependencies defined in Gemfile
|
|
37
|
+
end
|
|
38
|
+
Juwelier::RubygemsDotOrgTasks.new
|
|
39
|
+
|
|
40
|
+
desc 'Code coverage detail'
|
|
41
|
+
task :simplecov do
|
|
42
|
+
ENV['COVERAGE'] = 'true'
|
|
43
|
+
Rake::Task['test'].execute
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
require 'rdoc/task'
|
|
47
|
+
Rake::RDocTask.new do |rdoc|
|
|
48
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ''
|
|
49
|
+
|
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
51
|
+
rdoc.title = "quonfig #{version}"
|
|
52
|
+
rdoc.rdoc_files.include('README*')
|
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Add release task for CI
|
|
58
|
+
task :release do
|
|
59
|
+
sh 'mkdir -p pkg'
|
|
60
|
+
version = File.read('VERSION').strip
|
|
61
|
+
gem_file = "pkg/quonfig-#{version}.gem"
|
|
62
|
+
sh "gem build quonfig.gemspec --output #{gem_file}"
|
|
63
|
+
sh "gem push #{gem_file}"
|
|
64
|
+
end
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.2
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
|
|
6
|
+
gemspec = Dir.glob(File.expand_path("../../*.gemspec", __FILE__)).first
|
|
7
|
+
spec = Gem::Specification.load(gemspec)
|
|
8
|
+
|
|
9
|
+
# Add the require paths to the $LOAD_PATH
|
|
10
|
+
spec.require_paths.each do |path|
|
|
11
|
+
full_path = File.expand_path("../" + path, __dir__)
|
|
12
|
+
$LOAD_PATH.unshift(full_path) unless $LOAD_PATH.include?(full_path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
spec.require_paths.each do |path|
|
|
16
|
+
require "./lib/reforge-sdk"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require 'reforge-sdk'
|
|
20
|
+
|
|
21
|
+
$prefab = Reforge::Client.new(collect_logger_counts: false, collect_evaluation_summaries: false,
|
|
22
|
+
context_upload_mode: :none)
|
|
23
|
+
$prefab.get('a.live.integer')
|
|
24
|
+
|
|
25
|
+
puts '-' * 80
|
|
26
|
+
|
|
27
|
+
require 'allocation_stats'
|
|
28
|
+
|
|
29
|
+
$runs = 100
|
|
30
|
+
|
|
31
|
+
def measure(description)
|
|
32
|
+
puts "Measuring #{description}..."
|
|
33
|
+
stats = $prefab.with_context(user: { email_suffix: 'yahoo.com' }) do
|
|
34
|
+
AllocationStats.trace do
|
|
35
|
+
$runs.times do
|
|
36
|
+
yield
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
allocations = stats.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class)
|
|
42
|
+
|
|
43
|
+
if ENV['TOP']
|
|
44
|
+
puts allocations.sort_by_size.to_text.split("\n").first(20)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
puts "Total allocations: #{allocations.all.values.map(&:size).sum}"
|
|
48
|
+
puts "Total memory: #{allocations.all.values.flatten.map(&:memsize).sum}"
|
|
49
|
+
puts stats.gc_profiler_report
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
measure "no-JIT context (#{$runs} runs)" do
|
|
53
|
+
$prefab.get('a.live.integer')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts "\n\n"
|
|
57
|
+
|
|
58
|
+
measure "with JIT context (#{$runs} runs)" do
|
|
59
|
+
$prefab.get('a.live.integer', { a: { b: "c" } })
|
|
60
|
+
end
|
data/dev/benchmark
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
|
|
6
|
+
gemspec = Dir.glob(File.expand_path("../../*.gemspec", __FILE__)).first
|
|
7
|
+
spec = Gem::Specification.load(gemspec)
|
|
8
|
+
|
|
9
|
+
# Add the require paths to the $LOAD_PATH
|
|
10
|
+
spec.require_paths.each do |path|
|
|
11
|
+
full_path = File.expand_path("../" + path, __dir__)
|
|
12
|
+
$LOAD_PATH.unshift(full_path) unless $LOAD_PATH.include?(full_path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
spec.require_paths.each do |path|
|
|
16
|
+
require "./lib/reforge-sdk"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require 'reforge-sdk'
|
|
20
|
+
require 'benchmark/ips'
|
|
21
|
+
|
|
22
|
+
prefab = Reforge::Client.new(collect_logger_counts: false, collect_evaluation_summaries: false,
|
|
23
|
+
context_upload_mode: :none)
|
|
24
|
+
|
|
25
|
+
prefab.get('prefab.auth.allowed_origins')
|
|
26
|
+
|
|
27
|
+
prefab.with_context(user: { email_suffix: 'yahoo.com' }) do
|
|
28
|
+
Benchmark.ips do |x|
|
|
29
|
+
x.report("noop") do
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
x.report('prefab.get') do
|
|
33
|
+
prefab.get('prefab.auth.allowed_origins')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
x.report('prefab.get with jit context') do
|
|
37
|
+
prefab.get('prefab.auth.allowed_origins', { a: { b: "c" } })
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/dev/console
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bundle exec ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'irb'
|
|
5
|
+
require_relative "./script_setup"
|
|
6
|
+
|
|
7
|
+
if !ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL']
|
|
8
|
+
puts "run with REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL=debug (or trace) for more output"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Start an IRB session
|
|
12
|
+
IRB.start(__FILE__)
|
data/dev/script_setup.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
gemspec = Dir.glob(File.expand_path("../../*.gemspec", __FILE__)).first
|
|
6
|
+
spec = Gem::Specification.load(gemspec)
|
|
7
|
+
|
|
8
|
+
# Add the require paths to the $LOAD_PATH
|
|
9
|
+
spec.require_paths.each do |path|
|
|
10
|
+
full_path = File.expand_path("../" + path, __dir__)
|
|
11
|
+
$LOAD_PATH.unshift(full_path) unless $LOAD_PATH.include?(full_path)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
spec.require_paths.each do |path|
|
|
15
|
+
require "./lib/quonfig"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
SemanticLogger.add_appender(io: $stdout)
|