flipside 0.4.0 → 0.5.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/CHANGELOG.md +9 -0
- data/README.md +41 -19
- data/app/models/flipside/flippable.rb +74 -8
- data/lib/flipside/config/entities.rb +1 -1
- data/lib/flipside/config/registered_entity.rb +24 -26
- data/lib/flipside/version.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4ecf5b08df6bcc2ac0bd7a9f2e4396d361fefb53b497323725ab543ed3d60a8
|
|
4
|
+
data.tar.gz: a64de96569181f04e4e7f50f9c7122299ee79581115c0abfbdf24aceef706f03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b8b659ad364668c26e19e2666f78e5c354034c61b68b1c3b54f63a07f310749cc6295fe3cd77b36c61bfefa41f3266cb9c047ce459d688fed5951f4ee00d8ef
|
|
7
|
+
data.tar.gz: d61860274364b069a96b7547bc4f0c66b0de08b8eae4205f520084afa5b9fd1bb2e8b2d1e0022b05c47a8a8b47ef0421dafef87cb126152b3d7d70c39bb03d80
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## [0.5.0] - 2026-10-08
|
|
2
|
+
- Add `flipside_search_by`, `flipside_display_as` and `flipside_identified_by` to `Flipside::Flippable`. Each one registers the class as an entity, so `flipside_entity` is only needed for the defaults
|
|
3
|
+
- A block given to `flipside_search_by` runs in the context of the class, so it can call `where` directly
|
|
4
|
+
- An STI subclass inherits its parent's entity settings, and is registered as an entity of its own when it calls one of the macros itself
|
|
5
|
+
- Test against Ruby 3.4 and 4.0
|
|
6
|
+
|
|
7
|
+
### Deprecated
|
|
8
|
+
- The `search_by`, `display_as` and `identified_by` options of `flipside_entity`, in favour of `flipside_search_by`, `flipside_display_as` and `flipside_identified_by`. They will be removed in 1.0.
|
|
9
|
+
|
|
1
10
|
## [0.4.0] - 2026-09-25
|
|
2
11
|
- Show entities whose record has been deleted instead of raising in the UI
|
|
3
12
|
- Add `Flipside::Flippable` with `flipside_entity` and `flipside_role`, so models can register themselves (listed in `Flipside.flippables`); entities of a destroyed record are removed
|
data/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Flipside.add_entity(name: "MyFeature", entity: user)
|
|
|
97
97
|
Flipside.enabled? "MyFeature", user # => true
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
Entities reference their record polymorphically, so there is no foreign key to clean them up when the record is deleted. A model registered
|
|
100
|
+
Entities reference their record polymorphically, so there is no foreign key to clean them up when the record is deleted. A model registered as an entity (see [Registering from the model](#registering-from-the-model)) removes its entities when a record is destroyed.
|
|
101
101
|
|
|
102
102
|
Entities left behind by records deleted without callbacks (or before the concern was added) can be removed with:
|
|
103
103
|
```ruby
|
|
@@ -179,12 +179,13 @@ Flipside.default_object = -> { Current.user }
|
|
|
179
179
|
|
|
180
180
|
### Registering from the model
|
|
181
181
|
|
|
182
|
-
Entities and roles are searched for in the UI, so Flipside needs to know which classes can be used. A model registers itself by including `Flipside::Flippable` and calling `flipside_entity` and/or `flipside_role`:
|
|
182
|
+
Entities and roles are searched for in the UI, so Flipside needs to know which classes can be used. A model registers itself by including `Flipside::Flippable` and calling the entity macros (`flipside_entity`, `flipside_search_by`, `flipside_display_as`, `flipside_identified_by`) and/or `flipside_role`:
|
|
183
183
|
```ruby
|
|
184
184
|
class User < ApplicationRecord
|
|
185
185
|
include Flipside::Flippable
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
flipside_search_by :name
|
|
188
|
+
flipside_display_as :name
|
|
188
189
|
flipside_role :admin?
|
|
189
190
|
flipside_role :awesome?, display_as: "Awesome users"
|
|
190
191
|
end
|
|
@@ -204,36 +205,48 @@ Entities can be added to a feature by searching for records.
|
|
|
204
205
|
|
|
205
206
|

|
|
206
207
|
|
|
207
|
-
|
|
208
|
+
Each class that may be used as a feature enabler is registered as an entity by calling `flipside_entity`, `flipside_search_by`, `flipside_display_as` or `flipside_identified_by` (any combination of them, in any order). `flipside_entity` is only needed to register with the defaults. A registered entity class also removes the entities of a record when it is destroyed. Note that this includes soft deletion that runs destroy callbacks (e.g. paranoia), so a restored record comes back without its features. Soft deletion that skips them (e.g. discard) keeps the entities.
|
|
208
209
|
```ruby
|
|
209
210
|
class User < ApplicationRecord
|
|
210
211
|
include Flipside::Flippable
|
|
211
212
|
|
|
212
|
-
flipside_entity
|
|
213
|
+
flipside_entity
|
|
213
214
|
end
|
|
214
215
|
```
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
`flipside_identified_by` sets the column Flipside uses to identify records. This defaults to `:id` and typically does not need to be changed.
|
|
218
|
+
Currently composite keys are not supported.
|
|
219
|
+
```ruby
|
|
220
|
+
flipside_identified_by :uuid
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`flipside_search_by` dictates how records are found from searching in the ui. It takes a block, which is called with the search string in the context of the class and is expected to return an object responding to `to_a` (e.g. an AR collection).
|
|
219
224
|
This gives us the flexibility to decide how to search for entities. For example, to search for users with matching first name or last name or an email
|
|
220
225
|
starting with _query_, something like this could be used.
|
|
221
226
|
```ruby
|
|
222
|
-
|
|
223
|
-
where(
|
|
224
|
-
|
|
227
|
+
flipside_search_by do |str|
|
|
228
|
+
where(
|
|
229
|
+
"lower(first_name) = :name OR lower(last_name) = :name OR email LIKE :str",
|
|
230
|
+
name: str.downcase,
|
|
231
|
+
str: "#{str}%"
|
|
232
|
+
)
|
|
233
|
+
end
|
|
225
234
|
```
|
|
226
235
|
|
|
227
|
-
|
|
228
|
-
|
|
236
|
+
Instead of a block, it may be given the name of a column, e.g. `:name`. Then records with an exact match on that column are returned, i.e. `User.where(name: query)`:
|
|
237
|
+
```ruby
|
|
238
|
+
flipside_search_by :name
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Without `flipside_search_by`, records are found by an exact match on `identified_by`.
|
|
229
242
|
|
|
230
|
-
|
|
243
|
+
`flipside_display_as` configures how these entities show up in the combobox. When given the name of an instance method, that method is called on the entity.
|
|
231
244
|
For example, given the following setup. Users will be displayed with first name and last name:
|
|
232
245
|
```ruby
|
|
233
246
|
class User < ApplicationRecord
|
|
234
247
|
include Flipside::Flippable
|
|
235
248
|
|
|
236
|
-
|
|
249
|
+
flipside_display_as :name
|
|
237
250
|
|
|
238
251
|
def name
|
|
239
252
|
[first_name, last_name].compact.map(&:capitalize).join(" ")
|
|
@@ -241,11 +254,19 @@ class User < ApplicationRecord
|
|
|
241
254
|
end
|
|
242
255
|
```
|
|
243
256
|
|
|
244
|
-
When a
|
|
257
|
+
When given a block, it is called with the entity and is expected to return a string used for displaying the entity. The config above could then instead be done using:
|
|
245
258
|
```ruby
|
|
246
|
-
|
|
259
|
+
flipside_display_as do |user|
|
|
260
|
+
[user.first_name, user.last_name].compact.map(&:capitalize).join(" ")
|
|
261
|
+
end
|
|
247
262
|
```
|
|
248
263
|
|
|
264
|
+
Without `flipside_display_as`, entities are displayed by their `identified_by` value.
|
|
265
|
+
|
|
266
|
+
An STI subclass inherits these settings. When a subclass calls one of the macros itself, it is registered as an entity of its own, so it must also be listed in `Flipside.flippables` and shows up separately in the UI.
|
|
267
|
+
|
|
268
|
+
The `search_by`, `display_as` and `identified_by` keyword arguments of `flipside_entity` from Flipside 0.4.0 are deprecated in favour of these macros, and will be removed in 1.0.
|
|
269
|
+
|
|
249
270
|
### Roles
|
|
250
271
|
|
|
251
272
|
Features can be enabled for certain roles, by searching for roles (by method name).
|
|
@@ -271,7 +292,7 @@ Flipside.flippables = %w[User OtherGem::Account]
|
|
|
271
292
|
|
|
272
293
|
Rails.application.config.to_prepare do
|
|
273
294
|
OtherGem::Account.include(Flipside::Flippable)
|
|
274
|
-
OtherGem::Account.
|
|
295
|
+
OtherGem::Account.flipside_display_as(:name)
|
|
275
296
|
end
|
|
276
297
|
```
|
|
277
298
|
|
|
@@ -290,7 +311,8 @@ Flipside.flippables = %w[User]
|
|
|
290
311
|
class User < ApplicationRecord
|
|
291
312
|
include Flipside::Flippable
|
|
292
313
|
|
|
293
|
-
|
|
314
|
+
flipside_search_by :name
|
|
315
|
+
flipside_display_as :name
|
|
294
316
|
flipside_role :admin?
|
|
295
317
|
end
|
|
296
318
|
```
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "active_support/concern"
|
|
4
|
+
require "active_support/core_ext/class/attribute"
|
|
4
5
|
require "models/flipside/entity"
|
|
5
6
|
|
|
6
7
|
module Flipside
|
|
@@ -11,18 +12,42 @@ module Flipside
|
|
|
11
12
|
module Flippable
|
|
12
13
|
extend ActiveSupport::Concern
|
|
13
14
|
|
|
15
|
+
included do
|
|
16
|
+
# The class's Flipside::RegisteredEntity. Being a class attribute, it is
|
|
17
|
+
# inherited by STI subclasses and starts out empty in a reloaded class.
|
|
18
|
+
class_attribute :_flipside_registration, instance_accessor: false, instance_predicate: false
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
class_methods do
|
|
15
22
|
# Registers the class as an entity and removes its Flipside::Entity rows
|
|
16
|
-
# when a record is destroyed.
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
# when a record is destroyed. Calling flipside_search_by,
|
|
24
|
+
# flipside_display_as or flipside_identified_by does the same, so this is
|
|
25
|
+
# only needed to register with the defaults.
|
|
26
|
+
def flipside_entity(search_by: nil, display_as: nil, identified_by: nil)
|
|
27
|
+
flipside_registration
|
|
28
|
+
deprecated_entity_options(search_by:, display_as:, identified_by:)
|
|
29
|
+
end
|
|
19
30
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
# Sets how entities are found from a search in the UI, either with a block
|
|
32
|
+
# or the name of a column. The block is run in the context of the class,
|
|
33
|
+
# given the search string, and returns the matching records. A column
|
|
34
|
+
# matches the search string exactly. Registers the class as an entity, as
|
|
35
|
+
# flipside_entity does.
|
|
36
|
+
def flipside_search_by(column = nil, &block)
|
|
37
|
+
flipside_registration.search_by = name_or_block(column, block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Sets how entities are displayed in the UI, either with a block given the
|
|
41
|
+
# record or the name of an instance method. Either returns a string.
|
|
42
|
+
# Registers the class as an entity, as flipside_entity does.
|
|
43
|
+
def flipside_display_as(method_name = nil, &block)
|
|
44
|
+
flipside_registration.display_as = name_or_block(method_name, block)
|
|
45
|
+
end
|
|
24
46
|
|
|
25
|
-
|
|
47
|
+
# Sets the column that identifies an entity, :id by default. Registers the
|
|
48
|
+
# class as an entity, as flipside_entity does.
|
|
49
|
+
def flipside_identified_by(column)
|
|
50
|
+
flipside_registration.identified_by = column.to_sym
|
|
26
51
|
end
|
|
27
52
|
|
|
28
53
|
def flipside_role(method_name, display_as: nil)
|
|
@@ -38,6 +63,47 @@ module Flipside
|
|
|
38
63
|
|
|
39
64
|
Flipside.register_flippable(name)
|
|
40
65
|
end
|
|
66
|
+
|
|
67
|
+
# The registration this class owns, created on first use. An STI subclass
|
|
68
|
+
# starts from a copy of the settings it inherits.
|
|
69
|
+
def flipside_registration
|
|
70
|
+
return _flipside_registration if _flipside_registration&.class_name == name
|
|
71
|
+
|
|
72
|
+
register_flippable!
|
|
73
|
+
|
|
74
|
+
has_many :flipside_entities,
|
|
75
|
+
class_name: "Flipside::Entity",
|
|
76
|
+
as: :flippable,
|
|
77
|
+
dependent: :delete_all
|
|
78
|
+
|
|
79
|
+
self._flipside_registration = Flipside.add_registered_entity(
|
|
80
|
+
class_name: name,
|
|
81
|
+
**(_flipside_registration&.options || {})
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def name_or_block(name, block)
|
|
86
|
+
return name.to_sym if name && !block
|
|
87
|
+
return block if block && !name
|
|
88
|
+
|
|
89
|
+
raise ArgumentError, "pass either a name or a block"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def deprecated_entity_options(search_by:, display_as:, identified_by:)
|
|
93
|
+
return if search_by.nil? && display_as.nil? && identified_by.nil?
|
|
94
|
+
|
|
95
|
+
Flipside.deprecator.warn(
|
|
96
|
+
"The search_by, display_as and identified_by options of flipside_entity are " \
|
|
97
|
+
"deprecated and will be removed in Flipside #{Flipside.deprecator.deprecation_horizon}. " \
|
|
98
|
+
"Use flipside_search_by, flipside_display_as and flipside_identified_by in " \
|
|
99
|
+
"#{name} instead."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
registration = flipside_registration
|
|
103
|
+
registration.search_by = search_by if search_by
|
|
104
|
+
registration.display_as = display_as if display_as
|
|
105
|
+
registration.identified_by = identified_by if identified_by
|
|
106
|
+
end
|
|
41
107
|
end
|
|
42
108
|
end
|
|
43
109
|
end
|
|
@@ -12,7 +12,7 @@ module Flipside
|
|
|
12
12
|
add_registered_entity(class_name:, search_by:, display_as:, identified_by:)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def add_registered_entity(class_name:, search_by
|
|
15
|
+
def add_registered_entity(class_name:, search_by: nil, display_as: nil, identified_by: :id) # :nodoc:
|
|
16
16
|
registered_entities[class_name.to_s] = RegisteredEntity.new(
|
|
17
17
|
class_name:,
|
|
18
18
|
search_by:,
|
|
@@ -2,7 +2,8 @@ require "flipside/search_result"
|
|
|
2
2
|
|
|
3
3
|
module Flipside
|
|
4
4
|
class RegisteredEntity
|
|
5
|
-
attr_reader :class_name
|
|
5
|
+
attr_reader :class_name
|
|
6
|
+
attr_accessor :search_by, :display_as, :identified_by
|
|
6
7
|
|
|
7
8
|
def initialize(class_name:, identified_by: :id, search_by: nil, display_as: nil)
|
|
8
9
|
@class_name = class_name
|
|
@@ -11,8 +12,12 @@ module Flipside
|
|
|
11
12
|
@identified_by = identified_by
|
|
12
13
|
end
|
|
13
14
|
|
|
15
|
+
def options
|
|
16
|
+
{search_by:, display_as:, identified_by:}
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
def search(query)
|
|
15
|
-
Array(
|
|
20
|
+
Array(lookup(query)).map do |entity|
|
|
16
21
|
SearchResult.new(
|
|
17
22
|
entity,
|
|
18
23
|
display(entity),
|
|
@@ -22,39 +27,32 @@ module Flipside
|
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def find(identifier)
|
|
25
|
-
|
|
30
|
+
klass.find_by!("#{identified_by}": identifier)
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
def display(entity)
|
|
29
|
-
|
|
34
|
+
case display_as
|
|
35
|
+
when Proc then display_as.call(entity)
|
|
36
|
+
when Symbol then entity.public_send(display_as)
|
|
37
|
+
when String then display_as
|
|
38
|
+
else entity.public_send(identified_by)
|
|
39
|
+
end
|
|
30
40
|
end
|
|
31
41
|
|
|
32
42
|
private
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
->(query) { class_name.constantize.where("#{identified_by}": query) }
|
|
43
|
-
end
|
|
44
|
+
# A block runs in the context of the class, so it can call where directly.
|
|
45
|
+
# A Symbol names a column to match exactly.
|
|
46
|
+
def lookup(query)
|
|
47
|
+
case search_by
|
|
48
|
+
when Proc then klass.instance_exec(query, &search_by)
|
|
49
|
+
when Symbol then klass.where("#{search_by}": query)
|
|
50
|
+
else klass.where("#{identified_by}": query)
|
|
51
|
+
end
|
|
44
52
|
end
|
|
45
53
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
case display_as
|
|
49
|
-
when Proc
|
|
50
|
-
display_as
|
|
51
|
-
when Symbol
|
|
52
|
-
->(entity) { entity.public_send(display_as) }
|
|
53
|
-
when String
|
|
54
|
-
->(entity) { display_as }
|
|
55
|
-
else
|
|
56
|
-
->(entity) { entity.public_send(identified_by) }
|
|
57
|
-
end
|
|
54
|
+
def klass
|
|
55
|
+
class_name.constantize
|
|
58
56
|
end
|
|
59
57
|
end
|
|
60
58
|
end
|
data/lib/flipside/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flipside
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sammy Henningsson
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activerecord
|
|
@@ -177,7 +176,6 @@ metadata:
|
|
|
177
176
|
homepage_uri: https://github.com/apoex/flipside
|
|
178
177
|
source_code_uri: https://github.com/apoex/flipside
|
|
179
178
|
changelog_uri: https://github.com/apoex/flipside/blob/main/CHANGELOG.md
|
|
180
|
-
post_install_message:
|
|
181
179
|
rdoc_options: []
|
|
182
180
|
require_paths:
|
|
183
181
|
- app
|
|
@@ -193,8 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
193
191
|
- !ruby/object:Gem::Version
|
|
194
192
|
version: '0'
|
|
195
193
|
requirements: []
|
|
196
|
-
rubygems_version:
|
|
197
|
-
signing_key:
|
|
194
|
+
rubygems_version: 4.0.20
|
|
198
195
|
specification_version: 4
|
|
199
196
|
summary: Feature flags.
|
|
200
197
|
test_files: []
|