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 +4 -4
- data/README.md +158 -0
- data/lib/fino/ab_testing/analysis.rb +49 -0
- data/lib/fino/ab_testing/variant.rb +9 -3
- data/lib/fino/adapter.rb +6 -0
- data/lib/fino/definition/setting.rb +13 -4
- data/lib/fino/ext/hash.rb +1 -1
- data/lib/fino/library/ab_testing_analysis_support.rb +57 -0
- data/lib/fino/library.rb +6 -5
- data/lib/fino/registry.rb +19 -6
- data/lib/fino/setting_builder.rb +2 -2
- data/lib/fino/settings/boolean.rb +2 -2
- data/lib/fino/settings/float.rb +2 -2
- data/lib/fino/settings/integer.rb +2 -2
- data/lib/fino/settings/select.rb +100 -0
- data/lib/fino/settings/string.rb +2 -2
- data/lib/fino/solid/adapter.rb +1 -1
- data/lib/fino/version.rb +1 -1
- data/lib/fino.rb +5 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b848e452b4dd15c07680a8e27822e978a35152e8a2fcf24193e61af3bdbc2c55
|
|
4
|
+
data.tar.gz: dabde784e2b9c36d9f258b70a9885052f8d567e0ffc21ace004d1441bb219651
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 :
|
|
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 =
|
|
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 ||= [
|
|
52
|
+
@path ||= [section_definition&.name, setting_name].compact
|
|
44
53
|
end
|
|
45
54
|
|
|
46
55
|
def key
|
|
47
|
-
@key ||= path.
|
|
56
|
+
@key ||= path.join("/")
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
def eql?(other)
|
data/lib/fino/ext/hash.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
66
|
+
value = setting_definition.deserialize(raw_value)
|
|
66
67
|
|
|
67
|
-
overrides = raw_overrides.transform_values { |v| setting_definition.
|
|
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.
|
|
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(
|
|
65
|
-
@setting_definitions_by_path.dig(*
|
|
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!(
|
|
69
|
-
setting_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
|
-
|
|
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)
|
data/lib/fino/setting_builder.rb
CHANGED
|
@@ -23,7 +23,7 @@ class Fino::SettingBuilder
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
25
|
def deserialize_global_value(raw_value)
|
|
26
|
-
return setting_definition.
|
|
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.
|
|
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
|
data/lib/fino/settings/float.rb
CHANGED
|
@@ -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
|
data/lib/fino/settings/string.rb
CHANGED
|
@@ -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
|
data/lib/fino/solid/adapter.rb
CHANGED
|
@@ -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.
|
|
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
data/lib/fino.rb
CHANGED
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.
|
|
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:
|
|
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: []
|