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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9f532913a357e7f57e0c8c25027ac902d18b41f28ee43481675b320ce2b64b1
4
- data.tar.gz: 9fd4fe7cf2de639c2730cdab1115d952b7bd98fb91add5b0c19df021fa702ded
3
+ metadata.gz: d4ecf5b08df6bcc2ac0bd7a9f2e4396d361fefb53b497323725ab543ed3d60a8
4
+ data.tar.gz: a64de96569181f04e4e7f50f9c7122299ee79581115c0abfbdf24aceef706f03
5
5
  SHA512:
6
- metadata.gz: 4ff600b3a1d15051fe63d7c372c04ef03486271f4ccc0de3305ae1a052745553894389ed0989f596814c78142b211a342b4755d9d35b5b1db002b274171254bb
7
- data.tar.gz: 778c918d6b61bc4dd24407fadd5476657e382f35a36c1ee660d980e4f1b119d5f3ad55e366dd939234a6ce95142aed8b6815e45d0cefc2a5c1baa22a6775237c
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 with `flipside_entity` (see [Registering from the model](#registering-from-the-model)) removes its entities when a record is destroyed.
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
- flipside_entity search_by: :name, display_as: :name
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
  ![Add an entity](/add_entity.png)
206
207
 
207
- `flipside_entity` should be called in each class that may be used as a feature enabler. It 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
+ 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 search_by: :name, display_as: :name, identified_by: :id
213
+ flipside_entity
213
214
  end
214
215
  ```
215
216
 
216
- The `search_by` keyword argument, which may be a `Symbol` or a `Proc`, dictates how records are found from searching in the ui.
217
- When a `Symbol` is given, e.g. `:name`, then entities with an exact match on the corresponding attribute are returned. I.e. `User.where(name: query)`.
218
- When a `Proc` is given, then this `Proc` is called with the search string and is expected to return an object responding to `to_a` (e.g. an AR collection).
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
- flipside_entity search_by: ->(str) {
223
- where("lower(first_name) = :name OR lower(last_name) = :name or email LIKE :str", name: str.downcase, str: "#{str}%")
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
- The `identified_by` keyword argument, sets the column used as primary key for the corresponding table. This defaults to `:id` and typically does need to be change.
228
- Currently composite keys are not supported.
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
- The `display_as` keyword argument, is used to configure how these entities show up in the combobox. When set to a `Symbol`, then this value is sent to the corresponding entity.
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
- flipside_entity display_as: :name
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 `Proc` is given, then it is expected to take an entity as input and return a string used for displaying the entity. The config above could then instead be done using:
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
- flipside_entity display_as: ->(user) { [user.first_name, user.last_name].compact.map(&:capitalize).join(" ") }
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.flipside_entity(display_as: :name)
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
- flipside_entity search_by: :name, display_as: :name
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
- def flipside_entity(search_by: nil, display_as: nil, identified_by: :id)
18
- register_flippable!
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
- has_many :flipside_entities,
21
- class_name: "Flipside::Entity",
22
- as: :flippable,
23
- dependent: :delete_all
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
- Flipside.add_registered_entity(class_name: name, search_by:, display_as:, identified_by:)
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:, display_as:, identified_by: :id) # :nodoc:
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, :search_by, :display_as, :identified_by
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(lookup_proc.call(query)).map do |entity|
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
- class_name.constantize.find_by!("#{identified_by}": identifier)
30
+ klass.find_by!("#{identified_by}": identifier)
26
31
  end
27
32
 
28
33
  def display(entity)
29
- display_proc.call(entity)
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
- def lookup_proc
35
- @lookup_proc ||=
36
- case search_by
37
- when Proc
38
- search_by
39
- when Symbol
40
- ->(query) { class_name.constantize.where("#{search_by}": query) }
41
- else
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 display_proc
47
- @display_proc ||=
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flipside
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.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: 2026-09-25 00:00:00.000000000 Z
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: 3.5.11
197
- signing_key:
194
+ rubygems_version: 4.0.20
198
195
  specification_version: 4
199
196
  summary: Feature flags.
200
197
  test_files: []