featurehub-sdk 1.3.0 → 2.0.0

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/CLAUDE.md +85 -0
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +13 -1
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +20 -8
  7. data/README.md +306 -119
  8. data/examples/rails_example/.ruby-version +1 -1
  9. data/examples/rails_example/Dockerfile +1 -1
  10. data/examples/sinatra/.dockerignore +7 -0
  11. data/examples/sinatra/.ruby-version +1 -1
  12. data/examples/sinatra/Dockerfile +14 -25
  13. data/examples/sinatra/Gemfile +5 -4
  14. data/examples/sinatra/Gemfile.lock +40 -32
  15. data/examples/sinatra/app/application.rb +21 -9
  16. data/examples/sinatra/docker-compose.yaml +24 -0
  17. data/examples/sinatra/feature-flags.yaml +6 -0
  18. data/examples/sinatra/sinatra.iml +35 -14
  19. data/examples/sinatra/start.sh +2 -0
  20. data/featurehub-sdk.gemspec +4 -1
  21. data/lib/feature_hub/sdk/context.rb +28 -7
  22. data/lib/feature_hub/sdk/feature_hub_config.rb +68 -12
  23. data/lib/feature_hub/sdk/feature_repository.rb +52 -13
  24. data/lib/feature_hub/sdk/{feature_state.rb → feature_state_holder.rb} +13 -9
  25. data/lib/feature_hub/sdk/interceptors.rb +10 -6
  26. data/lib/feature_hub/sdk/internal_feature_repository.rb +7 -3
  27. data/lib/feature_hub/sdk/local_yaml_interceptor.rb +99 -0
  28. data/lib/feature_hub/sdk/local_yaml_store.rb +71 -0
  29. data/lib/feature_hub/sdk/poll_edge_service.rb +6 -11
  30. data/lib/feature_hub/sdk/raw_update_feature_listener.rb +19 -0
  31. data/lib/feature_hub/sdk/redis_session_store.rb +130 -0
  32. data/lib/feature_hub/sdk/strategy_attributes.rb +7 -0
  33. data/lib/feature_hub/sdk/streaming_edge_service.rb +4 -6
  34. data/lib/feature_hub/sdk/version.rb +2 -2
  35. data/lib/featurehub-sdk.rb +5 -1
  36. data/sig/feature_hub/featurehub.rbs +127 -28
  37. metadata +27 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cd81cc23b520be9a702b06ab17071c1eddb40bd2a79cc0617c22509f1574cc9
4
- data.tar.gz: e2c3cf069c8635fc574db0d6c5a8dcdef2170058d282747c2b0481f84998078b
3
+ metadata.gz: c3c5127a58f5d1a4c63242919d593399ee0e097dcbd54239584d3620eb00f5a9
4
+ data.tar.gz: 4de3a2a249d35c6dfd52249a6b42171cd27e9aac8ed4c95f2c0efdaa9947663b
5
5
  SHA512:
6
- metadata.gz: 7092b35679401cd732ece4a49366a84e136df729d8bbdc3285da81ade5cb90ffad19a5431c9fe25e9c5b8f9b2c777a144e90859d22c9230c22f3278909de35af
7
- data.tar.gz: a134bea6b240aa6aced1158e5c0bd70c4c3b7754d047387aeb517fdec8b35e2572599971aea11abfcdb477ed68dd2d29657b0747acd1a2c87fc93d59927e1457
6
+ metadata.gz: 9a41c73c154804782a9795337e33de707a482f2fedb41337783222f561ce1eccae41dfca59ad393a36d9fa8d214f8f54778b016092de3c04e4c9d83c9da44a95
7
+ data.tar.gz: aac1dfb0a3d61d77f4046b9710f146ce0f2d0562a7c324e8e03c43c7276c5eb555322f9b3e49543aeb9958b1b4328b15dc003c29da682160a3b29ac782c830af
data/.claude/CLAUDE.md ADDED
@@ -0,0 +1,85 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ bundle install # Install dependencies
9
+ bundle exec rake # Run tests + linting (default)
10
+ bundle exec rspec # Run tests only
11
+ bundle exec rubocop # Lint only
12
+
13
+ # Run a single test file
14
+ bundle exec rspec spec/feature_hub/sdk/feature_hub_config_spec.rb
15
+
16
+ # Run a specific test by line number
17
+ bundle exec rspec spec/feature_hub/sdk/feature_hub_config_spec.rb:42
18
+ ```
19
+
20
+ ## Architecture
21
+
22
+ This is the FeatureHub Ruby SDK (gem: `featurehub-sdk`, version 2.0.0), targeting Ruby >= 3.2. All code lives under `lib/feature_hub/sdk/` and is namespaced as `FeatureHub::Sdk`.
23
+
24
+ ### Core Data Flow
25
+
26
+ ```
27
+ FeatureHubConfig.init(url, api_key)
28
+ → creates EdgeService (streaming SSE or polling HTTP)
29
+ → edge service receives feature JSON from FeatureHub server
30
+ → FeatureHubRepository stores feature states
31
+
32
+ config.new_context().user_key("id").build()
33
+ → ClientEvalContext: polls edge, evaluates strategies locally
34
+ → ServerEvalContext: sends context as HTTP header, server evaluates
35
+
36
+ context.feature("MY_FLAG").boolean
37
+ → FeatureStateHolder checks interceptors, then calls repository.apply(strategies, ctx)
38
+ → ApplyFeature matches rollout strategies, returns Applied(value)
39
+ ```
40
+
41
+ ### Key Classes
42
+
43
+ - **FeatureHubConfig** ([feature_hub_config.rb](lib/feature_hub/sdk/feature_hub_config.rb)): Entry point. Detects client vs server evaluation by checking for `*` in the API key. Manages edge service lifecycle. Call `new_context()` to get a context builder.
44
+
45
+ - **FeatureHubRepository** ([feature_repository.rb](lib/feature_hub/sdk/feature_repository.rb)): Stores features as a hash (symbol keys → `FeatureStateHolder`). Notifies listeners on changes (`:features`, `:feature`, `:delete_feature`, `:failed`). Checks interceptors before applying strategies. Tracks readiness via `@ready`.
46
+
47
+ - **FeatureStateHolder** ([feature_state_holder.rb](lib/feature_hub/sdk/feature_state_holder.rb)): Wraps feature JSON (`key`, `id`, `type`, `value`, `version`, `l` (locked), `strategies`). Provides typed accessors: `.flag`, `.string`, `.number`, `.raw_json`, `.boolean`. Supports `with_context(ctx)` for strategy evaluation.
48
+
49
+ - **Context classes** ([context.rb](lib/feature_hub/sdk/context.rb)): Fluent builder pattern — `context.user_key("x").platform("ios").country("gb").build()`. Attributes stored as `symbol → [array]`. Two subclasses: `ClientEvalFeatureContext` (local strategy eval) and `ServerEvalFeatureContext` (server-side eval, sends `x-featurehub` header).
50
+
51
+ - **Edge services**: `StreamingEdgeService` ([streaming_edge_service.rb](lib/feature_hub/sdk/streaming_edge_service.rb)) uses SSE via `ld-eventsource`. `PollingEdgeService` ([poll_edge_service.rb](lib/feature_hub/sdk/poll_edge_service.rb)) uses Faraday with `Concurrent::TimerTask`, supports ETags. Call `force_new_edge_service()` after process fork (Puma/Passenger/Unicorn).
52
+
53
+ - **ApplyFeature** ([impl/apply_features.rb](lib/feature_hub/sdk/impl/apply_features.rb)): Client-side strategy evaluator. Iterates rollout strategies, calculates percentage allocation via Murmur3 hash of `percentage_key + feature_id`, matches attribute conditions via `MatcherRegistry`. Returns `Applied(matched:, value:)`.
54
+
55
+ - **EnvironmentInterceptor** ([interceptors.rb](lib/feature_hub/sdk/interceptors.rb)): Override features at runtime via `FEATUREHUB_OVERRIDE_FEATURES=true` + `FEATUREHUB_<FEATURE_NAME>=<value>` env vars.
56
+
57
+ ### Client vs Server Evaluation
58
+
59
+ - **Client-evaluated** API key contains `*` (e.g., `abc*def`): full strategy data sent to SDK, evaluated locally by `ApplyFeature`.
60
+ - **Server-evaluated** API key has no `*`: context attributes sent as `x-featurehub` HTTP header, server evaluates and returns resolved values.
61
+
62
+ ### Strategy Attribute Matchers
63
+
64
+ `MatcherRegistry` dispatches to typed matchers based on `field_type`: `BOOLEAN`, `STRING`/`DATE`/`DATE_TIME` (via `StringMatcher`), `NUMBER`, `SEMANTIC_VERSION` (uses `sem_version` gem), `IP_ADDRESS` (CIDR support). All conditions in a strategy must match for the strategy to apply.
65
+
66
+ ## RBS Type Signatures
67
+
68
+ Type definitions live in [sig/feature_hub/featurehub.rbs](sig/feature_hub/featurehub.rbs). Key signatures to be aware of:
69
+
70
+ - **Feature value type**: `[bool? | String? | Float?]` — the union type used throughout for feature values (`Applied#value`, `FeatureStateHolder#value`, `RolloutStrategy#value`, `InterceptorValue`)
71
+ - **`FeatureStateHolder#initialize`** takes `key:`, `repo:`, `feature_state:`, `parent_state:`, and `ctx:` — the `parent_state` and `ctx` support the `with_context` pattern for context-scoped evaluation
72
+ - **`InternalFeatureRepository`** is the abstract interface that `FeatureHubRepository` implements; `FeatureStateHolder` and context classes depend on this interface, not the concrete class
73
+ - **`ClientContext#build`** returns `ClientContext` (async); **`build_sync`** also returns `ClientContext` (blocking)
74
+ - **`FeatureHubConfig#repository`** takes an optional `InternalFeatureRepository?` and returns one — it acts as both getter and setter
75
+ - **`RolloutStrategyAttribute`**: `values` is `Array[[bool? | String? | Float?]]`; use `float_values` / `str_values` for typed access
76
+ - **`RolloutStrategyCondition`**: predicate methods only (e.g., `equals?`, `regex?`, `includes?`) — no raw string comparison against condition type strings
77
+
78
+ ## Conventions
79
+
80
+ - `# frozen_string_literal: true` on every file
81
+ - Double-quoted strings (RuboCop enforced)
82
+ - Feature keys stored/looked up as symbols internally
83
+ - RuboCop: max line length 120, metrics cops disabled, documentation disabled
84
+ - Tests mirror lib structure: `spec/feature_hub/sdk/**/*_spec.rb`
85
+ - Use `instance_double` for mocking, `aggregate_failures` for multiple assertions
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.0
1
+ 3.3.10
data/CHANGELOG.md CHANGED
@@ -1,4 +1,16 @@
1
- ## [1.3.0] - 2024-01-12
1
+ ## [2.0.0] - 2026-03-22
2
+
3
+ - Refactor FeatureState to FeatureStateHolder to be consistent with other SDKs
4
+ - Add FeatureValueType to reduce duplication
5
+ - Add local YAML file interceptor with an optional timer to watch for changes
6
+ - Add `RawUpdateFeatureListener` base class so custom listeners can react to raw edge updates (including update source tracking)
7
+ - Add `LocalYamlStore`: load features from a local YAML file without an Edge server, as an alternative to the value interceptor
8
+ - Add `RedisSessionStore`: persist features in Redis so they survive process restarts and are shared across processes (client-evaluated only)
9
+ - Support punch-through context evaluation for external stores so per-request context attributes are applied even when features come from Redis or YAML
10
+ - Add `value` accessor to `FeatureStateHolder` as a convenience shortcut alongside the typed accessors
11
+ - Edge services now explicitly close the repository on shutdown
12
+
13
+ ## [1.3.0] - 2026-01-11
2
14
 
3
15
  - update gem dependencies
4
16
  - bump minimum ruby version to 3.2
data/Gemfile CHANGED
@@ -21,4 +21,4 @@ gem "murmurhash3", "~> 0.1.7"
21
21
 
22
22
  gem "sem_version", "~> 2.0.0"
23
23
 
24
- gem "ld-eventsource", "~> 2.3.0"
24
+ gem "ld-eventsource", "~> 2.5.1"
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- featurehub-sdk (1.3.0)
4
+ featurehub-sdk (2.0.0)
5
5
  concurrent-ruby (~> 1.3)
6
6
  faraday (~> 2)
7
- ld-eventsource (~> 2.3.0)
7
+ ld-eventsource (~> 2.5.1)
8
8
  murmurhash3 (~> 0.1.7)
9
9
  sem_version (~> 2.0.0)
10
10
 
@@ -15,6 +15,7 @@ GEM
15
15
  public_suffix (>= 2.0.2, < 8.0)
16
16
  ast (2.4.3)
17
17
  concurrent-ruby (1.3.6)
18
+ connection_pool (3.0.2)
18
19
  diff-lcs (1.6.2)
19
20
  docile (1.4.1)
20
21
  domain_name (0.6.20240107)
@@ -25,6 +26,7 @@ GEM
25
26
  faraday-net_http (3.4.2)
26
27
  net-http (~> 0.5)
27
28
  ffi (1.17.3-arm64-darwin)
29
+ ffi (1.17.3-x86_64-darwin)
28
30
  ffi (1.17.3-x86_64-linux-gnu)
29
31
  ffi-compiler (1.3.2)
30
32
  ffi (>= 1.15.5)
@@ -37,9 +39,9 @@ GEM
37
39
  http-cookie (1.1.0)
38
40
  domain_name (~> 0.5)
39
41
  http-form_data (2.3.0)
40
- json (2.18.0)
42
+ json (2.19.2)
41
43
  language_server-protocol (3.17.0.5)
42
- ld-eventsource (2.3.0)
44
+ ld-eventsource (2.5.1)
43
45
  concurrent-ruby (~> 1.0)
44
46
  http (>= 4.4.1, < 6.0.0)
45
47
  lint_roller (1.1.0)
@@ -59,6 +61,10 @@ GEM
59
61
  racc (1.8.1)
60
62
  rainbow (3.1.1)
61
63
  rake (13.3.1)
64
+ redis (5.4.1)
65
+ redis-client (>= 0.22.0)
66
+ redis-client (0.28.0)
67
+ connection_pool
62
68
  regexp_parser (2.11.3)
63
69
  rspec (3.13.2)
64
70
  rspec-core (~> 3.13.0)
@@ -102,15 +108,17 @@ GEM
102
108
 
103
109
  PLATFORMS
104
110
  arm64-darwin
111
+ x86_64-darwin-24
105
112
  x86_64-linux
106
113
 
107
114
  DEPENDENCIES
108
115
  concurrent-ruby (~> 1.3)
109
116
  faraday (~> 2)
110
117
  featurehub-sdk!
111
- ld-eventsource (~> 2.3.0)
118
+ ld-eventsource (~> 2.5.1)
112
119
  murmurhash3 (~> 0.1.7)
113
120
  rake (~> 13.0)
121
+ redis (~> 5)
114
122
  rspec (~> 3.0)
115
123
  rubocop (~> 1.21)
116
124
  sem_version (~> 2.0.0)
@@ -120,21 +128,23 @@ CHECKSUMS
120
128
  addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057
121
129
  ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
122
130
  concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
131
+ connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
123
132
  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
124
133
  docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
125
134
  domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
126
135
  faraday (2.14.0) sha256=8699cfe5d97e55268f2596f9a9d5a43736808a943714e3d9a53e6110593941cd
127
136
  faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c
128
- featurehub-sdk (1.3.0)
137
+ featurehub-sdk (2.0.0)
129
138
  ffi (1.17.3-arm64-darwin) sha256=0c690555d4cee17a7f07c04d59df39b2fba74ec440b19da1f685c6579bb0717f
139
+ ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5
130
140
  ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
131
141
  ffi-compiler (1.3.2) sha256=a94f3d81d12caf5c5d4ecf13980a70d0aeaa72268f3b9cc13358bcc6509184a0
132
142
  http (5.3.1) sha256=c50802d8e9be3926cb84ac3b36d1a31fbbac383bc4cbecdce9053cb604231d7d
133
143
  http-cookie (1.1.0) sha256=38a5e60d1527eebc396831b8c4b9455440509881219273a6c99943d29eadbb19
134
144
  http-form_data (2.3.0) sha256=cc4eeb1361d9876821e31d7b1cf0b68f1cf874b201d27903480479d86448a5f3
135
- json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
145
+ json (2.19.2) sha256=e7e1bd318b2c37c4ceee2444841c86539bc462e81f40d134cf97826cb14e83cf
136
146
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
137
- ld-eventsource (2.3.0) sha256=b79187490fc567626c805b9f3d97d08a03d5e4cad045974b2089216bf37dba9f
147
+ ld-eventsource (2.5.1) sha256=19cb44b7d3f91f76b7401a1b74293b32c723389b2d6f9d81ecb803c628ac1b25
138
148
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
139
149
  llhttp-ffi (0.5.1) sha256=9a25a7fc19311f691a78c9c0ac0fbf4675adbd0cca74310228fdf841018fa7bc
140
150
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
@@ -147,6 +157,8 @@ CHECKSUMS
147
157
  racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
148
158
  rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
149
159
  rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
160
+ redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
161
+ redis-client (0.28.0) sha256=888892f9cd8787a41c0ece00bdf5f556dfff7770326ce40bb2bc11f1bfec824b
150
162
  regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
151
163
  rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
152
164
  rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d