fino-solid 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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +196 -0
- data/lib/fino/solid/README.md +284 -0
- data/lib/fino/solid/adapter.rb +76 -0
- data/lib/fino/solid/generators/install/USAGE +8 -0
- data/lib/fino/solid/generators/install/install_generator.rb +26 -0
- data/lib/fino/solid/generators/install/templates/create_fino_settings.rb.tt +12 -0
- data/lib/fino/solid/railtie.rb +8 -0
- data/lib/fino/solid/record.rb +11 -0
- data/lib/fino/solid/setting.rb +13 -0
- data/lib/fino/solid.rb +25 -0
- data/lib/fino/version.rb +6 -0
- data/lib/fino-solid.rb +4 -0
- metadata +125 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2a58a6b231e01407c065d6512bf369ff01b59cf31e8a4572a0f5b10c6f713ae1
|
|
4
|
+
data.tar.gz: 60a3d6bcceb5803271a61756aa0c466ebc3463e4356143118438d145427084d0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 11e09a0f69092545b9ff9c97f47e55660c823d4774aa34f878bb6a2f15db4669b7bf9974479a3c8a93ec301071942673ec0204d42d998f75fac0ec97aba3be2c
|
|
7
|
+
data.tar.gz: bab475d8c0590a1475fa096b25f60d161c2f0acd4fba7dfed42ad72e2c8a8233470cb1556286917c44b53f3571f17f53a94fc7aa0ef54d0a34ab4f1b5f755336
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Egor Iskrenkov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Fino
|
|
2
|
+
|
|
3
|
+
⚠️ Fino in active development phase at wasn't properly battle tested in production just yet. Give us a star and stay tuned for Production test results and new features
|
|
4
|
+
|
|
5
|
+
Fino is a dynamic settings engine for Ruby and Rails
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
### Define settings via DSL
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "fino-redis"
|
|
13
|
+
|
|
14
|
+
Fino.configure do
|
|
15
|
+
adapter do
|
|
16
|
+
Fino::Redis::Adapter.new(
|
|
17
|
+
Redis.new(**Rails.application.config_for(:redis))
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
cache { Fino::Cache::Memory.new(expires_in: 3.seconds) }
|
|
22
|
+
|
|
23
|
+
settings do
|
|
24
|
+
setting :maintenance_mode, :boolean, default: false
|
|
25
|
+
|
|
26
|
+
setting :api_rate_limit,
|
|
27
|
+
:integer,
|
|
28
|
+
default: 1000,
|
|
29
|
+
description: "Maximum API requests per minute per user to prevent abuse"
|
|
30
|
+
|
|
31
|
+
section :openai, label: "OpenAI" do
|
|
32
|
+
setting :model,
|
|
33
|
+
:string,
|
|
34
|
+
default: "gpt-5",
|
|
35
|
+
description: "OpenAI model"
|
|
36
|
+
|
|
37
|
+
setting :temperature,
|
|
38
|
+
:float,
|
|
39
|
+
default: 0.7,
|
|
40
|
+
description: "Model temperature"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
section :feature_toggles, label: "Feature Toggles" do
|
|
44
|
+
setting :new_ui, :boolean, default: true
|
|
45
|
+
setting :beta_functionality, :boolean, default: false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
section :my_micro_service, label: "My Micro Service" do
|
|
49
|
+
setting :http_read_timeout, :integer, default: 200 # in ms
|
|
50
|
+
setting :http_open_timeout, :integer, default: 100 # in ms
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Work with settings
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Fino.value(:model, at: :openai) #=> "gpt-5"
|
|
60
|
+
Fino.value(:temperature, at: :openai) #=> 0.7
|
|
61
|
+
|
|
62
|
+
Fino.values(:model, :temperature, at: :openai) #=> ["gpt-4", 0.7]
|
|
63
|
+
|
|
64
|
+
Fino.set(model: "gpt-5", at: :openai)
|
|
65
|
+
Fino.value(:model, at: :openai) #=> "gpt-5"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Overrides
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
Fino.value(:model, at: :openai) #=> "gpt-5"
|
|
72
|
+
|
|
73
|
+
Fino.set(model: "gpt-5", at: :openai, overrides: { "qa" => "our_local_model_not_to_pay_to_sam_altman" })
|
|
74
|
+
|
|
75
|
+
Fino.value(:model, at: :openai) #=> "gpt-5"
|
|
76
|
+
Fino.value(:model, at: :openai, for: "qa") #=> "our_local_model_not_to_pay_to_sam_altman"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### A/B testing
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Fino.value(:model, at: :openai) #=> "gpt-5"
|
|
83
|
+
|
|
84
|
+
# "gpt-5" becomes the control variant value and a 20.0% variant is created with value "gpt-6"
|
|
85
|
+
Fino.set(model: "gpt-5", at: :openai, variants: { 20.0 => "gpt-6" })
|
|
86
|
+
|
|
87
|
+
Fino.setting(:model, at: :openai).experiment.variant(for: "user_1") #=> #<Fino::AbTesting::Variant percentage: 20.0, value: "gpt-6">
|
|
88
|
+
|
|
89
|
+
# Picked variant is sticked to the user
|
|
90
|
+
Fino.value(:model, at: :openai, for: "user_1") #=> "gpt-6"
|
|
91
|
+
Fino.value(:model, at: :openai, for: "user_1") #=> "gpt-6"
|
|
92
|
+
|
|
93
|
+
Fino.value(:model, at: :openai, for: "user_2") #=> "gpt-5"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Rails integration
|
|
97
|
+
|
|
98
|
+
Fino easily integrates with Rails. Just add the gem to your Gemfile:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
gem "fino-rails", require: false
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
to get built-in UI engine for your settings!
|
|
105
|
+
|
|
106
|
+
### UI engine
|
|
107
|
+
|
|
108
|
+
Mount Fino Rails engine in your `config/routes.rb`:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
Rails.application.routes.draw do
|
|
112
|
+
mount Fino::Rails::Engine, at: "/fino"
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Configuration
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
Rails.application.configure do
|
|
120
|
+
config.fino.instrument = true
|
|
121
|
+
config.fino.log = true
|
|
122
|
+
config.fino.cache_within_request = false
|
|
123
|
+
config.fino.preload_before_request = true
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
<img width="1493" height="676" alt="Screenshot 2025-09-19 at 13 09 06" src="https://github.com/user-attachments/assets/19b6147a-e18c-41cf-aac7-99111efcc9d5" />
|
|
128
|
+
|
|
129
|
+
<img width="1775" height="845" alt="Screenshot 2025-09-19 at 13 09 33" src="https://github.com/user-attachments/assets/c0010abd-285d-43d0-ae5d-ce0edb781309" />
|
|
130
|
+
|
|
131
|
+
## Performance tweaks
|
|
132
|
+
|
|
133
|
+
1. In Memory cache
|
|
134
|
+
|
|
135
|
+
Fino provides in-memory settings caching functionality which will store settings received from adaper in memory for
|
|
136
|
+
a very quick access. As this kind of cache is not distributed between machines, but belongs to each process
|
|
137
|
+
separately, it's impossible to invalidate all at once, so be aware that setting update application time will depend
|
|
138
|
+
on cache TTL you configure
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
Fino.configure do
|
|
142
|
+
# ...
|
|
143
|
+
cache { Fino::Cache::Memory.new(expires_in: 3.seconds) }
|
|
144
|
+
# ...
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
2. Request scoped cache
|
|
149
|
+
|
|
150
|
+
When using Fino in Rails context it's possible to cache settings within request, in current thread storage. This is
|
|
151
|
+
safe way to cache settings as it's lifetime is limited, thus it is enabled by default
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
Rails.application.configure do
|
|
155
|
+
config.fino.cache_within_request = true
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
3. Preloading
|
|
160
|
+
|
|
161
|
+
In Rails context it is possible to tell Fino to preload multiple settings before processing request in a single
|
|
162
|
+
adapter call. Preloading is recommended for requests that use multiple different settings in their logic
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
# Preload all settings
|
|
166
|
+
Rails.application.configure do
|
|
167
|
+
config.fino.preload_before_request = true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Preload specific subset of settings depending on request
|
|
171
|
+
Rails.application.configure do
|
|
172
|
+
config.fino.preload_before_request = ->(request) {
|
|
173
|
+
case request.path
|
|
174
|
+
when "request/using/all/settings"
|
|
175
|
+
true
|
|
176
|
+
when "request/not/using/settings"
|
|
177
|
+
false
|
|
178
|
+
when "request/using/specific/settings"
|
|
179
|
+
[
|
|
180
|
+
:api_rate_limit,
|
|
181
|
+
openai: [:model, :temperature]
|
|
182
|
+
]
|
|
183
|
+
end
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Releasing
|
|
189
|
+
|
|
190
|
+
`rake release`
|
|
191
|
+
|
|
192
|
+
## Contributing
|
|
193
|
+
|
|
194
|
+
1. Fork it
|
|
195
|
+
2. Do contribution
|
|
196
|
+
6. Create Pull Request into this repo
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Fino::Solid
|
|
2
|
+
|
|
3
|
+
ActiveRecord adapter for [Fino](https://github.com/eiskrenkov/fino) settings engine, inspired by solid_queue.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Database-backed persistence** using ActiveRecord
|
|
8
|
+
- **Efficient read performance** with single-table design
|
|
9
|
+
- **Multi-database support** (SQLite, PostgreSQL, MySQL)
|
|
10
|
+
- **Rails integration** with migration generators
|
|
11
|
+
- **Supports all Fino features**: overrides, A/B testing, sections
|
|
12
|
+
- **Thread-safe** and production-ready
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add to your Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "fino-solid"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
### Rails Applications
|
|
31
|
+
|
|
32
|
+
1. Generate the migration:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bin/rails generate fino:solid:install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. Run the migration:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bin/rails db:migrate
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
3. Configure Fino to use the Solid adapter:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# config/initializers/fino.rb
|
|
48
|
+
require "fino-solid"
|
|
49
|
+
|
|
50
|
+
Fino.configure do
|
|
51
|
+
adapter { Fino::Solid::Adapter.new }
|
|
52
|
+
|
|
53
|
+
settings do
|
|
54
|
+
setting :maintenance_mode, :boolean, default: false
|
|
55
|
+
# ... your settings
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Non-Rails Applications
|
|
61
|
+
|
|
62
|
+
1. Create the migration manually:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
class CreateFinoSettings < ActiveRecord::Migration[7.0]
|
|
66
|
+
def change
|
|
67
|
+
create_table :fino_settings do |t|
|
|
68
|
+
t.string :key, null: false
|
|
69
|
+
t.text :data
|
|
70
|
+
t.timestamps
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
add_index :fino_settings, :key, unique: true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
2. Configure the adapter:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
require "fino-solid"
|
|
82
|
+
require "active_record"
|
|
83
|
+
|
|
84
|
+
ActiveRecord::Base.establish_connection(
|
|
85
|
+
adapter: "sqlite3",
|
|
86
|
+
database: "db/fino.sqlite3"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
Fino.configure do
|
|
90
|
+
adapter { Fino::Solid::Adapter.new }
|
|
91
|
+
|
|
92
|
+
settings do
|
|
93
|
+
# ... your settings
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
### Multi-Database Setup
|
|
101
|
+
|
|
102
|
+
If you're using Rails multi-database configuration (like solid_queue), you can configure Fino::Solid to use a specific database:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# config/initializers/fino.rb
|
|
106
|
+
Fino::Solid.configure do
|
|
107
|
+
self.connects_to = { database: { writing: :settings } }
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Then configure your database.yml:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
production:
|
|
115
|
+
primary:
|
|
116
|
+
<<: *default
|
|
117
|
+
database: my_app_production
|
|
118
|
+
settings:
|
|
119
|
+
<<: *default
|
|
120
|
+
database: my_app_settings_production
|
|
121
|
+
migrations_paths: db/settings_migrate
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Database Schema
|
|
125
|
+
|
|
126
|
+
The adapter uses a simple, efficient single-table design:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
create_table :fino_settings do |t|
|
|
130
|
+
t.string :key, null: false # "api_rate_limit" or "openai/model"
|
|
131
|
+
t.text :data # JSON: {"v": "value", "s/scope/v": "override", ...}
|
|
132
|
+
t.timestamps
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
add_index :fino_settings, :key, unique: true
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Data Structure
|
|
139
|
+
|
|
140
|
+
Settings are stored as JSON with this structure:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"v": "serialized_value",
|
|
145
|
+
"s/qa/v": "override_for_qa_scope",
|
|
146
|
+
"s/admin/v": "override_for_admin_scope",
|
|
147
|
+
"v/20.0/v": "variant_value_for_20_percent",
|
|
148
|
+
"v/30.0/v": "variant_value_for_30_percent"
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
This mirrors the Redis adapter structure for consistency.
|
|
153
|
+
|
|
154
|
+
## Performance
|
|
155
|
+
|
|
156
|
+
### Read Performance
|
|
157
|
+
|
|
158
|
+
- **Single setting**: 1 query (`SELECT * FROM fino_settings WHERE key = ?`)
|
|
159
|
+
- **Multiple settings**: 1 query (`SELECT * FROM fino_settings WHERE key IN (?, ?, ?)`)
|
|
160
|
+
- **All settings**: 1 query (`SELECT * FROM fino_settings`)
|
|
161
|
+
|
|
162
|
+
### Write Performance
|
|
163
|
+
|
|
164
|
+
- Uses ActiveRecord's `upsert` for atomic write operations
|
|
165
|
+
- Single transaction per setting update
|
|
166
|
+
|
|
167
|
+
### Optimization Tips
|
|
168
|
+
|
|
169
|
+
1. **Use in-memory cache** to reduce database queries:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
Fino.configure do
|
|
173
|
+
adapter { Fino::Solid::Adapter.new }
|
|
174
|
+
cache { Fino::Cache::Memory.new(expires_in: 3.seconds) }
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
2. **Batch reads** with `read_multi`:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
# In Rails:
|
|
182
|
+
Fino.values(:maintenance_mode, :api_rate_limit) # 1 query instead of 2
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
3. **Preload settings** in Rails:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
Rails.application.configure do
|
|
189
|
+
config.fino.preload_before_request = true
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Supported Databases
|
|
194
|
+
|
|
195
|
+
- **SQLite** 3.38+ (with JSON support)
|
|
196
|
+
- **PostgreSQL** 9.4+ (with JSON/JSONB)
|
|
197
|
+
- **MySQL** 5.7+ (with JSON support)
|
|
198
|
+
|
|
199
|
+
## Examples
|
|
200
|
+
|
|
201
|
+
### Basic Usage
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
# Set a value
|
|
205
|
+
Fino.set(api_rate_limit: 1000)
|
|
206
|
+
|
|
207
|
+
# Read a value
|
|
208
|
+
Fino.value(:api_rate_limit) # => 1000
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### With Overrides
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# Set value with scope overrides
|
|
215
|
+
Fino.set(
|
|
216
|
+
api_rate_limit: 1000,
|
|
217
|
+
overrides: {
|
|
218
|
+
"premium" => 5000,
|
|
219
|
+
"enterprise" => 10000
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Read with scope
|
|
224
|
+
Fino.value(:api_rate_limit) # => 1000
|
|
225
|
+
Fino.value(:api_rate_limit, for: "premium") # => 5000
|
|
226
|
+
Fino.value(:api_rate_limit, for: "enterprise") # => 10000
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### With A/B Testing
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
# Create an experiment
|
|
233
|
+
Fino.set(
|
|
234
|
+
api_rate_limit: 1000,
|
|
235
|
+
variants: {
|
|
236
|
+
20.0 => 2000, # 20% of users get 2000
|
|
237
|
+
30.0 => 3000 # 30% of users get 3000
|
|
238
|
+
}
|
|
239
|
+
# Remaining 50% get control value (1000)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Users are deterministically assigned to variants
|
|
243
|
+
Fino.value(:api_rate_limit, for: "user_123") # => 2000 (always same)
|
|
244
|
+
Fino.value(:api_rate_limit, for: "user_456") # => 1000 (control)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Comparison with Redis Adapter
|
|
248
|
+
|
|
249
|
+
| Feature | Fino::Solid | Fino::Redis |
|
|
250
|
+
|---------|-------------|-------------|
|
|
251
|
+
| Persistence | SQL Database | Redis |
|
|
252
|
+
| Read Performance | Excellent (indexed) | Excellent (in-memory) |
|
|
253
|
+
| Write Performance | Good (ACID) | Excellent |
|
|
254
|
+
| Durability | ACID compliant | Depends on Redis config |
|
|
255
|
+
| Multi-database | Native support | Namespace support |
|
|
256
|
+
| Backup/Restore | Standard DB tools | Redis tools |
|
|
257
|
+
| Query Flexibility | SQL available | Limited |
|
|
258
|
+
| Setup Complexity | Low (Rails) | Medium |
|
|
259
|
+
|
|
260
|
+
## Testing
|
|
261
|
+
|
|
262
|
+
The gem includes comprehensive integration tests using shared examples:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# Run Solid adapter tests
|
|
266
|
+
bundle exec rspec spec/integration/solid_adapter_spec.rb
|
|
267
|
+
|
|
268
|
+
# Test all databases
|
|
269
|
+
DATABASE=sqlite bundle exec rspec spec/integration/solid_adapter_spec.rb
|
|
270
|
+
DATABASE=postgresql bundle exec rspec spec/integration/solid_adapter_spec.rb
|
|
271
|
+
DATABASE=mysql bundle exec rspec spec/integration/solid_adapter_spec.rb
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Contributing
|
|
275
|
+
|
|
276
|
+
1. Fork it
|
|
277
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
278
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
279
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
280
|
+
5. Create new Pull Request
|
|
281
|
+
|
|
282
|
+
## License
|
|
283
|
+
|
|
284
|
+
MIT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fino
|
|
4
|
+
module Solid
|
|
5
|
+
class Adapter
|
|
6
|
+
include Fino::Adapter
|
|
7
|
+
|
|
8
|
+
SCOPE_PREFIX = "s"
|
|
9
|
+
VARIANT_PREFIX = "v"
|
|
10
|
+
VALUE_KEY = "v"
|
|
11
|
+
|
|
12
|
+
def read(setting_key)
|
|
13
|
+
setting = Fino::Solid::Setting.find_by(key: setting_key)
|
|
14
|
+
setting&.data || {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def read_multi(setting_keys)
|
|
18
|
+
settings_by_key = Fino::Solid::Setting.where(key: setting_keys).index_by(&:key)
|
|
19
|
+
|
|
20
|
+
setting_keys.map { |key| settings_by_key[key]&.data || {} }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def write(setting_definition, value, overrides, variants)
|
|
24
|
+
serialize_value = ->(raw_value) { setting_definition.type_class.serialize(raw_value) }
|
|
25
|
+
|
|
26
|
+
data = { VALUE_KEY => serialize_value.call(value) }
|
|
27
|
+
|
|
28
|
+
overrides.each do |scope, scope_value|
|
|
29
|
+
data["#{SCOPE_PREFIX}/#{scope}/#{VALUE_KEY}"] = serialize_value.call(scope_value)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
variants.each do |variant|
|
|
33
|
+
next if variant.value == Fino::AbTesting::Variant::CONTROL_VALUE
|
|
34
|
+
|
|
35
|
+
data["#{VARIANT_PREFIX}/#{variant.percentage}/#{VALUE_KEY}"] = serialize_value.call(variant.value)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Fino::Solid::Setting.upsert(
|
|
39
|
+
{ key: setting_definition.key, data: data },
|
|
40
|
+
unique_by: :key
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def read_persisted_setting_keys
|
|
45
|
+
Fino::Solid::Setting.pluck(:key)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def clear(setting_key)
|
|
49
|
+
Fino::Solid::Setting.where(key: setting_key).delete_all > 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fetch_value_from(raw_adapter_data)
|
|
53
|
+
raw_adapter_data.key?(VALUE_KEY) ? raw_adapter_data.delete(VALUE_KEY) : Fino::EMPTINESS
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def fetch_raw_overrides_from(raw_adapter_data)
|
|
57
|
+
raw_adapter_data.each_with_object({}) do |(key, value), memo|
|
|
58
|
+
next unless key.start_with?("#{SCOPE_PREFIX}/")
|
|
59
|
+
|
|
60
|
+
scope = key.split("/", 3)[1]
|
|
61
|
+
memo[scope] = value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def fetch_raw_variants_from(raw_adapter_data)
|
|
66
|
+
raw_adapter_data.each_with_object([]) do |(key, value), memo|
|
|
67
|
+
next unless key.start_with?("#{VARIANT_PREFIX}/")
|
|
68
|
+
|
|
69
|
+
percentage = key.split("/", 3)[1]
|
|
70
|
+
|
|
71
|
+
memo << { percentage: percentage.to_f, value: value }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Fino
|
|
7
|
+
module Solid
|
|
8
|
+
module Generators
|
|
9
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
10
|
+
include ::ActiveRecord::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
def copy_migration
|
|
15
|
+
migration_template "create_fino_settings.rb.tt", File.join(db_migrate_path, "create_fino_settings.rb")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def db_migrate_path
|
|
21
|
+
"db/migrate"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateFinoSettings < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :fino_settings do |t|
|
|
4
|
+
t.string :key, null: false
|
|
5
|
+
t.text :data
|
|
6
|
+
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
add_index :fino_settings, :key, unique: true
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/fino/solid.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
|
|
5
|
+
module Fino
|
|
6
|
+
module Solid
|
|
7
|
+
mattr_accessor :connects_to
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def configure(&block)
|
|
11
|
+
instance_eval(&block)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require "fino/solid/record"
|
|
18
|
+
require "fino/solid/setting"
|
|
19
|
+
require "fino/solid/adapter"
|
|
20
|
+
require "fino/solid/railtie" if defined?(Rails::Railtie)
|
|
21
|
+
|
|
22
|
+
if defined?(Rails) && defined?(Rails::Generators)
|
|
23
|
+
require "rails/generators"
|
|
24
|
+
require_relative "solid/generators/install/install_generator"
|
|
25
|
+
end
|
data/lib/fino/version.rb
ADDED
data/lib/fino-solid.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fino-solid
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.4.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Egor Iskrenkov
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: fino
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 1.4.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 1.4.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activerecord
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: sqlite3
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.4'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.4'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: pg
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: mysql2
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.5'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.5'
|
|
82
|
+
email:
|
|
83
|
+
- egor@iskrenkov.me
|
|
84
|
+
executables: []
|
|
85
|
+
extensions: []
|
|
86
|
+
extra_rdoc_files: []
|
|
87
|
+
files:
|
|
88
|
+
- LICENSE
|
|
89
|
+
- README.md
|
|
90
|
+
- lib/fino-solid.rb
|
|
91
|
+
- lib/fino/solid.rb
|
|
92
|
+
- lib/fino/solid/README.md
|
|
93
|
+
- lib/fino/solid/adapter.rb
|
|
94
|
+
- lib/fino/solid/generators/install/USAGE
|
|
95
|
+
- lib/fino/solid/generators/install/install_generator.rb
|
|
96
|
+
- lib/fino/solid/generators/install/templates/create_fino_settings.rb.tt
|
|
97
|
+
- lib/fino/solid/railtie.rb
|
|
98
|
+
- lib/fino/solid/record.rb
|
|
99
|
+
- lib/fino/solid/setting.rb
|
|
100
|
+
- lib/fino/version.rb
|
|
101
|
+
homepage: https://github.com/eiskrenkov/fino
|
|
102
|
+
licenses:
|
|
103
|
+
- MIT
|
|
104
|
+
metadata:
|
|
105
|
+
source_code_uri: https://github.com/eiskrenkov/fino
|
|
106
|
+
bug_tracker_uri: https://github.com/eiskrenkov/fino/issues
|
|
107
|
+
rubygems_mfa_required: 'true'
|
|
108
|
+
rdoc_options: []
|
|
109
|
+
require_paths:
|
|
110
|
+
- lib
|
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: 3.0.0
|
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
requirements: []
|
|
122
|
+
rubygems_version: 3.6.9
|
|
123
|
+
specification_version: 4
|
|
124
|
+
summary: ActiveRecord adapter for Fino settings engine
|
|
125
|
+
test_files: []
|