flipper 0.16.0 → 1.4.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 (285) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +1 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/dependabot.yml +6 -0
  5. data/.github/workflows/ci.yml +110 -0
  6. data/.github/workflows/examples.yml +105 -0
  7. data/.github/workflows/release.yml +54 -0
  8. data/.rspec +1 -0
  9. data/CLAUDE.md +87 -0
  10. data/Changelog.md +2 -215
  11. data/Dockerfile +1 -1
  12. data/Gemfile +28 -20
  13. data/README.md +72 -62
  14. data/Rakefile +13 -3
  15. data/benchmark/enabled_ips.rb +10 -0
  16. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  17. data/benchmark/enabled_profile.rb +20 -0
  18. data/benchmark/instrumentation_ips.rb +21 -0
  19. data/benchmark/typecast_ips.rb +27 -0
  20. data/docker-compose.yml +37 -34
  21. data/docs/DockerCompose.md +0 -1
  22. data/docs/README.md +1 -0
  23. data/docs/images/banner.jpg +0 -0
  24. data/docs/images/flipper_cloud.png +0 -0
  25. data/examples/api/basic.ru +18 -0
  26. data/examples/api/custom_memoized.ru +36 -0
  27. data/examples/api/memoized.ru +42 -0
  28. data/examples/basic.rb +1 -12
  29. data/examples/cloud/app.ru +12 -0
  30. data/examples/cloud/backoff_policy.rb +13 -0
  31. data/examples/cloud/basic.rb +22 -0
  32. data/examples/cloud/cloud_setup.rb +20 -0
  33. data/examples/cloud/forked.rb +36 -0
  34. data/examples/cloud/import.rb +17 -0
  35. data/examples/cloud/poll_interval/README.md +111 -0
  36. data/examples/cloud/poll_interval/client.rb +108 -0
  37. data/examples/cloud/poll_interval/server.rb +98 -0
  38. data/examples/cloud/threaded.rb +33 -0
  39. data/examples/configuring_default.rb +2 -5
  40. data/examples/dsl.rb +10 -35
  41. data/examples/enabled_for_actor.rb +10 -15
  42. data/examples/expressions.rb +237 -0
  43. data/examples/group.rb +3 -6
  44. data/examples/group_dynamic_lookup.rb +5 -19
  45. data/examples/group_with_members.rb +4 -14
  46. data/examples/importing.rb +1 -1
  47. data/examples/individual_actor.rb +2 -5
  48. data/examples/instrumentation.rb +2 -2
  49. data/examples/instrumentation_last_accessed_at.rb +38 -0
  50. data/examples/memoizing.rb +35 -0
  51. data/examples/mirroring.rb +59 -0
  52. data/examples/percentage_of_actors.rb +6 -16
  53. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  54. data/examples/percentage_of_actors_group.rb +5 -18
  55. data/examples/percentage_of_time.rb +3 -6
  56. data/examples/strict.rb +18 -0
  57. data/exe/flipper +5 -0
  58. data/flipper-cloud.gemspec +19 -0
  59. data/flipper.gemspec +10 -7
  60. data/lib/flipper/actor.rb +10 -3
  61. data/lib/flipper/adapter.rb +50 -8
  62. data/lib/flipper/adapter_builder.rb +44 -0
  63. data/lib/flipper/adapters/actor_limit.rb +54 -0
  64. data/lib/flipper/adapters/cache_base.rb +161 -0
  65. data/lib/flipper/adapters/dual_write.rb +63 -0
  66. data/lib/flipper/adapters/failover.rb +85 -0
  67. data/lib/flipper/adapters/failsafe.rb +72 -0
  68. data/lib/flipper/adapters/http/client.rb +64 -7
  69. data/lib/flipper/adapters/http/error.rb +19 -1
  70. data/lib/flipper/adapters/http.rb +97 -43
  71. data/lib/flipper/adapters/instrumented.rb +47 -26
  72. data/lib/flipper/adapters/memoizable.rb +44 -40
  73. data/lib/flipper/adapters/memory.rb +75 -111
  74. data/lib/flipper/adapters/operation_logger.rb +22 -78
  75. data/lib/flipper/adapters/poll/poller.rb +2 -0
  76. data/lib/flipper/adapters/poll.rb +52 -0
  77. data/lib/flipper/adapters/pstore.rb +27 -17
  78. data/lib/flipper/adapters/read_only.rb +8 -41
  79. data/lib/flipper/adapters/strict.rb +45 -0
  80. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  81. data/lib/flipper/adapters/sync/interval_synchronizer.rb +2 -7
  82. data/lib/flipper/adapters/sync/synchronizer.rb +13 -6
  83. data/lib/flipper/adapters/sync.rb +23 -29
  84. data/lib/flipper/adapters/wrapper.rb +54 -0
  85. data/lib/flipper/cli.rb +314 -0
  86. data/lib/flipper/cloud/configuration.rb +271 -0
  87. data/lib/flipper/cloud/dsl.rb +27 -0
  88. data/lib/flipper/cloud/message_verifier.rb +95 -0
  89. data/lib/flipper/cloud/middleware.rb +63 -0
  90. data/lib/flipper/cloud/migrate.rb +71 -0
  91. data/lib/flipper/cloud/routes.rb +14 -0
  92. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  93. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  94. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  95. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  96. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  97. data/lib/flipper/cloud/telemetry.rb +191 -0
  98. data/lib/flipper/cloud.rb +54 -0
  99. data/lib/flipper/configuration.rb +54 -7
  100. data/lib/flipper/dsl.rb +58 -47
  101. data/lib/flipper/engine.rb +102 -0
  102. data/lib/flipper/errors.rb +3 -21
  103. data/lib/flipper/export.rb +24 -0
  104. data/lib/flipper/exporter.rb +17 -0
  105. data/lib/flipper/exporters/json/export.rb +32 -0
  106. data/lib/flipper/exporters/json/v1.rb +33 -0
  107. data/lib/flipper/expression/builder.rb +73 -0
  108. data/lib/flipper/expression/constant.rb +25 -0
  109. data/lib/flipper/expression.rb +71 -0
  110. data/lib/flipper/expressions/all.rb +9 -0
  111. data/lib/flipper/expressions/any.rb +9 -0
  112. data/lib/flipper/expressions/boolean.rb +9 -0
  113. data/lib/flipper/expressions/comparable.rb +13 -0
  114. data/lib/flipper/expressions/equal.rb +9 -0
  115. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  116. data/lib/flipper/expressions/greater_than.rb +9 -0
  117. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  118. data/lib/flipper/expressions/less_than.rb +9 -0
  119. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  120. data/lib/flipper/expressions/not_equal.rb +9 -0
  121. data/lib/flipper/expressions/now.rb +9 -0
  122. data/lib/flipper/expressions/number.rb +9 -0
  123. data/lib/flipper/expressions/percentage.rb +9 -0
  124. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  125. data/lib/flipper/expressions/property.rb +9 -0
  126. data/lib/flipper/expressions/random.rb +9 -0
  127. data/lib/flipper/expressions/string.rb +9 -0
  128. data/lib/flipper/expressions/time.rb +16 -0
  129. data/lib/flipper/feature.rb +95 -28
  130. data/lib/flipper/feature_check_context.rb +10 -6
  131. data/lib/flipper/gate.rb +13 -11
  132. data/lib/flipper/gate_values.rb +5 -18
  133. data/lib/flipper/gates/actor.rb +10 -17
  134. data/lib/flipper/gates/boolean.rb +1 -1
  135. data/lib/flipper/gates/expression.rb +75 -0
  136. data/lib/flipper/gates/group.rb +5 -7
  137. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  138. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  139. data/lib/flipper/identifier.rb +17 -0
  140. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  141. data/lib/flipper/instrumentation/statsd.rb +4 -2
  142. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  143. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  144. data/lib/flipper/instrumenters/memory.rb +6 -2
  145. data/lib/flipper/metadata.rb +8 -1
  146. data/lib/flipper/middleware/memoizer.rb +46 -27
  147. data/lib/flipper/middleware/setup_env.rb +13 -3
  148. data/lib/flipper/model/active_record.rb +23 -0
  149. data/lib/flipper/poller.rb +157 -0
  150. data/lib/flipper/serializers/gzip.rb +22 -0
  151. data/lib/flipper/serializers/json.rb +17 -0
  152. data/lib/flipper/spec/shared_adapter_specs.rb +122 -56
  153. data/lib/flipper/test/shared_adapter_test.rb +120 -52
  154. data/lib/flipper/test_help.rb +43 -0
  155. data/lib/flipper/typecast.rb +59 -18
  156. data/lib/flipper/types/actor.rb +19 -13
  157. data/lib/flipper/types/group.rb +12 -5
  158. data/lib/flipper/types/percentage.rb +1 -1
  159. data/lib/flipper/version.rb +11 -1
  160. data/lib/flipper.rb +71 -12
  161. data/lib/generators/flipper/setup_generator.rb +68 -0
  162. data/lib/generators/flipper/templates/initializer.rb +45 -0
  163. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  164. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  165. data/lib/generators/flipper/update_generator.rb +35 -0
  166. data/package-lock.json +41 -0
  167. data/package.json +10 -0
  168. data/spec/fixtures/environment.rb +1 -0
  169. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  170. data/spec/flipper/actor_spec.rb +10 -2
  171. data/spec/flipper/adapter_builder_spec.rb +72 -0
  172. data/spec/flipper/adapter_spec.rb +52 -6
  173. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  174. data/spec/flipper/adapters/dual_write_spec.rb +82 -0
  175. data/spec/flipper/adapters/failover_spec.rb +141 -0
  176. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  177. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  178. data/spec/flipper/adapters/http_spec.rb +402 -65
  179. data/spec/flipper/adapters/instrumented_spec.rb +31 -13
  180. data/spec/flipper/adapters/memoizable_spec.rb +51 -33
  181. data/spec/flipper/adapters/memory_spec.rb +33 -5
  182. data/spec/flipper/adapters/operation_logger_spec.rb +38 -12
  183. data/spec/flipper/adapters/poll_spec.rb +41 -0
  184. data/spec/flipper/adapters/pstore_spec.rb +0 -2
  185. data/spec/flipper/adapters/read_only_spec.rb +32 -18
  186. data/spec/flipper/adapters/strict_spec.rb +64 -0
  187. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -1
  188. data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
  189. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -1
  190. data/spec/flipper/adapters/sync_spec.rb +17 -6
  191. data/spec/flipper/cli_spec.rb +217 -0
  192. data/spec/flipper/cloud/configuration_spec.rb +257 -0
  193. data/spec/flipper/cloud/dsl_spec.rb +90 -0
  194. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  195. data/spec/flipper/cloud/middleware_spec.rb +307 -0
  196. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  197. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  198. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  199. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  200. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  201. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  202. data/spec/flipper/cloud_spec.rb +186 -0
  203. data/spec/flipper/configuration_spec.rb +37 -3
  204. data/spec/flipper/dsl_spec.rb +67 -80
  205. data/spec/flipper/engine_spec.rb +374 -0
  206. data/spec/flipper/export_spec.rb +13 -0
  207. data/spec/flipper/exporter_spec.rb +16 -0
  208. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  209. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  210. data/spec/flipper/expression/builder_spec.rb +248 -0
  211. data/spec/flipper/expression_spec.rb +188 -0
  212. data/spec/flipper/expressions/all_spec.rb +15 -0
  213. data/spec/flipper/expressions/any_spec.rb +15 -0
  214. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  215. data/spec/flipper/expressions/equal_spec.rb +24 -0
  216. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  217. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  218. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  219. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  220. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  221. data/spec/flipper/expressions/now_spec.rb +11 -0
  222. data/spec/flipper/expressions/number_spec.rb +21 -0
  223. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  224. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  225. data/spec/flipper/expressions/property_spec.rb +13 -0
  226. data/spec/flipper/expressions/random_spec.rb +9 -0
  227. data/spec/flipper/expressions/string_spec.rb +11 -0
  228. data/spec/flipper/expressions/time_spec.rb +29 -0
  229. data/spec/flipper/feature_check_context_spec.rb +18 -20
  230. data/spec/flipper/feature_spec.rb +461 -48
  231. data/spec/flipper/gate_spec.rb +0 -2
  232. data/spec/flipper/gate_values_spec.rb +2 -34
  233. data/spec/flipper/gates/actor_spec.rb +0 -2
  234. data/spec/flipper/gates/boolean_spec.rb +1 -3
  235. data/spec/flipper/gates/expression_spec.rb +190 -0
  236. data/spec/flipper/gates/group_spec.rb +2 -5
  237. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
  238. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
  239. data/spec/flipper/identifier_spec.rb +12 -0
  240. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -7
  241. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -3
  242. data/spec/flipper/instrumenters/memory_spec.rb +18 -1
  243. data/spec/flipper/instrumenters/noop_spec.rb +14 -8
  244. data/spec/flipper/middleware/memoizer_spec.rb +199 -62
  245. data/spec/flipper/middleware/setup_env_spec.rb +23 -5
  246. data/spec/flipper/model/active_record_spec.rb +72 -0
  247. data/spec/flipper/poller_spec.rb +390 -0
  248. data/spec/flipper/registry_spec.rb +0 -1
  249. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  250. data/spec/flipper/serializers/json_spec.rb +13 -0
  251. data/spec/flipper/typecast_spec.rb +121 -7
  252. data/spec/flipper/types/actor_spec.rb +63 -47
  253. data/spec/flipper/types/boolean_spec.rb +0 -1
  254. data/spec/flipper/types/group_spec.rb +24 -3
  255. data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
  256. data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
  257. data/spec/flipper/types/percentage_spec.rb +0 -1
  258. data/spec/{integration_spec.rb → flipper_integration_spec.rb} +301 -59
  259. data/spec/flipper_spec.rb +123 -29
  260. data/spec/{helper.rb → spec_helper.rb} +23 -21
  261. data/spec/support/actor_names.yml +1 -0
  262. data/spec/support/descriptions.yml +1 -0
  263. data/spec/support/fail_on_output.rb +8 -0
  264. data/spec/support/fake_backoff_policy.rb +15 -0
  265. data/spec/support/skippable.rb +18 -0
  266. data/spec/support/spec_helpers.rb +53 -6
  267. data/test/adapters/actor_limit_test.rb +20 -0
  268. data/test/test_helper.rb +2 -1
  269. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  270. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  271. data/test_rails/helper.rb +31 -0
  272. data/test_rails/system/test_help_test.rb +52 -0
  273. metadata +200 -82
  274. data/.rubocop.yml +0 -54
  275. data/.rubocop_todo.yml +0 -199
  276. data/docs/Adapters.md +0 -124
  277. data/docs/Caveats.md +0 -4
  278. data/docs/Gates.md +0 -167
  279. data/docs/Instrumentation.md +0 -27
  280. data/docs/Optimization.md +0 -114
  281. data/docs/api/README.md +0 -849
  282. data/docs/http/README.md +0 -35
  283. data/docs/read-only/README.md +0 -21
  284. data/examples/example_setup.rb +0 -8
  285. data/test/helper.rb +0 -11
@@ -7,25 +7,57 @@ module Flipper
7
7
  class Http
8
8
  class Client
9
9
  DEFAULT_HEADERS = {
10
- 'Content-Type' => 'application/json',
11
- 'Accept' => 'application/json',
12
- 'User-Agent' => "Flipper HTTP Adapter v#{VERSION}",
10
+ 'content-type' => 'application/json',
11
+ 'accept' => 'application/json',
12
+ 'user-agent' => "Flipper HTTP Adapter v#{VERSION}",
13
13
  }.freeze
14
14
 
15
15
  HTTPS_SCHEME = "https".freeze
16
16
 
17
+ CLIENT_FRAMEWORKS = {
18
+ rails: -> { Rails.version if defined?(Rails) },
19
+ sinatra: -> { Sinatra::VERSION if defined?(Sinatra) },
20
+ hanami: -> { Hanami::VERSION if defined?(Hanami) },
21
+ sidekiq: -> { Sidekiq::VERSION if defined?(Sidekiq) },
22
+ good_job: -> { GoodJob::VERSION if defined?(GoodJob) },
23
+ }
24
+
25
+ attr_reader :uri, :headers
26
+ attr_reader :basic_auth_username, :basic_auth_password
27
+ attr_reader :read_timeout, :open_timeout, :write_timeout
28
+ attr_reader :max_retries, :debug_output
29
+
17
30
  def initialize(options = {})
18
31
  @uri = URI(options.fetch(:url))
19
- @headers = DEFAULT_HEADERS.merge(options[:headers] || {})
20
32
  @basic_auth_username = options[:basic_auth_username]
21
33
  @basic_auth_password = options[:basic_auth_password]
22
34
  @read_timeout = options[:read_timeout]
23
35
  @open_timeout = options[:open_timeout]
36
+ @write_timeout = options[:write_timeout]
37
+ @max_retries = options.key?(:max_retries) ? options[:max_retries] : 0
24
38
  @debug_output = options[:debug_output]
39
+
40
+ @headers = {}
41
+ DEFAULT_HEADERS.each { |key, value| add_header key, value }
42
+ if options[:headers]
43
+ options[:headers].each { |key, value| add_header key, value }
44
+ end
25
45
  end
26
46
 
27
- def get(path)
28
- perform Net::HTTP::Get, path, @headers
47
+ def add_header(key, value)
48
+ @headers[normalize_header_key(key)] = value
49
+ end
50
+
51
+ def get(path, options = {})
52
+ headers = @headers.dup
53
+
54
+ if options[:headers]
55
+ options[:headers].each do |key, value|
56
+ headers[normalize_header_key(key)] = value
57
+ end
58
+ end
59
+
60
+ perform Net::HTTP::Get, path, headers
29
61
  end
30
62
 
31
63
  def post(path, body = nil)
@@ -57,6 +89,8 @@ module Flipper
57
89
  http = Net::HTTP.new(uri.host, uri.port)
58
90
  http.read_timeout = @read_timeout if @read_timeout
59
91
  http.open_timeout = @open_timeout if @open_timeout
92
+ http.max_retries = @max_retries if @max_retries
93
+ http.write_timeout = @write_timeout if @write_timeout
60
94
  http.set_debug_output(@debug_output) if @debug_output
61
95
 
62
96
  if uri.scheme == HTTPS_SCHEME
@@ -68,9 +102,24 @@ module Flipper
68
102
  end
69
103
 
70
104
  def build_request(http_method, uri, headers, options)
105
+ request_headers = {
106
+ 'client-language' => "ruby",
107
+ 'client-language-version' => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
108
+ 'client-platform' => RUBY_PLATFORM,
109
+ 'client-engine' => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
110
+ 'client-pid' => Process.pid.to_s,
111
+ 'client-thread' => Thread.current.object_id.to_s,
112
+ 'client-hostname' => Socket.gethostname,
113
+ }.merge(headers)
114
+
71
115
  body = options[:body]
72
116
  request = http_method.new(uri.request_uri)
73
- request.initialize_http_header(headers) if headers
117
+ request.initialize_http_header(request_headers)
118
+
119
+ client_frameworks.each do |framework, version|
120
+ request.add_field("client-framework", [framework, version].join("="))
121
+ end
122
+
74
123
  request.body = body if body
75
124
 
76
125
  if @basic_auth_username && @basic_auth_password
@@ -79,6 +128,14 @@ module Flipper
79
128
 
80
129
  request
81
130
  end
131
+
132
+ def client_frameworks
133
+ CLIENT_FRAMEWORKS.transform_values { |detect| detect.call rescue nil }.compact
134
+ end
135
+
136
+ def normalize_header_key(key)
137
+ key.to_s.downcase.gsub('_'.freeze, '-'.freeze)
138
+ end
82
139
  end
83
140
  end
84
141
  end
@@ -1,3 +1,5 @@
1
+ require "json"
2
+
1
3
  module Flipper
2
4
  module Adapters
3
5
  class Http
@@ -6,7 +8,23 @@ module Flipper
6
8
 
7
9
  def initialize(response)
8
10
  @response = response
9
- super("Failed with status: #{response.code}")
11
+ message = "Failed with status: #{response.code}"
12
+
13
+ begin
14
+ data = Typecast.from_json(response.body)
15
+
16
+ if error_message = data["message"]
17
+ message << "\n\n#{data["message"]}"
18
+ end
19
+
20
+ if more_info = data["more_info"]
21
+ message << "\n#{data["more_info"]}"
22
+ end
23
+ rescue
24
+ # welp we tried
25
+ end
26
+
27
+ super(message)
10
28
  end
11
29
  end
12
30
  end
@@ -10,7 +10,7 @@ module Flipper
10
10
  class Http
11
11
  include Flipper::Adapter
12
12
 
13
- attr_reader :name
13
+ attr_reader :client
14
14
 
15
15
  def initialize(options = {})
16
16
  @client = Client.new(url: options.fetch(:url),
@@ -19,14 +19,19 @@ module Flipper
19
19
  basic_auth_password: options[:basic_auth_password],
20
20
  read_timeout: options[:read_timeout],
21
21
  open_timeout: options[:open_timeout],
22
+ write_timeout: options[:write_timeout],
23
+ max_retries: options[:max_retries],
22
24
  debug_output: options[:debug_output])
23
- @name = :http
25
+ @last_get_all_etag = nil
26
+ @last_get_all_result = nil
27
+ @last_get_all_response = nil
28
+ @get_all_mutex = Mutex.new
24
29
  end
25
30
 
26
31
  def get(feature)
27
32
  response = @client.get("/features/#{feature.key}")
28
33
  if response.is_a?(Net::HTTPOK)
29
- parsed_response = JSON.parse(response.body)
34
+ parsed_response = Typecast.from_json(response.body)
30
35
  result_for_feature(feature, parsed_response.fetch('gates'))
31
36
  elsif response.is_a?(Net::HTTPNotFound)
32
37
  default_config
@@ -35,18 +40,12 @@ module Flipper
35
40
  end
36
41
  end
37
42
 
38
- def add(feature)
39
- body = JSON.generate(name: feature.key)
40
- response = @client.post('/features', body)
41
- response.is_a?(Net::HTTPOK)
42
- end
43
-
44
43
  def get_multi(features)
45
44
  csv_keys = features.map(&:key).join(',')
46
- response = @client.get("/features?keys=#{csv_keys}")
45
+ response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
47
46
  raise Error, response unless response.is_a?(Net::HTTPOK)
48
47
 
49
- parsed_response = JSON.parse(response.body)
48
+ parsed_response = Typecast.from_json(response.body)
50
49
  parsed_features = parsed_response.fetch('features')
51
50
  gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
52
51
  hash[parsed_feature['key']] = parsed_feature['gates']
@@ -60,78 +59,129 @@ module Flipper
60
59
  result
61
60
  end
62
61
 
63
- def get_all
64
- response = @client.get("/features")
62
+ def get_all(cache_bust: false)
63
+ options = {}
64
+ path = "/features?exclude_gate_names=true"
65
+ path += "&_cb=#{Time.now.to_i}" if cache_bust
66
+ etag = @get_all_mutex.synchronize { @last_get_all_etag }
67
+
68
+ if etag
69
+ options[:headers] = { if_none_match: etag }
70
+ end
71
+
72
+ response = @client.get(path, options)
73
+ @get_all_mutex.synchronize { @last_get_all_response = response }
74
+
75
+ if response.is_a?(Net::HTTPNotModified)
76
+ cached_result = @get_all_mutex.synchronize { @last_get_all_result }
77
+
78
+ if cached_result
79
+ return cached_result
80
+ else
81
+ raise Error, response
82
+ end
83
+ end
84
+
65
85
  raise Error, response unless response.is_a?(Net::HTTPOK)
66
86
 
67
- parsed_response = JSON.parse(response.body)
68
- parsed_features = parsed_response.fetch('features')
87
+ parsed_response = response.body.empty? ? {} : Typecast.from_json(response.body)
88
+ parsed_features = parsed_response['features'] || []
69
89
  gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
70
90
  hash[parsed_feature['key']] = parsed_feature['gates']
71
91
  hash
72
92
  end
73
93
 
74
94
  result = {}
75
- gates_by_key.keys.each do |key|
95
+ gates_by_key.each_key do |key|
76
96
  feature = Feature.new(key, self)
77
97
  result[feature.key] = result_for_feature(feature, gates_by_key[feature.key])
78
98
  end
99
+
100
+ @get_all_mutex.synchronize do
101
+ @last_get_all_etag = response['etag'] if response['etag']
102
+ @last_get_all_result = result
103
+ end
104
+
79
105
  result
80
106
  end
81
107
 
108
+ def last_get_all_response
109
+ @get_all_mutex.synchronize { @last_get_all_response }
110
+ end
111
+
82
112
  def features
83
- response = @client.get('/features')
113
+ response = @client.get('/features?exclude_gate_names=true')
84
114
  raise Error, response unless response.is_a?(Net::HTTPOK)
85
115
 
86
- parsed_response = JSON.parse(response.body)
116
+ parsed_response = Typecast.from_json(response.body)
87
117
  parsed_response['features'].map { |feature| feature['key'] }.to_set
88
118
  end
89
119
 
120
+ def add(feature)
121
+ body = JSON.generate(name: feature.key)
122
+ response = @client.post('/features', body)
123
+ raise Error, response unless response.is_a?(Net::HTTPOK)
124
+ true
125
+ end
126
+
90
127
  def remove(feature)
91
128
  response = @client.delete("/features/#{feature.key}")
92
- response.is_a?(Net::HTTPNoContent)
129
+ raise Error, response unless response.is_a?(Net::HTTPNoContent)
130
+ true
93
131
  end
94
132
 
95
133
  def enable(feature, gate, thing)
96
- body = request_body_for_gate(gate, thing.value.to_s)
134
+ body = request_body_for_gate(gate, thing.value)
97
135
  query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
98
136
  response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
99
- response.is_a?(Net::HTTPOK)
137
+ raise Error, response unless response.is_a?(Net::HTTPOK)
138
+ true
100
139
  end
101
140
 
102
141
  def disable(feature, gate, thing)
103
- body = request_body_for_gate(gate, thing.value.to_s)
142
+ body = request_body_for_gate(gate, thing.value)
104
143
  query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
105
- response =
106
- case gate.key
107
- when :percentage_of_actors, :percentage_of_time
108
- @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
109
- else
110
- @client.delete("/features/#{feature.key}/#{gate.key}#{query_string}", body)
111
- end
112
- response.is_a?(Net::HTTPOK)
144
+ response = case gate.key
145
+ when :percentage_of_actors, :percentage_of_time
146
+ @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
147
+ else
148
+ @client.delete("/features/#{feature.key}/#{gate.key}#{query_string}", body)
149
+ end
150
+ raise Error, response unless response.is_a?(Net::HTTPOK)
151
+ true
113
152
  end
114
153
 
115
154
  def clear(feature)
116
155
  response = @client.delete("/features/#{feature.key}/clear")
117
- response.is_a?(Net::HTTPNoContent)
156
+ raise Error, response unless response.is_a?(Net::HTTPNoContent)
157
+ true
158
+ end
159
+
160
+ def import(source)
161
+ adapter = self.class.from(source)
162
+ export = adapter.export(format: :json, version: 1)
163
+ response = @client.post("/import", export.contents)
164
+ raise Error, response unless response.is_a?(Net::HTTPNoContent)
165
+ true
118
166
  end
119
167
 
120
168
  private
121
169
 
122
170
  def request_body_for_gate(gate, value)
123
171
  data = case gate.key
124
- when :boolean
125
- {}
126
- when :groups
127
- { name: value }
128
- when :actors
129
- { flipper_id: value }
130
- when :percentage_of_actors, :percentage_of_time
131
- { percentage: value }
132
- else
133
- raise "#{gate.key} is not a valid flipper gate key"
134
- end
172
+ when :boolean
173
+ {}
174
+ when :groups
175
+ { name: value.to_s }
176
+ when :actors
177
+ { flipper_id: value.to_s }
178
+ when :percentage_of_actors, :percentage_of_time
179
+ { percentage: value.to_s }
180
+ when :expression
181
+ value
182
+ else
183
+ raise "#{gate.key} is not a valid flipper gate key"
184
+ end
135
185
  JSON.generate(data)
136
186
  end
137
187
 
@@ -152,13 +202,17 @@ module Flipper
152
202
  case gate.data_type
153
203
  when :boolean, :integer
154
204
  value ? value.to_s : value
205
+ when :json
206
+ value
155
207
  when :set
156
208
  value ? value.to_set : Set.new
157
209
  else
158
- unsupported_data_type(gate.data_type)
210
+ unsupported_data_type gate.data_type
159
211
  end
160
212
  end
161
213
 
214
+ private
215
+
162
216
  def unsupported_data_type(data_type)
163
217
  raise "#{data_type} is not supported by this adapter"
164
218
  end
@@ -4,7 +4,7 @@ module Flipper
4
4
  module Adapters
5
5
  # Internal: Adapter that wraps another adapter and instruments all adapter
6
6
  # operations.
7
- class Instrumented < SimpleDelegator
7
+ class Instrumented
8
8
  include ::Flipper::Adapter
9
9
 
10
10
  # Private: The name of instrumentation events.
@@ -13,9 +13,6 @@ module Flipper
13
13
  # Private: What is used to instrument all the things.
14
14
  attr_reader :instrumenter
15
15
 
16
- # Public: The name of the adapter.
17
- attr_reader :name
18
-
19
16
  # Internal: Initializes a new adapter instance.
20
17
  #
21
18
  # adapter - Vanilla adapter instance to wrap.
@@ -24,126 +21,150 @@ module Flipper
24
21
  # :instrumenter - What to use to instrument all the things.
25
22
  #
26
23
  def initialize(adapter, options = {})
27
- super(adapter)
28
24
  @adapter = adapter
29
- @name = :instrumented
30
25
  @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
31
26
  end
32
27
 
33
28
  # Public
34
29
  def features
35
- payload = {
30
+ default_payload = {
36
31
  operation: :features,
37
32
  adapter_name: @adapter.name,
38
33
  }
39
34
 
40
- @instrumenter.instrument(InstrumentationName, payload) do |payload|
35
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
41
36
  payload[:result] = @adapter.features
42
37
  end
43
38
  end
44
39
 
45
40
  # Public
46
41
  def add(feature)
47
- payload = {
42
+ default_payload = {
48
43
  operation: :add,
49
44
  adapter_name: @adapter.name,
50
45
  feature_name: feature.name,
51
46
  }
52
47
 
53
- @instrumenter.instrument(InstrumentationName, payload) do |payload|
48
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
54
49
  payload[:result] = @adapter.add(feature)
55
50
  end
56
51
  end
57
52
 
58
53
  # Public
59
54
  def remove(feature)
60
- payload = {
55
+ default_payload = {
61
56
  operation: :remove,
62
57
  adapter_name: @adapter.name,
63
58
  feature_name: feature.name,
64
59
  }
65
60
 
66
- @instrumenter.instrument(InstrumentationName, payload) do |payload|
61
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
67
62
  payload[:result] = @adapter.remove(feature)
68
63
  end
69
64
  end
70
65
 
71
66
  # Public
72
67
  def clear(feature)
73
- payload = {
68
+ default_payload = {
74
69
  operation: :clear,
75
70
  adapter_name: @adapter.name,
76
71
  feature_name: feature.name,
77
72
  }
78
73
 
79
- @instrumenter.instrument(InstrumentationName, payload) do |payload|
74
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
80
75
  payload[:result] = @adapter.clear(feature)
81
76
  end
82
77
  end
83
78
 
84
79
  # Public
85
80
  def get(feature)
86
- payload = {
81
+ default_payload = {
87
82
  operation: :get,
88
83
  adapter_name: @adapter.name,
89
84
  feature_name: feature.name,
90
85
  }
91
86
 
92
- @instrumenter.instrument(InstrumentationName, payload) do |payload|
87
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
93
88
  payload[:result] = @adapter.get(feature)
94
89
  end
95
90
  end
96
91
 
97
92
  def get_multi(features)
98
- payload = {
93
+ default_payload = {
99
94
  operation: :get_multi,
100
95
  adapter_name: @adapter.name,
101
96
  feature_names: features.map(&:name),
102
97
  }
103
98
 
104
- @instrumenter.instrument(InstrumentationName, payload) do |payload|
99
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
105
100
  payload[:result] = @adapter.get_multi(features)
106
101
  end
107
102
  end
108
103
 
109
- def get_all
110
- payload = {
104
+ def get_all(**kwargs)
105
+ default_payload = {
111
106
  operation: :get_all,
112
107
  adapter_name: @adapter.name,
113
108
  }
114
109
 
115
- @instrumenter.instrument(InstrumentationName, payload) do |payload|
116
- payload[:result] = @adapter.get_all
110
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
111
+ payload[:result] = @adapter.get_all(**kwargs)
117
112
  end
118
113
  end
119
114
 
120
115
  # Public
121
116
  def enable(feature, gate, thing)
122
- payload = {
117
+ default_payload = {
123
118
  operation: :enable,
124
119
  adapter_name: @adapter.name,
125
120
  feature_name: feature.name,
126
121
  gate_name: gate.name,
122
+ thing_value: thing.value,
127
123
  }
128
124
 
129
- @instrumenter.instrument(InstrumentationName, payload) do |payload|
125
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
130
126
  payload[:result] = @adapter.enable(feature, gate, thing)
131
127
  end
132
128
  end
133
129
 
134
130
  # Public
135
131
  def disable(feature, gate, thing)
136
- payload = {
132
+ default_payload = {
137
133
  operation: :disable,
138
134
  adapter_name: @adapter.name,
139
135
  feature_name: feature.name,
140
136
  gate_name: gate.name,
137
+ thing_value: thing.value,
141
138
  }
142
139
 
143
- @instrumenter.instrument(InstrumentationName, payload) do |payload|
140
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
144
141
  payload[:result] = @adapter.disable(feature, gate, thing)
145
142
  end
146
143
  end
144
+
145
+ def import(source)
146
+ default_payload = {
147
+ operation: :import,
148
+ adapter_name: @adapter.name,
149
+ }
150
+
151
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
152
+ payload[:result] = @adapter.import(source)
153
+ end
154
+ end
155
+
156
+ def export(format: :json, version: 1)
157
+ default_payload = {
158
+ operation: :export,
159
+ adapter_name: @adapter.name,
160
+ format: format,
161
+ version: version,
162
+ }
163
+
164
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
165
+ payload[:result] = @adapter.export(format: format, version: version)
166
+ end
167
+ end
147
168
  end
148
169
  end
149
170
  end