fino-rails 1.0.4

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f7a56251fd94a51a2bd2725b8e6f2ab6258f719226ca136e7d7411254319d57a
4
+ data.tar.gz: 33cfed847bdd658ebf88c3743c1193fad57a6981a9c815e177d875474b0c93e9
5
+ SHA512:
6
+ metadata.gz: 25450bbbb1a24050116f6ec49235d345ef78130328baf52d2d335e4205bf6b6f5d4d2362898a8210de4523dd1aca1075c7acf3e16e5ee2020a3c3aef19b6e517
7
+ data.tar.gz: 29feda12bb2bdaa16357952868986a77adebf88767e62751e2e750fbe315c7f403e334c7727d37656116561f1f786eb0d446edea48767ee223c5b3a279d9f824
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # Fino
2
+
3
+ ⚠️ Fino is under active development. API changes are possible ⚠️
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
+ section :openai, label: "OpenAI" do
27
+ setting :model,
28
+ :string,
29
+ default: "gpt-4o",
30
+ description: "OpenAI model"
31
+
32
+ setting :temperature,
33
+ :float,
34
+ default: 0.7,
35
+ description: "Model temperature"
36
+ end
37
+
38
+ section :feature_toggles, label: "Feature Toggles" do
39
+ setting :new_ui, :boolean, default: true
40
+ setting :beta_functionality, :boolean, default: false
41
+ end
42
+
43
+ section :my_micro_service, label: "My Micro Service" do
44
+ setting :http_read_timeout, :integer, default: 200 # in ms
45
+ setting :http_open_timeout, :integer, default: 100 # in ms
46
+ end
47
+ end
48
+ end
49
+ ```
50
+
51
+ ### Work with settings
52
+
53
+ ```ruby
54
+ Fino.value(:model, at: :openai) #=> "gpt-4o"
55
+ Fino.value(:temperature, at: :openai) #=> 0.7
56
+
57
+ Fino.values(:model, :temperature, at: :openai) #=> ["gpt-4", 0.7]
58
+
59
+ Fino.set("gpt-5", :model, at: :openai)
60
+ Fino.value(:model, at: :openai) #=> "gpt-5"
61
+ ```
62
+
63
+ ### Manage settings via UI
64
+
65
+ ```ruby
66
+ gem "fino-rails"
67
+ ```
68
+
69
+ Mount Fino Rails engine in your `config/routes.rb`:
70
+
71
+ ```ruby
72
+ Rails.application.routes.draw do
73
+ mount Fino::Rails::Engine, at: "/fino"
74
+ end
75
+ ```
76
+
77
+ <img width="1229" height="641" alt="Screenshot 2025-09-04 at 16 01 51" src="https://github.com/user-attachments/assets/646df84c-c25b-4890-9637-c481e18c9bd4" />
78
+
79
+ ## TODO
80
+
81
+ - Preloading settings to be able to fetch all of them in one adapter call
82
+ - Request scoped memoization when integrating with Rails
83
+ - Nicer UI
84
+ - Basic validations (presence, range, numericality)
85
+ - Enum setting type
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fino::Rails::ApplicationController < ActionController::Base
4
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fino::Rails::SettingsController < Fino::Rails::ApplicationController
4
+ def index
5
+ @settings = Fino.library.all
6
+ end
7
+
8
+ def edit
9
+ setting_name, at = parse_setting_path(params[:key])
10
+
11
+ @setting = Fino.setting(setting_name, at: at)
12
+ end
13
+
14
+ def update
15
+ setting_name, at = parse_setting_path(params[:key])
16
+
17
+ Fino.set(params[:value], setting_name, at: at)
18
+
19
+ redirect_to root_path, notice: "Setting updated successfully"
20
+ rescue Fino::Registry::UnknownSetting
21
+ redirect_to root_path, alert: "Setting not found"
22
+ end
23
+
24
+ private
25
+
26
+ def parse_setting_path(key)
27
+ key.split("/").map(&:to_sym).reverse
28
+ end
29
+ end
@@ -0,0 +1,179 @@
1
+ <% content_for(:title, "Edit #{@setting.name}") %>
2
+ <% content_for(:head) do %>
3
+ <style>
4
+ .form-group {
5
+ margin-bottom: 1.5rem;
6
+ }
7
+
8
+ .form-label {
9
+ display: block;
10
+ font-size: 0.875rem;
11
+ font-weight: 500;
12
+ color: #0f172a;
13
+ margin-bottom: 0.5rem;
14
+ }
15
+
16
+ .form-input {
17
+ display: block;
18
+ width: 100%;
19
+ padding: 0.5rem 0.75rem;
20
+ font-size: 0.875rem;
21
+ line-height: 1.5;
22
+ color: #0f172a;
23
+ background-color: #ffffff;
24
+ border: 1px solid #e2e8f0;
25
+ border-radius: 6px;
26
+ transition: border-color 0.15s ease-in-out;
27
+ }
28
+
29
+ .form-input:focus {
30
+ outline: none;
31
+ border-color: #0f172a;
32
+ box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.1);
33
+ }
34
+
35
+ .form-select {
36
+ display: block;
37
+ width: 100%;
38
+ padding: 0.5rem 0.75rem;
39
+ font-size: 0.875rem;
40
+ line-height: 1.5;
41
+ color: #0f172a;
42
+ background-color: #ffffff;
43
+ border: 1px solid #e2e8f0;
44
+ border-radius: 6px;
45
+ transition: border-color 0.15s ease-in-out;
46
+ }
47
+
48
+ .form-select:focus {
49
+ outline: none;
50
+ border-color: #0f172a;
51
+ box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.1);
52
+ }
53
+
54
+ .form-checkbox {
55
+ width: 1rem;
56
+ height: 1rem;
57
+ border: 1px solid #e2e8f0;
58
+ border-radius: 4px;
59
+ margin-right: 0.5rem;
60
+ }
61
+
62
+ .form-help {
63
+ font-size: 0.75rem;
64
+ color: #64748b;
65
+ margin-top: 0.25rem;
66
+ }
67
+
68
+ .button-group {
69
+ display: flex;
70
+ gap: 0.75rem;
71
+ margin-top: 2rem;
72
+ }
73
+
74
+ .breadcrumb {
75
+ margin-bottom: 1.5rem;
76
+ font-size: 0.875rem;
77
+ color: #64748b;
78
+ }
79
+
80
+ .breadcrumb a {
81
+ color: #0f172a;
82
+ text-decoration: none;
83
+ }
84
+
85
+ .breadcrumb a:hover {
86
+ text-decoration: underline;
87
+ }
88
+
89
+ .setting-info {
90
+ background-color: #f8fafc;
91
+ border: 1px solid #e2e8f0;
92
+ border-radius: 6px;
93
+ padding: 1rem;
94
+ margin-bottom: 1.5rem;
95
+ }
96
+
97
+ .setting-info-title {
98
+ font-weight: 600;
99
+ margin-bottom: 0.5rem;
100
+ color: #0f172a;
101
+ }
102
+
103
+ .setting-info-detail {
104
+ font-size: 0.875rem;
105
+ color: #64748b;
106
+ margin-bottom: 0.25rem;
107
+ }
108
+ </style>
109
+ <% end %>
110
+
111
+ <div class="breadcrumb">
112
+ <%= link_to "Settings", root_path %> / Edit <%= @setting.name %>
113
+ </div>
114
+
115
+ <div class="header">
116
+ <h1>Edit Setting</h1>
117
+ <p>Modify the configuration value for this setting</p>
118
+ </div>
119
+
120
+ <div class="card">
121
+ <div class="card-header">
122
+ <h2 class="card-title"><%= @setting.name %></h2>
123
+ </div>
124
+ <div class="card-content">
125
+ <div class="setting-info">
126
+ <div class="setting-info-title">Setting Information</div>
127
+ <div class="setting-info-detail"><strong>Type:</strong> <%= @setting.class.name.demodulize %></div>
128
+ <% if @setting.section_name %>
129
+ <div class="setting-info-detail"><strong>Section:</strong> <%= @setting.section_name %></div>
130
+ <% end %>
131
+ <div class="setting-info-detail"><strong>Default:</strong> <code><%= @setting.definition.default.inspect %></code></div>
132
+ <% if @setting.definition.options[:description].present? %>
133
+ <div class="setting-info-detail"><strong>Description:</strong> <%= @setting.definition.options[:description] %></div>
134
+ <% end %>
135
+ </div>
136
+
137
+ <%= form_with url: update_setting_path(@setting.key), method: :put, local: true do |f| %>
138
+ <div class="form-group">
139
+ <%= f.label :value, class: "form-label" do %>
140
+ Current Value
141
+ <% if @setting.definition.options[:description].present? %>
142
+ <div class="form-help"><%= @setting.definition.options[:description] %></div>
143
+ <% end %>
144
+ <% end %>
145
+
146
+ <% case @setting.class.name.demodulize.downcase %>
147
+ <% when 'boolean' %>
148
+ <%= f.check_box :value,
149
+ { class: "form-checkbox", checked: @setting.value },
150
+ '1',
151
+ '0' %>
152
+ <% when 'integer' %>
153
+ <%= f.number_field :value,
154
+ value: @setting.value,
155
+ class: "form-input",
156
+ step: 1 %>
157
+ <% when 'float' %>
158
+ <%= f.number_field :value,
159
+ value: @setting.value,
160
+ class: "form-input",
161
+ step: 0.1 %>
162
+ <% else %>
163
+ <%= f.text_field :value,
164
+ value: @setting.value,
165
+ class: "form-input" %>
166
+ <% end %>
167
+
168
+ <div class="form-help">
169
+ Default value: <%= @setting.definition.default.inspect %>
170
+ </div>
171
+ </div>
172
+
173
+ <div class="button-group">
174
+ <%= f.submit "Update Setting", class: "btn btn-primary" %>
175
+ <%= link_to "Cancel", root_path, class: "btn btn-secondary" %>
176
+ </div>
177
+ <% end %>
178
+ </div>
179
+ </div>
@@ -0,0 +1,155 @@
1
+ <% content_for(:title, 'Application Settings') %>
2
+ <% content_for(:head) do %>
3
+ <style>
4
+ .settings-grid {
5
+ display: grid;
6
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
7
+ gap: 1.5rem;
8
+ }
9
+
10
+ .setting-item {
11
+ display: block;
12
+ padding: 1rem;
13
+ border-bottom: 1px solid #e2e8f0;
14
+ text-decoration: none;
15
+ color: inherit;
16
+ transition: background-color 0.15s ease-in-out;
17
+ }
18
+
19
+ .setting-item:hover {
20
+ background-color: #f8fafc;
21
+ }
22
+
23
+ .setting-item:last-child {
24
+ border-bottom: none;
25
+ }
26
+
27
+ .setting-name {
28
+ font-weight: 600;
29
+ color: #0f172a;
30
+ font-size: 0.875rem;
31
+ margin-bottom: 0.25rem;
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 0.5rem;
35
+ }
36
+
37
+ .setting-type {
38
+ display: inline-block;
39
+ padding: 0.125rem 0.5rem;
40
+ background-color: #f1f5f9;
41
+ color: #475569;
42
+ font-size: 0.75rem;
43
+ font-weight: 500;
44
+ border-radius: 4px;
45
+ text-transform: uppercase;
46
+ letter-spacing: 0.025em;
47
+ }
48
+
49
+ .setting-value {
50
+ color: #0f172a;
51
+ font-size: 0.875rem;
52
+ margin-top: 0.5rem;
53
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
54
+ background-color: #f8fafc;
55
+ padding: 0.5rem;
56
+ border-radius: 4px;
57
+ border: 1px solid #e2e8f0;
58
+ font-weight: 500;
59
+ }
60
+
61
+ .setting-default {
62
+ color: #64748b;
63
+ font-size: 0.75rem;
64
+ margin-top: 0.25rem;
65
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
66
+ }
67
+
68
+ .setting-description {
69
+ color: #64748b;
70
+ font-size: 0.875rem;
71
+ margin-top: 0.25rem;
72
+ font-style: italic;
73
+ }
74
+
75
+ .empty-state {
76
+ text-align: center;
77
+ padding: 3rem 1rem;
78
+ color: #64748b;
79
+ }
80
+
81
+ .empty-state svg {
82
+ width: 48px;
83
+ height: 48px;
84
+ margin: 0 auto 1rem;
85
+ color: #cbd5e1;
86
+ }
87
+
88
+ @media (max-width: 640px) {
89
+ .settings-grid {
90
+ grid-template-columns: 1fr;
91
+ }
92
+ }
93
+ </style>
94
+ <% end %>
95
+
96
+ <div class="header">
97
+ <h1>Application Settings</h1>
98
+ <p>Global configuration settings for your application</p>
99
+ </div>
100
+
101
+ <%
102
+ # Group settings by section
103
+ grouped_settings = @settings.group_by { |setting| setting.section_name || 'Global' }
104
+
105
+ if grouped_settings.empty?
106
+ %>
107
+ <div class="empty-state">
108
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
109
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
110
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
111
+ </svg>
112
+ <h3>No settings configured</h3>
113
+ <p>Configure settings in your application to see them here.</p>
114
+ </div>
115
+ <% else %>
116
+ <div class="settings-grid">
117
+ <%
118
+ # Show Global settings first, then other sections
119
+ sections_order = grouped_settings.keys.sort_by { |section| section == 'Global' ? 0 : 1 }
120
+ sections_order.each do |section_name|
121
+ settings = grouped_settings[section_name]
122
+ is_global = section_name == 'Global'
123
+ %>
124
+ <div class="card">
125
+ <div class="card-header">
126
+ <h2 class="card-title">
127
+ <%= is_global ? 'Global Settings' : section_name.to_s.humanize %>
128
+ </h2>
129
+ </div>
130
+ <div class="card-content" style="padding: 0;">
131
+ <% settings.each do |setting| %>
132
+ <%= link_to edit_setting_path(setting.key), class: "setting-item" do %>
133
+ <div class="setting-name">
134
+ <%= setting.name %>
135
+ <span class="setting-type"><%= setting.class.name.demodulize %></span>
136
+ </div>
137
+
138
+ <% if setting.definition.options[:description].present? %>
139
+ <div class="setting-description"><%= setting.definition.options[:description] %></div>
140
+ <% end %>
141
+
142
+ <div class="setting-value">
143
+ <%= setting.value.inspect %>
144
+ </div>
145
+
146
+ <div class="setting-default">
147
+ Default: <%= setting.default.inspect %>
148
+ </div>
149
+ <% end %>
150
+ <% end %>
151
+ </div>
152
+ </div>
153
+ <% end %>
154
+ </div>
155
+ <% end %>
@@ -0,0 +1,138 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Fino - <%= yield(:title) || 'Settings' %></title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
16
+ line-height: 1.6;
17
+ color: #0f172a;
18
+ background-color: #ffffff;
19
+ min-height: 100vh;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1200px;
24
+ margin: 0 auto;
25
+ padding: 2rem 1rem;
26
+ }
27
+
28
+ .header {
29
+ margin-bottom: 2rem;
30
+ }
31
+
32
+ .header h1 {
33
+ font-size: 2rem;
34
+ font-weight: 600;
35
+ color: #0f172a;
36
+ margin-bottom: 0.5rem;
37
+ }
38
+
39
+ .header p {
40
+ color: #64748b;
41
+ font-size: 1rem;
42
+ }
43
+
44
+ .card {
45
+ background: #ffffff;
46
+ border-radius: 8px;
47
+ border: 1px solid #e2e8f0;
48
+ }
49
+
50
+ .card-header {
51
+ padding: 1.5rem;
52
+ border-bottom: 1px solid #e2e8f0;
53
+ }
54
+
55
+ .card-title {
56
+ font-size: 1.125rem;
57
+ font-weight: 600;
58
+ color: #0f172a;
59
+ }
60
+
61
+ .card-content {
62
+ padding: 1.5rem;
63
+ }
64
+
65
+ .btn {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ border-radius: 6px;
70
+ font-size: 0.875rem;
71
+ font-weight: 500;
72
+ transition: colors 0.15s ease-in-out;
73
+ cursor: pointer;
74
+ text-decoration: none;
75
+ border: 1px solid transparent;
76
+ }
77
+
78
+ .btn-primary {
79
+ background-color: #0f172a;
80
+ color: #ffffff;
81
+ padding: 0.5rem 1rem;
82
+ }
83
+
84
+ .btn-primary:hover {
85
+ background-color: #1e293b;
86
+ }
87
+
88
+ .btn-secondary {
89
+ background-color: transparent;
90
+ color: #0f172a;
91
+ border-color: #e2e8f0;
92
+ padding: 0.5rem 1rem;
93
+ }
94
+
95
+ .btn-secondary:hover {
96
+ background-color: #f8fafc;
97
+ }
98
+
99
+ .btn-ghost {
100
+ background-color: transparent;
101
+ color: #0f172a;
102
+ padding: 0.5rem;
103
+ }
104
+
105
+ .btn-ghost:hover {
106
+ background-color: #f8fafc;
107
+ }
108
+
109
+ .text-muted {
110
+ color: #64748b;
111
+ }
112
+
113
+ .text-sm {
114
+ font-size: 0.875rem;
115
+ }
116
+
117
+ .text-xs {
118
+ font-size: 0.75rem;
119
+ }
120
+
121
+ @media (max-width: 640px) {
122
+ .container {
123
+ padding: 1rem 0.5rem;
124
+ }
125
+
126
+ .header h1 {
127
+ font-size: 1.5rem;
128
+ }
129
+ }
130
+ </style>
131
+ <%= yield(:head) %>
132
+ </head>
133
+ <body>
134
+ <div class="container">
135
+ <%= yield %>
136
+ </div>
137
+ </body>
138
+ </html>
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ Fino::Rails::Engine.routes.draw do
4
+ root to: "settings#index"
5
+
6
+ get "settings/*key", to: "settings#edit", as: :edit_setting
7
+ put "settings/*key", to: "settings#update", as: :update_setting
8
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fino-rails"
4
+
5
+ class Fino::Rails::Engine < Rails::Engine
6
+ isolate_namespace Fino::Rails
7
+
8
+ #
9
+ # Engine
10
+ #
11
+
12
+ paths["app"] << root.join("lib", "fino", "rails", "app")
13
+ paths["config/initializers"] << root.join("lib", "fino", "rails", "config", "initializers")
14
+
15
+ initializer "fino.rails.engine.views" do |_app|
16
+ ActiveSupport.on_load :action_controller do
17
+ prepend_view_path Fino::Rails::Engine.root.join("lib", "fino", "rails", "app", "views")
18
+ end
19
+ end
20
+
21
+ initializer "fino.rails.engine.routes", before: :add_routing_paths do |app|
22
+ custom_routes = root.join("lib", "fino", "rails", "config", "routes.rb")
23
+ app.routes_reloader.paths << custom_routes.to_s
24
+ end
25
+
26
+ #
27
+ # Configuration
28
+ #
29
+
30
+ config.before_configuration do
31
+ config.fino = ActiveSupport::OrderedOptions.new.update(
32
+ instrument: Rails.env.development?,
33
+ log: Rails.env.development?
34
+ )
35
+ end
36
+
37
+ #
38
+ # Initializers
39
+ #
40
+
41
+ initializer "fino.log", after: :load_config_initializers do |app|
42
+ config = app.config.fino
43
+
44
+ require "fino/rails/instrumentation/log_subscriber" if config.instrument && config.log
45
+ end
46
+
47
+ initializer "fino.pipeline" do |app|
48
+ config = app.config.fino
49
+
50
+ Fino.configure do
51
+ pipeline do
52
+ if config.instrument
53
+ wrap do |pipe|
54
+ Fino::Rails::Instrumentation::Pipe.new(pipe)
55
+ end
56
+ end
57
+
58
+ use Fino::Rails::RequestScopedCache::Pipe if defined?(Rails::Server)
59
+ end
60
+ end
61
+ end
62
+
63
+ initializer "fino.request_scoped_caching.middleware" do |app|
64
+ app.middleware.use Fino::Rails::RequestScopedCache::Middleware if defined?(Rails::Server)
65
+ end
66
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/log_subscriber"
4
+
5
+ class Fino::Rails::Instrumentation::LogSubscriber < ActiveSupport::LogSubscriber
6
+ def pipe(event) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
7
+ return unless logger.debug?
8
+
9
+ payload = event.payload
10
+ operation = payload[:operation]
11
+ pipe_name = payload[:pipe_name]
12
+ setting_name = payload[:setting_name]
13
+ duration = event.duration
14
+
15
+ name = color("#{pipe_name} #{operation.to_s.upcase}", :cyan, bold: true)
16
+ duration_text = color("(#{duration.round(1)}ms)", nil, bold: true)
17
+
18
+ message = " #{name} #{duration_text}"
19
+ message += " setting=#{setting_name}" if setting_name && !setting_name.to_s.empty?
20
+
21
+ debug(message)
22
+ end
23
+ end
24
+
25
+ Fino::Rails::Instrumentation::LogSubscriber.attach_to :fino
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+
5
+ class Fino::Rails::Instrumentation::Pipe
6
+ INSTRUMENTATION_NAMESPACE = "pipe.fino"
7
+
8
+ include Fino::Pipe
9
+
10
+ def initialize(pipe)
11
+ @pipe = pipe
12
+ setup_instrumented_methods
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :pipe
18
+
19
+ def setup_instrumented_methods # rubocop:disable Metrics/MethodLength
20
+ Fino::Pipe.public_instance_methods(false).each do |method_name|
21
+ self.class.define_method(method_name) do |*args, **kwargs, &block|
22
+ payload = {
23
+ operation: method_name,
24
+ pipe_name: pipe.class.name,
25
+ setting_name: extract_setting_name(method_name, args)
26
+ }
27
+
28
+ ActiveSupport::Notifications.instrument(INSTRUMENTATION_NAMESPACE, payload) do |instrumentation_payload|
29
+ pipe.public_send(method_name, *args, **kwargs, &block).tap do |result|
30
+ instrumentation_payload[:result] = result
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def extract_setting_name(method_name, args) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
38
+ case method_name
39
+ in /.*_multi$/
40
+ args.first&.map { |sd| sd.respond_to?(:key) ? sd.key : sd.to_s }&.join(", ") || args.first&.to_s
41
+ else
42
+ args.first.respond_to?(:key) ? args.first.key : args.first&.to_s
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fino::Rails::RequestScopedCache::Middleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ Fino::Rails::RequestScopedCache::Pipe.with_temporary_cache do
10
+ @app.call(env)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fino::Rails::RequestScopedCache::Pipe
4
+ include Fino::Pipe
5
+
6
+ def self.with_temporary_cache
7
+ Thread.current[:fino_request_scoped_cache] = Fino::Cache::Memory.new(expires_in: nil)
8
+ yield
9
+ ensure
10
+ Thread.current[:fino_request_scoped_cache] = nil
11
+ end
12
+
13
+ def read(setting_definition)
14
+ cache.fetch(setting_definition.key) do
15
+ pipe.read(setting_definition)
16
+ end
17
+ end
18
+
19
+ def read_multi(setting_definitions)
20
+ cache.fetch_multi(setting_definitions.map(&:key)) do |missing_keys|
21
+ uncached_setting_definitions = setting_definitions.filter { |sd| missing_keys.include?(sd.key) }
22
+
23
+ missing_keys.zip(pipe.read_multi(uncached_setting_definitions))
24
+ end
25
+ end
26
+
27
+ def write(setting_definition, value)
28
+ pipe.write(setting_definition, value)
29
+
30
+ cache.write(
31
+ setting_definition.key,
32
+ setting_definition.type_class.build(setting_definition, value)
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def cache
39
+ Thread.current[:fino_request_scoped_cache] ||
40
+ raise(ArgumentError, "No request store available. Make sure to use Middleware")
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fino
4
+ VERSION = "1.0.4"
5
+ REQUIRED_RUBY_VERSION = ">= 3.0.0"
6
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fino-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.4
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.0.4
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 1.0.4
26
+ - !ruby/object:Gem::Dependency
27
+ name: rails
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '8.0'
40
+ email:
41
+ - egor@iskrenkov.me
42
+ executables: []
43
+ extensions: []
44
+ extra_rdoc_files: []
45
+ files:
46
+ - README.md
47
+ - lib/fino/rails/app/controllers/fino/rails/application_controller.rb
48
+ - lib/fino/rails/app/controllers/fino/rails/settings_controller.rb
49
+ - lib/fino/rails/app/views/fino/rails/settings/edit.html.erb
50
+ - lib/fino/rails/app/views/fino/rails/settings/index.html.erb
51
+ - lib/fino/rails/app/views/layouts/fino/rails/application.html.erb
52
+ - lib/fino/rails/config/routes.rb
53
+ - lib/fino/rails/engine.rb
54
+ - lib/fino/rails/instrumentation/log_subscriber.rb
55
+ - lib/fino/rails/instrumentation/pipe.rb
56
+ - lib/fino/rails/request_scoped_cache/middleware.rb
57
+ - lib/fino/rails/request_scoped_cache/pipe.rb
58
+ - lib/fino/version.rb
59
+ homepage: https://github.com/eiskrenkov/fino
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ source_code_uri: https://github.com/eiskrenkov/fino
64
+ bug_tracker_uri: https://github.com/eiskrenkov/fino/issues
65
+ rubygems_mfa_required: 'true'
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 3.0.0
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.6.9
81
+ specification_version: 4
82
+ summary: Rails integration and UI for Fino settings engine
83
+ test_files: []