fino 1.9.1 → 1.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d244c8f2e96f931cda9cb2b996d8cddaa37232ba06a1d5a8780a42121b45b025
4
- data.tar.gz: 641f2f41ab79d7685036704e76879389509f82b4d096f00f2ab07404dfdc5db4
3
+ metadata.gz: b848e452b4dd15c07680a8e27822e978a35152e8a2fcf24193e61af3bdbc2c55
4
+ data.tar.gz: dabde784e2b9c36d9f258b70a9885052f8d567e0ffc21ace004d1441bb219651
5
5
  SHA512:
6
- metadata.gz: d3940a3eae361e59e2696ac55b254a8199cd9f450b872e7f34b94b415591590850973eef1811b9358d76351b41445f01bc21649e9dc68ec4fd7b5d175990a5cf
7
- data.tar.gz: cd740a8f69fb2a91f4a9c3b15822e0c6ee45ae22e606e909994ef19c7062afd2e979a8bbae09484ab8963c817ad8455944039269cdcdc3ebef51cdf3afb8142a
6
+ metadata.gz: 795b2922a6b205ce6e9015f34c69d7cf7b5b501d54cec81968766d6510148d734b1f0c09fd3193646616759b826964aef9def7d7a572e11dbe362ca50f06a6fe
7
+ data.tar.gz: 9a85da0390f427490012c41a2eb004bbdb7bdabdf7a2166da450fc7e003d010f9a97baed16ea2f6cdb316a41474703198220ebb2dda0b28bac12cf86d7f96775
data/README.md CHANGED
@@ -83,6 +83,126 @@ Fino.disable(:maintenance_mode, for: "qa")
83
83
  Fino.enabled?(:maintenance_mode, for: "qa") #=> false
84
84
  ```
85
85
 
86
+ ### Select setting
87
+
88
+ The simplest way to define select setting in fino is the following
89
+
90
+ ```ruby
91
+ Fino.configure do
92
+ # ...
93
+ section :storefront, label: "Storefront" do
94
+ setting :purchase_button_color,
95
+ :select,
96
+ options: [
97
+ Fino::Settings::Select::Option.new(label: "Red", value: "red"),
98
+ Fino::Settings::Select::Option.new(label: "Blue", value: "blue")
99
+ ],
100
+ default: "red",
101
+ description: "Color of the purchase button"
102
+ end
103
+ # ...
104
+ end
105
+ ```
106
+
107
+ Options must be an array of `Fino::Settings::Select::Option` instances
108
+
109
+ Then you can interact with the setting like that
110
+
111
+ ```ruby
112
+ # Read selected option
113
+ selected_option = Fino.value(:purchase_button_color, at: :storefront)
114
+ # => #<Fino::Settings::Select::Option:0x0000000124fecd90 @label="Red", @metadata={}, @value="red">
115
+
116
+ # Read setting value
117
+ selected_option.value
118
+ # => "red"
119
+
120
+ # Read options
121
+ Fino.setting(:purchase_button_color, at: :storefront).options
122
+ # => [#<Fino::Settings::Select::Option:0x0000000124fecd90 @label="Red", @metadata={}, @value="red">,
123
+ #<Fino::Settings::Select::Option:0x0000000124fecca0 @label="Blue", @metadata={}, @value="blue">]
124
+ ```
125
+
126
+ #### Dynamic option
127
+
128
+ Options can also be defined dynamically using any callable object. Let's take a look at dynamic options in an example
129
+ of LLM model setting using [RubyLLM](https://github.com/crmne/ruby_llm) by @crmne
130
+
131
+ To define dynamic select options, use `:select` as setting type and provide a callable to `options`. Your object's
132
+ `call` method might be called with `refresh` option which is true only when user initiates options refresh manually.
133
+ This is useful for updating models list in RubyLLM for example
134
+
135
+ ```ruby
136
+ section :llm, label: "LLM" do
137
+ setting :model,
138
+ :select,
139
+ options: proc { |refresh:|
140
+ RubyLLM.models.refresh! if refresh
141
+ models = RubyLLM.models.chat_models
142
+
143
+ openai_models = models.by_provider(:openai)
144
+ anthropic_models = models.by_provider(:anthropic)
145
+
146
+ build_pricing_label = proc do |model|
147
+ text_pricing = model.pricing&.text_tokens
148
+ next unless text_pricing && text_pricing.input && text_pricing.output
149
+
150
+ "$#{text_pricing.input} / $#{text_pricing.output} per 1M tokens"
151
+ end
152
+
153
+ [*openai_models, *anthropic_models].map do |model|
154
+ Fino::Settings::Select::Option.new(
155
+ label: model.name,
156
+ value: model.id,
157
+ metadata: {
158
+ provider: model.provider_class.name,
159
+ pricing: build_pricing_label.call(model)
160
+ }.compact
161
+ )
162
+ end
163
+ },
164
+ default: "gpt-5",
165
+ description: "Chat model for AI-powered features"
166
+ end
167
+ end
168
+ ```
169
+
170
+ ```ruby
171
+ # Read selected option
172
+ selected_option = Fino.setting(:model, at: :llm).value
173
+ # => #<Fino::Settings::Select::Option:0x0000000126257728
174
+ # @label="GPT-4",
175
+ # @metadata={provider: "OpenAI", pricing: "$30 / $60 per 1M tokens"},
176
+ # @value="gpt-4">
177
+
178
+ # Read setting value
179
+ selected_option.value
180
+ # => "gpt-4"
181
+
182
+ # Read options
183
+ Fino.setting(:model, at: :llm).options
184
+ # => [#<Fino::Settings::Select::Option:0x000000012629e448
185
+ # @label="GPT-5.3 Codex Spark",
186
+ # @metadata={provider: "OpenAI", pricing: "$1.75 / $14 per 1M tokens"},
187
+ # @value="gpt-5.3-codex-spark">,
188
+ # #<Fino::Settings::Select::Option:0x000000012629e1c8
189
+ # @label="GPT-5.4",
190
+ # @metadata={provider: "OpenAI", pricing: "$2.5 / $15 per 1M tokens"},
191
+ # @value="gpt-5.4">, ...]
192
+
193
+ # Refresh options
194
+ Fino.setting(:model, at: :llm).refresh!
195
+ # I, [2026-03-22T18:23:13.615270 #67293] INFO -- RubyLLM: Fetching models from providers:
196
+ # I, [2026-03-22T18:23:13.615934 #67293] INFO -- RubyLLM: Fetching models from models.dev API...
197
+ ```
198
+
199
+ #### Use with RubyLLM
200
+
201
+ ```ruby
202
+ chat = RubyLLM.chat(model: Fino.setting(:model, at: :llm).value)
203
+ chat.ask "Why Ruby?"
204
+ ```
205
+
86
206
  ### Overrides
87
207
 
88
208
  ```ruby
@@ -116,6 +236,44 @@ Fino.value(:model, at: :openai, for: "user_1") #=> "gpt-6"
116
236
  Fino.value(:model, at: :openai, for: "user_2") #=> "gpt-5"
117
237
  ```
118
238
 
239
+ #### Experiment analysis
240
+
241
+ Some Fino adapters support A/B testing analysis, e.g built-in Redis adapter
242
+
243
+ When you run an A/B test for a setting, fino automatically calculates variant based on a stable identifier you pass as
244
+ a `for` option
245
+
246
+ ```ruby
247
+ Fino.set(model: "gpt-5", at: :openai, variants: { 20.0 => "gpt-6" })
248
+
249
+ Fino.value(:model, at: :openai, for: "user_1") #=> "gpt-6"
250
+ Fino.value(:model, at: :openai, for: "user_1") #=> "gpt-6"
251
+
252
+ Fino.value(:model, at: :openai, for: "user_2") #=> "gpt-5"
253
+ ```
254
+
255
+ Later in your code, when user performs a "desired" action, simply call
256
+
257
+ ```ruby
258
+ Fino.convert!(:model, at: :openai, for: "user_2")
259
+ ```
260
+
261
+ to record a "convertion" for `user_2`. As Fino knows the right variant for `user_2`, conversion will be counted
262
+ todards it. Thanks to that later you'll be able to call
263
+
264
+ ```ruby
265
+ Fino.analyse(:model, at: :openai)
266
+ ```
267
+
268
+ to receive a detailed report over variants performance. Also bar charts comparing all variants and a chart displaying
269
+ amount of conversions over time per variant will be accessible on UI with `fino-rails`
270
+
271
+ To reset analysis data for an experiment:
272
+
273
+ ```ruby
274
+ Fino.reset_analysis!(:model, at: :openai)
275
+ ```
276
+
119
277
  ### Unit conversion
120
278
 
121
279
  Fino is able to convert numeric settings into various units
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fino::AbTesting::Analysis
4
+ class VariantData
5
+ attr_reader :variant, :conversions_count, :daily_conversions
6
+
7
+ def initialize(variant:, conversions_count:, daily_conversions:)
8
+ @variant = variant
9
+ @conversions_count = conversions_count
10
+ @daily_conversions = daily_conversions
11
+ end
12
+ end
13
+
14
+ class << self
15
+ def from_raw_conversions(setting_instance, raw_conversions) # rubocop:disable Metrics/MethodLength
16
+ experiment = setting_instance.experiment
17
+
18
+ variants_data = experiment.variants.map do |variant|
19
+ entries = raw_conversions.fetch(variant, [])
20
+
21
+ daily_conversions = entries
22
+ .group_by { |_scope, score| Time.at(score / 1000.0).to_date.to_s }
23
+ .transform_values(&:size)
24
+
25
+ Fino::AbTesting::Analysis::VariantData.new(
26
+ variant: variant,
27
+ conversions_count: entries.size,
28
+ daily_conversions: daily_conversions
29
+ )
30
+ end
31
+
32
+ Fino::AbTesting::Analysis.new(variants_data)
33
+ end
34
+ end
35
+
36
+ attr_reader :variants_data
37
+
38
+ def initialize(variants_data)
39
+ @variants_data = variants_data
40
+ end
41
+
42
+ def any_conversions?
43
+ variants_data.any? { |vd| vd.conversions_count > 0 }
44
+ end
45
+
46
+ def total_conversions
47
+ variants_data.sum(&:conversions_count)
48
+ end
49
+ end
@@ -5,19 +5,25 @@ class Fino::AbTesting::Variant
5
5
  def inspect
6
6
  "Fino::AbTesting::Variant::CONTROL_VALUE"
7
7
  end
8
+
9
+ def to_s
10
+ "control"
11
+ end
8
12
  end.new
9
13
 
10
14
  include Fino::PrettyInspectable
11
15
 
12
- attr_reader :id, :percentage, :value
16
+ attr_reader :percentage, :value
13
17
 
14
18
  def initialize(percentage:, value:)
15
- @id = SecureRandom.uuid
16
-
17
19
  @percentage = percentage.to_f
18
20
  @value = value
19
21
  end
20
22
 
23
+ def id
24
+ "#{percentage}-#{value}"
25
+ end
26
+
21
27
  private
22
28
 
23
29
  def inspectable_attributes
data/lib/fino/adapter.rb CHANGED
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fino::Adapter
4
+ AB_TESTING_ANALYSIS_METHODS = %i[record_ab_testing_conversion read_ab_testing_conversions clear_ab_testing_conversions].freeze
5
+
6
+ def supports_ab_testing_analysis?
7
+ AB_TESTING_ANALYSIS_METHODS.all? { |method| respond_to?(method) }
8
+ end
9
+
4
10
  def read(setting_key)
5
11
  raise NotImplementedError
6
12
  end
@@ -5,7 +5,8 @@ class Fino::Definition::Setting
5
5
  Fino::Settings::String,
6
6
  Fino::Settings::Integer,
7
7
  Fino::Settings::Float,
8
- Fino::Settings::Boolean
8
+ Fino::Settings::Boolean,
9
+ Fino::Settings::Select
9
10
  ].freeze
10
11
 
11
12
  SETTING_TYPE_TO_TYPE_CLASS_MAPPING = TYPE_CLASSES.each_with_object({}) do |klass, hash|
@@ -27,8 +28,16 @@ class Fino::Definition::Setting
27
28
  end
28
29
  end
29
30
 
31
+ def serialize(value)
32
+ type_class.serialize(self, value)
33
+ end
34
+
35
+ def deserialize(raw_value)
36
+ type_class.deserialize(self, raw_value)
37
+ end
38
+
30
39
  def default
31
- defined?(@default) ? @default : @default = type_class.deserialize(options[:default])
40
+ defined?(@default) ? @default : @default = deserialize(options[:default])
32
41
  end
33
42
 
34
43
  def description
@@ -40,11 +49,11 @@ class Fino::Definition::Setting
40
49
  end
41
50
 
42
51
  def path
43
- @path ||= [setting_name, section_definition&.name].compact
52
+ @path ||= [section_definition&.name, setting_name].compact
44
53
  end
45
54
 
46
55
  def key
47
- @key ||= path.reverse.join("/")
56
+ @key ||= path.join("/")
48
57
  end
49
58
 
50
59
  def eql?(other)
data/lib/fino/ext/hash.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Fino::Ext::Hash
4
4
  refine Hash do
5
5
  def deep_set(value, *path)
6
- item = path.pop
6
+ item = path.shift
7
7
 
8
8
  if path.empty?
9
9
  self[item] = value
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fino::Library::AbTestingAnalysisSupport
4
+ SettingNotAbTested = Class.new(Fino::Error)
5
+ AdapterDoesNotSupportAbTestingAnalysis = Class.new(Fino::Error)
6
+
7
+ def convert!(setting_name, at: nil, for: nil, time: Time.now)
8
+ ensure_ab_testing_analysis_supported!
9
+
10
+ scope = binding.local_variable_get(:for)
11
+
12
+ setting_instance = fetch_ab_testable_setting(setting_name, at: at)
13
+ variant = setting_instance.experiment.variant(for: scope)
14
+ timestamp_ms = (time.to_f * 1000).to_i
15
+
16
+ adapter.record_ab_testing_conversion(setting_instance.definition, variant, scope, timestamp_ms)
17
+ end
18
+
19
+ def convert(setting_name, at: nil, time: Time.now, **context)
20
+ convert!(setting_name, at: at, time: time, **context)
21
+ rescue SettingNotAbTested, AdapterDoesNotSupportAbTestingAnalysis
22
+ nil
23
+ end
24
+
25
+ def analyse(setting_name, at: nil)
26
+ ensure_ab_testing_analysis_supported!
27
+
28
+ setting_instance = fetch_ab_testable_setting(setting_name, at: at)
29
+ raw_conversions = adapter.read_ab_testing_conversions(
30
+ setting_instance.definition,
31
+ setting_instance.experiment.variants
32
+ )
33
+
34
+ Fino::AbTesting::Analysis.from_raw_conversions(setting_instance, raw_conversions)
35
+ end
36
+
37
+ def reset_analysis!(setting_name, at: nil)
38
+ ensure_ab_testing_analysis_supported!
39
+
40
+ setting_instance = fetch_ab_testable_setting(setting_name, at: at)
41
+ adapter.clear_ab_testing_conversions(setting_instance.definition.key)
42
+ end
43
+
44
+ private
45
+
46
+ def ensure_ab_testing_analysis_supported!
47
+ return if adapter.supports_ab_testing_analysis?
48
+
49
+ raise AdapterDoesNotSupportAbTestingAnalysis, "Adapter #{adapter.class.name} does not support A/B testing analysis"
50
+ end
51
+
52
+ def fetch_ab_testable_setting(setting_name, at: nil)
53
+ setting(setting_name, at: at).tap do |setting_instance|
54
+ raise SettingNotAbTested, "Setting #{setting_name} is not A/B tested" unless setting_instance.ab_tested?
55
+ end
56
+ end
57
+ end
data/lib/fino/library.rb CHANGED
@@ -4,6 +4,7 @@ require "forwardable"
4
4
 
5
5
  class Fino::Library
6
6
  include FeatureTogglesSupport
7
+ include AbTestingAnalysisSupport
7
8
 
8
9
  def initialize(configuration)
9
10
  @configuration = configuration
@@ -40,7 +41,7 @@ class Fino::Library
40
41
  setting_definition = build_setting_definition(setting_name, at: at)
41
42
  current_setting = pipeline.read(setting_definition)
42
43
 
43
- deserialized_overrides = overrides.transform_values { |v| setting_definition.type_class.deserialize(v) }
44
+ deserialized_overrides = overrides.transform_values { |v| setting_definition.deserialize(v) }
44
45
  merged_overrides = current_setting.overrides.merge(deserialized_overrides)
45
46
 
46
47
  variants = current_setting.experiment&.variants || []
@@ -62,16 +63,16 @@ class Fino::Library
62
63
 
63
64
  setting_name, raw_value = data.first
64
65
  setting_definition = build_setting_definition(setting_name, at: at)
65
- value = setting_definition.type_class.deserialize(raw_value)
66
+ value = setting_definition.deserialize(raw_value)
66
67
 
67
- overrides = raw_overrides.transform_values { |v| setting_definition.type_class.deserialize(v) }
68
+ overrides = raw_overrides.transform_values { |v| setting_definition.deserialize(v) }
68
69
 
69
70
  experiment = Fino::AbTesting::Experiment.new(setting_definition)
70
71
 
71
72
  raw_variants.map do |percentage, value|
72
73
  experiment << Fino::AbTesting::Variant.new(
73
74
  percentage: percentage,
74
- value: setting_definition.type_class.deserialize(value)
75
+ value: setting_definition.deserialize(value)
75
76
  )
76
77
  end
77
78
 
@@ -113,7 +114,7 @@ class Fino::Library
113
114
  attr_reader :configuration
114
115
 
115
116
  def build_setting_definition(setting_name, at: nil)
116
- configuration.registry.setting_definition!(setting_name.to_s, at&.to_s)
117
+ configuration.registry.setting_definition!(setting_name.to_s, at: at&.to_s)
117
118
  end
118
119
 
119
120
  def pipeline
data/lib/fino/registry.rb CHANGED
@@ -51,7 +51,7 @@ class Fino::Registry
51
51
 
52
52
  using Fino::Ext::Hash
53
53
 
54
- attr_reader :setting_definitions, :section_definitions
54
+ attr_reader :setting_definitions, :section_definitions, :option_registry
55
55
 
56
56
  def initialize
57
57
  @setting_definitions = Set.new
@@ -59,14 +59,16 @@ class Fino::Registry
59
59
 
60
60
  @section_definitions = Set.new
61
61
  @section_definitions_by_name = {}
62
+
63
+ @option_registry = Fino::Settings::Select::OptionRegistry.new
62
64
  end
63
65
 
64
- def setting_definition(*path)
65
- @setting_definitions_by_path.dig(*path.compact.reverse.map(&:to_s))
66
+ def setting_definition(setting_name, at: nil)
67
+ @setting_definitions_by_path.dig(*[at, setting_name].compact.map(&:to_s))
66
68
  end
67
69
 
68
- def setting_definition!(*path)
69
- setting_definition(*path).tap do |definition|
70
+ def setting_definition!(setting_name, at: nil)
71
+ setting_definition(setting_name, at: at).tap do |definition|
70
72
  raise UnknownSetting, "Unknown setting: #{path.compact.join('.')}" unless definition
71
73
  end
72
74
  end
@@ -91,7 +93,18 @@ class Fino::Registry
91
93
  raise DuplicateSetting, "#{setting_definition.setting_name} is already registered at #{setting_definition.key}"
92
94
  end
93
95
 
94
- @setting_definitions_by_path.deep_set(setting_definition, *setting_definition.path.map(&:to_s))
96
+ path = setting_definition.path.map(&:to_s)
97
+
98
+ @setting_definitions_by_path.deep_set(setting_definition, *path)
99
+
100
+ register_type_specific_data(setting_definition, path)
101
+ end
102
+
103
+ def register_type_specific_data(setting_definition, path)
104
+ case setting_definition.type
105
+ when Fino::Settings::Select.type_identifier
106
+ @option_registry.register(setting_definition.options.fetch(:options, []), path)
107
+ end
95
108
  end
96
109
 
97
110
  def register_section(section_definition)
@@ -23,7 +23,7 @@ class Fino::SettingBuilder
23
23
  private
24
24
 
25
25
  def deserialize_global_value(raw_value)
26
- return setting_definition.options[:default] if raw_value.equal?(Fino::EMPTINESS)
26
+ return setting_definition.default if raw_value.equal?(Fino::EMPTINESS)
27
27
 
28
28
  deserialize(raw_value)
29
29
  end
@@ -46,6 +46,6 @@ class Fino::SettingBuilder
46
46
  end
47
47
 
48
48
  def deserialize(value)
49
- setting_definition.type_class.deserialize(value)
49
+ setting_definition.deserialize(value)
50
50
  end
51
51
  end
@@ -6,11 +6,11 @@ class Fino::Settings::Boolean
6
6
  self.type_identifier = :boolean
7
7
 
8
8
  class << self
9
- def serialize(value)
9
+ def serialize(_setting_definition, value)
10
10
  value ? "1" : "0"
11
11
  end
12
12
 
13
- def deserialize(raw_value)
13
+ def deserialize(_setting_definition, raw_value)
14
14
  case raw_value
15
15
  when "1", 1, true, "true", "t", "yes", "y" then true
16
16
  else false
@@ -7,11 +7,11 @@ class Fino::Settings::Float
7
7
  self.type_identifier = :float
8
8
 
9
9
  class << self
10
- def serialize(value)
10
+ def serialize(_setting_definition, value)
11
11
  value.to_s
12
12
  end
13
13
 
14
- def deserialize(raw_value)
14
+ def deserialize(_setting_definition, raw_value)
15
15
  raw_value.to_f
16
16
  end
17
17
  end
@@ -7,11 +7,11 @@ class Fino::Settings::Integer
7
7
  self.type_identifier = :integer
8
8
 
9
9
  class << self
10
- def serialize(value)
10
+ def serialize(_setting_definition, value)
11
11
  value.to_s
12
12
  end
13
13
 
14
- def deserialize(raw_value)
14
+ def deserialize(_setting_definition, raw_value)
15
15
  raw_value.to_i
16
16
  end
17
17
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fino::Settings::Select
4
+ class Option
5
+ attr_reader :label, :value, :metadata
6
+
7
+ def initialize(label:, value:, metadata: {})
8
+ @label = label
9
+ @value = value
10
+ @metadata = metadata
11
+ end
12
+
13
+ def to_s
14
+ value.to_s
15
+ end
16
+ end
17
+
18
+ class OptionRegistry
19
+ using Fino::Ext::Hash
20
+
21
+ UnknownOption = Class.new(Fino::Error)
22
+
23
+ def initialize
24
+ @options = {}
25
+ @indexed_options = {}
26
+ @builders = {}
27
+ end
28
+
29
+ def register(builder, path)
30
+ @builders.deep_set(builder, *path)
31
+ end
32
+
33
+ def options(*path)
34
+ resolve(path)
35
+ @options.dig(*path.compact.map(&:to_s))
36
+ end
37
+
38
+ def option(value, *path)
39
+ resolve(path)
40
+ @indexed_options.dig(*path.compact.map(&:to_s)).fetch(value) do
41
+ raise UnknownOption, "Unknown option: #{value} for setting: #{path.compact.join('.')}"
42
+ end
43
+ end
44
+
45
+ def refreshable?(*path)
46
+ builder = @builders.dig(*path.compact.map(&:to_s))
47
+ builder.respond_to?(:call)
48
+ end
49
+
50
+ def refresh!(*path)
51
+ string_path = path.compact.map(&:to_s)
52
+ builder = @builders.dig(*string_path)
53
+
54
+ return unless builder.respond_to?(:call)
55
+
56
+ resolve(path, force: true)
57
+ @options.dig(*string_path)
58
+ end
59
+
60
+ private
61
+
62
+ def resolve(path, force: false)
63
+ string_path = path.compact.map(&:to_s)
64
+
65
+ return if !force && @options.dig(*string_path)
66
+
67
+ builder = @builders.dig(*string_path)
68
+ options = builder.respond_to?(:call) ? builder.call(refresh: force) : builder
69
+
70
+ @options.deep_set(options, *string_path)
71
+ @indexed_options.deep_set(options.index_by(&:value), *string_path)
72
+ end
73
+ end
74
+
75
+ include Fino::Setting
76
+
77
+ self.type_identifier = :select
78
+
79
+ class << self
80
+ def serialize(setting_definition, value)
81
+ Fino.registry.option_registry.option(value.value, *setting_definition.path).value
82
+ end
83
+
84
+ def deserialize(setting_definition, raw_value)
85
+ Fino.registry.option_registry.option(raw_value, *setting_definition.path)
86
+ end
87
+ end
88
+
89
+ def options
90
+ Fino.registry.option_registry.options(*definition.path)
91
+ end
92
+
93
+ def refreshable?
94
+ Fino.registry.option_registry.refreshable?(*definition.path)
95
+ end
96
+
97
+ def refresh!
98
+ Fino.registry.option_registry.refresh!(*definition.path)
99
+ end
100
+ end
@@ -6,11 +6,11 @@ class Fino::Settings::String
6
6
  self.type_identifier = :string
7
7
 
8
8
  class << self
9
- def serialize(value)
9
+ def serialize(_setting_definition, value)
10
10
  value
11
11
  end
12
12
 
13
- def deserialize(raw_value)
13
+ def deserialize(_setting_definition, raw_value)
14
14
  raw_value
15
15
  end
16
16
  end
@@ -21,7 +21,7 @@ module Fino
21
21
  end
22
22
 
23
23
  def write(setting_definition, value, overrides, variants)
24
- serialize_value = ->(raw_value) { setting_definition.type_class.serialize(raw_value) }
24
+ serialize_value = ->(raw_value) { setting_definition.serialize(raw_value) }
25
25
 
26
26
  data = { VALUE_KEY => serialize_value.call(value) }
27
27
 
data/lib/fino/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fino
4
- VERSION = "1.9.1"
4
+ VERSION = "1.10.0"
5
5
  REQUIRED_RUBY_VERSION = ">= 3.2.0"
6
6
  end
data/lib/fino.rb CHANGED
@@ -47,7 +47,11 @@ module Fino
47
47
  :settings,
48
48
  :slice,
49
49
  :set,
50
- :add_override
50
+ :add_override,
51
+ :convert,
52
+ :convert!,
53
+ :analyse,
54
+ :reset_analysis!
51
55
 
52
56
  def library
53
57
  raise NotImplementedError
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fino
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.1
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Egor Iskrenkov
@@ -33,6 +33,7 @@ files:
33
33
  - README.md
34
34
  - lib/fino-solid.rb
35
35
  - lib/fino.rb
36
+ - lib/fino/ab_testing/analysis.rb
36
37
  - lib/fino/ab_testing/experiment.rb
37
38
  - lib/fino/ab_testing/variant.rb
38
39
  - lib/fino/ab_testing/variant_picker.rb
@@ -46,6 +47,7 @@ files:
46
47
  - lib/fino/expirator.rb
47
48
  - lib/fino/ext/hash.rb
48
49
  - lib/fino/library.rb
50
+ - lib/fino/library/ab_testing_analysis_support.rb
49
51
  - lib/fino/library/feature_toggles_support.rb
50
52
  - lib/fino/metadata.rb
51
53
  - lib/fino/pipe.rb
@@ -61,6 +63,7 @@ files:
61
63
  - lib/fino/settings/integer.rb
62
64
  - lib/fino/settings/numeric.rb
63
65
  - lib/fino/settings/section.rb
66
+ - lib/fino/settings/select.rb
64
67
  - lib/fino/settings/string.rb
65
68
  - lib/fino/solid.rb
66
69
  - lib/fino/solid/adapter.rb
@@ -90,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
93
  - !ruby/object:Gem::Version
91
94
  version: '0'
92
95
  requirements: []
93
- rubygems_version: 4.0.3
96
+ rubygems_version: 3.6.9
94
97
  specification_version: 4
95
98
  summary: Plug & play distributed settings engine for Ruby and Rails
96
99
  test_files: []