compony 0.11.8 → 0.11.10

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.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.yardopts +36 -1
  4. data/CHANGELOG.md +43 -0
  5. data/CLAUDE.md +85 -0
  6. data/Gemfile.lock +1 -1
  7. data/README.md +13 -3
  8. data/VERSION +1 -1
  9. data/compony.gemspec +4 -4
  10. data/doc/ComponentGenerator.html +2 -2
  11. data/doc/Components.html +2 -2
  12. data/doc/ComponentsGenerator.html +2 -2
  13. data/doc/Compony/Component.html +55 -55
  14. data/doc/Compony/ComponentMixins/Default/Labelling.html +2 -2
  15. data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +2 -2
  16. data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +110 -71
  17. data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +65 -29
  18. data/doc/Compony/ComponentMixins/Default/Standalone.html +2 -2
  19. data/doc/Compony/ComponentMixins/Default.html +2 -2
  20. data/doc/Compony/ComponentMixins/Resourceful.html +214 -75
  21. data/doc/Compony/ComponentMixins.html +2 -2
  22. data/doc/Compony/Components/Buttons/CssButton.html +2 -2
  23. data/doc/Compony/Components/Buttons/Link.html +2 -2
  24. data/doc/Compony/Components/Buttons.html +2 -2
  25. data/doc/Compony/Components/Destroy.html +84 -30
  26. data/doc/Compony/Components/Edit.html +111 -39
  27. data/doc/Compony/Components/Form.html +552 -209
  28. data/doc/Compony/Components/Index.html +2 -2
  29. data/doc/Compony/Components/List.html +51 -35
  30. data/doc/Compony/Components/New.html +111 -39
  31. data/doc/Compony/Components/Show.html +2 -2
  32. data/doc/Compony/Components/WithForm.html +195 -48
  33. data/doc/Compony/Components.html +2 -2
  34. data/doc/Compony/ControllerMixin.html +2 -2
  35. data/doc/Compony/Engine.html +2 -2
  36. data/doc/Compony/Intent.html +3 -3
  37. data/doc/Compony/ManageIntentsDsl.html +2 -2
  38. data/doc/Compony/MethodAccessibleHash.html +2 -2
  39. data/doc/Compony/ModelFields/Anchormodel.html +2 -2
  40. data/doc/Compony/ModelFields/Association.html +2 -2
  41. data/doc/Compony/ModelFields/Attachment.html +2 -2
  42. data/doc/Compony/ModelFields/Base.html +2 -2
  43. data/doc/Compony/ModelFields/Boolean.html +2 -2
  44. data/doc/Compony/ModelFields/Color.html +2 -2
  45. data/doc/Compony/ModelFields/Currency.html +2 -2
  46. data/doc/Compony/ModelFields/Date.html +2 -2
  47. data/doc/Compony/ModelFields/Datetime.html +2 -2
  48. data/doc/Compony/ModelFields/Decimal.html +2 -2
  49. data/doc/Compony/ModelFields/Email.html +2 -2
  50. data/doc/Compony/ModelFields/Float.html +2 -2
  51. data/doc/Compony/ModelFields/Integer.html +2 -2
  52. data/doc/Compony/ModelFields/Percentage.html +2 -2
  53. data/doc/Compony/ModelFields/Phone.html +2 -2
  54. data/doc/Compony/ModelFields/RichText.html +2 -2
  55. data/doc/Compony/ModelFields/String.html +2 -2
  56. data/doc/Compony/ModelFields/Text.html +2 -2
  57. data/doc/Compony/ModelFields/Time.html +2 -2
  58. data/doc/Compony/ModelFields/Url.html +2 -2
  59. data/doc/Compony/ModelFields.html +2 -2
  60. data/doc/Compony/ModelMixin.html +36 -36
  61. data/doc/Compony/NaturalOrdering.html +2 -2
  62. data/doc/Compony/RequestContext.html +2 -2
  63. data/doc/Compony/Version.html +2 -2
  64. data/doc/Compony/ViewHelpers.html +2 -2
  65. data/doc/Compony/VirtualModel.html +2 -2
  66. data/doc/Compony.html +33 -37
  67. data/doc/ComponyController.html +2 -2
  68. data/doc/_index.html +98 -2
  69. data/doc/file.CHANGELOG.html +765 -0
  70. data/doc/file.README.html +26 -5
  71. data/doc/file.basic_component.html +314 -0
  72. data/doc/file.cookbook.html +189 -0
  73. data/doc/file.destroy.html +105 -0
  74. data/doc/file.dsl_reference.html +672 -0
  75. data/doc/file.edit.html +109 -0
  76. data/doc/file.example.html +291 -0
  77. data/doc/file.example_advanced.html +257 -0
  78. data/doc/file.feasibility.html +115 -0
  79. data/doc/file.form.html +195 -0
  80. data/doc/file.generators.html +89 -0
  81. data/doc/file.glossary.html +217 -0
  82. data/doc/file.gotchas.html +222 -0
  83. data/doc/file.index.html +135 -0
  84. data/doc/file.inheritance.html +136 -0
  85. data/doc/file.installation.html +115 -0
  86. data/doc/file.integrations.html +218 -0
  87. data/doc/file.intents.html +265 -0
  88. data/doc/file.internal_datastructures.html +129 -0
  89. data/doc/file.list.html +253 -0
  90. data/doc/file.maintaining.html +127 -0
  91. data/doc/file.model_fields.html +137 -0
  92. data/doc/file.nesting.html +237 -0
  93. data/doc/file.new.html +109 -0
  94. data/doc/file.ownership.html +98 -0
  95. data/doc/file.patterns.html +669 -0
  96. data/doc/file.pre_built_components.html +99 -0
  97. data/doc/file.resourceful.html +181 -0
  98. data/doc/file.show.html +158 -0
  99. data/doc/file.standalone.html +233 -0
  100. data/doc/file.virtual_models.html +117 -0
  101. data/doc/file.with_form.html +157 -0
  102. data/doc/file_list.html +160 -0
  103. data/doc/guide/cookbook.md +41 -0
  104. data/doc/guide/dsl_reference.md +155 -0
  105. data/doc/guide/example_advanced.md +209 -0
  106. data/doc/guide/generators.md +1 -1
  107. data/doc/guide/glossary.md +42 -0
  108. data/doc/guide/gotchas.md +125 -0
  109. data/doc/guide/maintaining.md +64 -0
  110. data/doc/guide/patterns.md +681 -0
  111. data/doc/guide/pre_built_components/edit.md +1 -1
  112. data/doc/guide/pre_built_components/index.md +64 -1
  113. data/doc/guide/pre_built_components/list.md +111 -7
  114. data/doc/guide/pre_built_components/show.md +57 -2
  115. data/doc/guide/pre_built_components/with_form.md +56 -9
  116. data/doc/guide/pre_built_components.md +7 -2
  117. data/doc/guide/standalone.md +16 -1
  118. data/doc/index.html +26 -5
  119. data/doc/integrations.md +61 -0
  120. data/doc/llms.txt +62 -0
  121. data/doc/top-level-namespace.html +2 -2
  122. data/lib/compony/component.rb +8 -3
  123. data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +32 -15
  124. data/lib/compony/component_mixins/default/standalone/verb_dsl.rb +11 -3
  125. data/lib/compony/component_mixins/resourceful.rb +30 -16
  126. data/lib/compony/components/destroy.rb +21 -1
  127. data/lib/compony/components/edit.rb +25 -1
  128. data/lib/compony/components/form.rb +63 -21
  129. data/lib/compony/components/list.rb +9 -1
  130. data/lib/compony/components/new.rb +25 -1
  131. data/lib/compony/components/with_form.rb +20 -5
  132. data/lib/compony/intent.rb +1 -1
  133. data/lib/compony/model_mixin.rb +66 -3
  134. data/lib/compony.rb +4 -6
  135. metadata +44 -2
@@ -52,12 +52,13 @@ module Compony
52
52
 
53
53
  # DSL method, part of the Feasibility feature
54
54
  # Block must return `false` if the action should be prevented.
55
- def prevent(action_names, message, &block)
55
+ # @param assoc [ActiveRecord::Reflection] Internal, set by {autodetect_feasibilities!}. Allows {precompute_feasibility} to batch the check.
56
+ def prevent(action_names, message, assoc: nil, &block)
56
57
  action_names = [action_names] unless action_names.is_a? Enumerable
57
58
  action_names.each do |action_name|
58
59
  self.feasibility_preventions = feasibility_preventions.dup # Prevent cross-class contamination
59
60
  feasibility_preventions[action_name.to_sym] ||= []
60
- feasibility_preventions[action_name.to_sym] << MethodAccessibleHash.new(action_name:, message:, block:)
61
+ feasibility_preventions[action_name.to_sym] << MethodAccessibleHash.new(action_name:, message:, assoc:, block:)
61
62
  end
62
63
  end
63
64
 
@@ -71,7 +72,9 @@ module Compony
71
72
  return if autodetect_feasibilities_completed
72
73
  # Add a prevention that reflects the `has_many` `dependent' properties. Avoids that users can press buttons that will result in a failed destroy.
73
74
  reflect_on_all_associations.select { |assoc| %i[restrict_with_exception restrict_with_error].include? assoc.options[:dependent] }.each do |assoc|
74
- prevent(:destroy, I18n.t('compony.feasibility.has_dependent_models', dependent_class: assoc.klass.model_name.human(count: 2))) do
75
+ # The `assoc:` is stored so that `precompute_feasibility` can resolve dependent existence for a whole collection in a single query per
76
+ # association (see batchable_feasibility_assoc?). When called on a single record (no precompute), the block below is used instead.
77
+ prevent(:destroy, I18n.t('compony.feasibility.has_dependent_models', dependent_class: assoc.klass.model_name.human(count: 2)), assoc:) do
75
78
  if assoc.is_a? ActiveRecord::Reflection::HasOneReflection
76
79
  !public_send(assoc.name).nil?
77
80
  else
@@ -82,6 +85,66 @@ module Compony
82
85
  self.autodetect_feasibilities_completed = true
83
86
  end
84
87
 
88
+ # Precomputes and caches feasibility for an entire collection of records, avoiding the N+1 queries that arise when `feasible?` is called per
89
+ # record (e.g. when rendering destroy buttons for every row of an index). For every autodetected dependent-association prevention that can be
90
+ # batched (see {batchable_feasibility_assoc?}), this issues a single existence query across all records instead of one query per record.
91
+ # Preventions that cannot be batched (custom `prevent` blocks, polymorphic/through/STI-ambiguous or argument-taking-scope associations) fall
92
+ # back to the per-record block. After this call, `feasible?` / `feasibility_messages` for the given action return cached results with no further
93
+ # queries for the batched preventions.
94
+ # @param records [Enumerable] the records to precompute feasibility for, typically the current index page
95
+ # @param action_name [Symbol,String] the action to precompute, e.g. :destroy
96
+ def precompute_feasibility(records, action_name)
97
+ action_name = action_name.to_sym
98
+ records = records.to_a
99
+ return if records.empty?
100
+ autodetect_feasibilities!
101
+ # Seed the per-record message cache so feasible? treats these as already computed.
102
+ records.each do |record|
103
+ messages = record.instance_variable_get(:@feasibility_messages) || record.instance_variable_set(:@feasibility_messages, {})
104
+ messages[action_name] = []
105
+ end
106
+ Array(feasibility_preventions[action_name]).each do |prevention|
107
+ assoc = prevention[:assoc]
108
+ if assoc && batchable_feasibility_assoc?(assoc)
109
+ apply_batched_feasibility_prevention(records, action_name, prevention, assoc)
110
+ else
111
+ # Fallback: run the prevention block once per record (custom blocks, polymorphic/through/scoped associations).
112
+ records.each do |record|
113
+ record.instance_variable_get(:@feasibility_messages)[action_name] << prevention.message if record.instance_exec(&prevention.block)
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ # Whether a dependent association can be resolved for a whole collection in a single existence query. Conservative on purpose: anything that
120
+ # would make a flat `WHERE foreign_key IN (...)` query incorrect falls back to the per-record block.
121
+ def batchable_feasibility_assoc?(assoc)
122
+ return false unless %i[has_many has_one].include?(assoc.macro)
123
+ return false if assoc.through_reflection # has_*_through: join semantics, not a flat foreign key
124
+ return false if assoc.options[:as] # polymorphic inverse: needs a type column too
125
+ return false if assoc.polymorphic? # defensive; polymorphic on this side
126
+ return false if assoc.scope&.arity&.positive? # scope needs the owner instance, can't merge onto a bare relation
127
+ assoc.klass.present? && assoc.foreign_key.present?
128
+ rescue StandardError
129
+ # If anything about the reflection can't be resolved (e.g. STI / missing constant), prefer the safe per-record fallback.
130
+ false
131
+ end
132
+
133
+ # Runs one existence query for `assoc` across all `records` and appends the prevention message to every record that has at least one
134
+ # dependent row. See {precompute_feasibility}.
135
+ def apply_batched_feasibility_prevention(records, action_name, prevention, assoc)
136
+ ids = records.map(&:id).compact
137
+ return if ids.empty?
138
+ scope = assoc.klass.all
139
+ scope = scope.instance_exec(&assoc.scope) if assoc.scope # honor static `has_many ..., -> { where(...) }` conditions
140
+ # reorder(nil) drops any ORDER BY inherited from a default_scope or the association scope: PostgreSQL rejects
141
+ # `SELECT DISTINCT ... ORDER BY <col>` when the ordering column is not in the select list, and ordering is irrelevant here anyway.
142
+ triggered_ids = scope.where(assoc.foreign_key => ids).reorder(nil).distinct.pluck(assoc.foreign_key).to_set
143
+ records.each do |record|
144
+ record.instance_variable_get(:@feasibility_messages)[action_name] << prevention.message if triggered_ids.include?(record.id)
145
+ end
146
+ end
147
+
85
148
  # Provides Ransack defaults (auth_object must be a cancancan ability)
86
149
  def ransackable_attributes(auth_object)
87
150
  auth_object.permitted_attributes(:read, self).map(&:to_s)
data/lib/compony.rb CHANGED
@@ -122,12 +122,10 @@ module Compony
122
122
  ##########=====-------
123
123
 
124
124
  # Pure helper to create a Compony Intent. If given an intent, will return it unchanged. Otherwise, will give all params to the intent initializer.
125
- def self.intent(intent_or_comp_args, ...)
126
- if intent_or_comp_args.is_a?(Intent)
127
- return intent_or_comp_args
128
- else
129
- return Intent.new(intent_or_comp_args, ...)
130
- end
125
+ def self.intent(*args, **)
126
+ first = args.first
127
+ return first if first.is_a?(Intent)
128
+ return Intent.new(*args, **)
131
129
  end
132
130
 
133
131
  # Generates a Rails path to a component. Examples: `Compony.path(:index, :users)`, `Compony.path(:show, User.first)`
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compony
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.8
4
+ version: 0.11.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Kalbermatter
@@ -172,6 +172,7 @@ files:
172
172
  - ".ruby-version"
173
173
  - ".yardopts"
174
174
  - CHANGELOG.md
175
+ - CLAUDE.md
175
176
  - Gemfile
176
177
  - Gemfile.lock
177
178
  - LICENSE
@@ -250,20 +251,59 @@ files:
250
251
  - doc/css/common.css
251
252
  - doc/css/full_list.css
252
253
  - doc/css/style.css
254
+ - doc/file.CHANGELOG.html
253
255
  - doc/file.README.html
256
+ - doc/file.basic_component.html
257
+ - doc/file.cookbook.html
258
+ - doc/file.destroy.html
259
+ - doc/file.dsl_reference.html
260
+ - doc/file.edit.html
261
+ - doc/file.example.html
262
+ - doc/file.example_advanced.html
263
+ - doc/file.feasibility.html
264
+ - doc/file.form.html
265
+ - doc/file.generators.html
266
+ - doc/file.glossary.html
267
+ - doc/file.gotchas.html
268
+ - doc/file.index.html
269
+ - doc/file.inheritance.html
270
+ - doc/file.installation.html
271
+ - doc/file.integrations.html
272
+ - doc/file.intents.html
273
+ - doc/file.internal_datastructures.html
274
+ - doc/file.list.html
275
+ - doc/file.maintaining.html
276
+ - doc/file.model_fields.html
277
+ - doc/file.nesting.html
278
+ - doc/file.new.html
279
+ - doc/file.ownership.html
280
+ - doc/file.patterns.html
281
+ - doc/file.pre_built_components.html
282
+ - doc/file.resourceful.html
283
+ - doc/file.show.html
284
+ - doc/file.standalone.html
285
+ - doc/file.virtual_models.html
286
+ - doc/file.with_form.html
254
287
  - doc/file_list.html
255
288
  - doc/frames.html
256
289
  - doc/guide/basic_component.md
290
+ - doc/guide/cookbook.md
291
+ - doc/guide/dsl_reference.md
257
292
  - doc/guide/example.md
293
+ - doc/guide/example_advanced.md
258
294
  - doc/guide/feasibility.md
259
295
  - doc/guide/generators.md
296
+ - doc/guide/glossary.md
297
+ - doc/guide/gotchas.md
260
298
  - doc/guide/inheritance.md
261
299
  - doc/guide/installation.md
262
300
  - doc/guide/intents.md
263
301
  - doc/guide/internal_datastructures.md
302
+ - doc/guide/maintaining.md
264
303
  - doc/guide/model_fields.md
265
304
  - doc/guide/nesting.md
266
305
  - doc/guide/ownership.md
306
+ - doc/guide/patterns.md
267
307
  - doc/guide/pre_built_components.md
268
308
  - doc/guide/pre_built_components/destroy.md
269
309
  - doc/guide/pre_built_components/edit.md
@@ -282,9 +322,11 @@ files:
282
322
  - doc/imgs/intro-example-new.png
283
323
  - doc/imgs/intro-example-show.png
284
324
  - doc/index.html
325
+ - doc/integrations.md
285
326
  - doc/js/app.js
286
327
  - doc/js/full_list.js
287
328
  - doc/js/jquery.js
329
+ - doc/llms.txt
288
330
  - doc/method_list.html
289
331
  - doc/resourceful_lifecycle.graphml
290
332
  - doc/resourceful_lifecycle.pdf
@@ -369,7 +411,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
369
411
  - !ruby/object:Gem::Version
370
412
  version: '0'
371
413
  requirements: []
372
- rubygems_version: 4.0.11
414
+ rubygems_version: 4.0.13
373
415
  specification_version: 4
374
416
  summary: Compony is a Gem that allows you to write your Rails application in component-style
375
417
  fashion. It combines a controller action and route along \ with its view into a