better_model 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6ba3b1688c75e16f9499780eebd533c307cd8ba18f9c37a03597c8d62310fae7
4
+ data.tar.gz: '0835904e4ea513187668f8f9c36e3d9855096bc46a2150555fae900b320b764d'
5
+ SHA512:
6
+ metadata.gz: d42074244a7e4af9321b68e9657f343ac4c087956856cfd0cbfe68e72adc1fce5200fc051cdfe1badd0e6bc753dfa3abcf0603113e3dede3cd7ce083b3ba932c
7
+ data.tar.gz: e5305841ab87d40047b6bc458bdac8a163a96d0d9e864ead0a0335ab84b53759a5da837cb6b95f1c2bdc3596fcd74dc7281d77d370a3db0aa9cc98f72c281708
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright alessiobussolari
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,312 @@
1
+ # BetterModel
2
+
3
+ BetterModel is a Rails engine gem (Rails 8.1+) that provides powerful extensions for ActiveRecord models, including declarative status management, permissions, archiving, sorting, filtering, and unified search capabilities.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "better_model"
11
+ ```
12
+
13
+ And then execute:
14
+ ```bash
15
+ $ bundle install
16
+ ```
17
+
18
+ Or install it yourself as:
19
+ ```bash
20
+ $ gem install better_model
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ Simply include `BetterModel` in your model to get all features:
26
+
27
+ ```ruby
28
+ class Article < ApplicationRecord
29
+ include BetterModel # Includes all BetterModel concerns
30
+
31
+ # 1. STATUSABLE - Define statuses with lambdas
32
+ is :draft, -> { status == "draft" }
33
+ is :published, -> { status == "published" && published_at.present? }
34
+ is :expired, -> { expires_at.present? && expires_at <= Time.current }
35
+ is :popular, -> { view_count >= 100 }
36
+ is :active, -> { is?(:published) && !is?(:expired) }
37
+
38
+ # 2. PERMISSIBLE - Define permissions based on statuses
39
+ permit :edit, -> { is?(:draft) || (is?(:published) && !is?(:expired)) }
40
+ permit :delete, -> { is?(:draft) }
41
+ permit :publish, -> { is?(:draft) }
42
+ permit :unpublish, -> { is?(:published) }
43
+
44
+ # 3. SORTABLE - Define sortable fields
45
+ sort :title, :view_count, :published_at, :created_at
46
+
47
+ # 4. PREDICABLE - Define searchable/filterable fields
48
+ predicates :title, :status, :view_count, :published_at, :created_at, :featured
49
+
50
+ # 5. ARCHIVABLE - Soft delete with tracking (opt-in)
51
+ archivable do
52
+ skip_archived_by_default true # Hide archived records by default
53
+ end
54
+
55
+ # 6. SEARCHABLE - Configure unified search interface
56
+ searchable do
57
+ per_page 25
58
+ max_per_page 100
59
+ default_order [:sort_published_at_desc]
60
+ security :status_required, [:status_eq]
61
+ end
62
+ end
63
+ ```
64
+
65
+ Now you can use all the features:
66
+
67
+ ```ruby
68
+ # Check statuses
69
+ article.is?(:draft) # => true/false
70
+ article.is_published? # => true/false
71
+ article.statuses # => { draft: true, published: false, ... }
72
+
73
+ # Check permissions
74
+ article.permit?(:edit) # => true/false
75
+ article.permit_delete? # => true/false
76
+ article.permissions # => { edit: true, delete: true, ... }
77
+
78
+ # Sort
79
+ Article.sort_title_asc
80
+ Article.sort_view_count_desc
81
+ Article.sort_published_at_desc
82
+
83
+ # Filter with predicates
84
+ Article.status_eq("published")
85
+ Article.title_cont("Rails")
86
+ Article.view_count_gteq(100)
87
+ Article.published_at_present
88
+
89
+ # Archive records
90
+ article.archive!(by: current_user, reason: "Outdated")
91
+ article.archived? # => true
92
+ article.restore!
93
+
94
+ # Query archived records
95
+ Article.archived
96
+ Article.not_archived
97
+ Article.archived_recently(7.days)
98
+
99
+ # Unified search with filters, sorting, and pagination
100
+ Article.search(
101
+ { status_eq: "published", view_count_gteq: 50 },
102
+ orders: [:sort_published_at_desc],
103
+ pagination: { page: 1, per_page: 25 }
104
+ )
105
+ ```
106
+
107
+ ### Including Individual Concerns (Advanced)
108
+
109
+ If you only need specific features, you can include individual concerns:
110
+
111
+ ```ruby
112
+ class Article < ApplicationRecord
113
+ include BetterModel::Statusable # Only status management
114
+ include BetterModel::Permissible # Only permissions
115
+ include BetterModel::Archivable # Only archiving
116
+ include BetterModel::Sortable # Only sorting
117
+ include BetterModel::Predicable # Only filtering
118
+ include BetterModel::Searchable # Only search (requires Predicable & Sortable)
119
+
120
+ # Define your features...
121
+ end
122
+ ```
123
+
124
+ ## Requirements
125
+
126
+ - **Ruby:** 3.0 or higher
127
+ - **Rails:** 8.1 or higher
128
+ - **ActiveRecord:** Included with Rails
129
+
130
+ ## Database Compatibility
131
+
132
+ BetterModel works with all databases supported by ActiveRecord:
133
+
134
+ | Database | Status | Notes |
135
+ |----------|--------|-------|
136
+ | **PostgreSQL** | ✅ Full support | Recommended. Includes array and JSONB predicates |
137
+ | **MySQL/MariaDB** | ✅ Full support | NULLS emulation for sorting |
138
+ | **SQLite** | ✅ Full support | Great for development and testing |
139
+ | **SQL Server** | ✅ Full support | Standard features work |
140
+ | **Oracle** | ✅ Full support | Standard features work |
141
+
142
+ **PostgreSQL-Specific Features:**
143
+ - Array predicates: `overlaps`, `contains`, `contained_by`
144
+ - JSONB predicates: `has_key`, `has_any_key`, `has_all_keys`, `jsonb_contains`
145
+
146
+ ## Features
147
+
148
+ BetterModel provides six powerful concerns that work together seamlessly:
149
+
150
+ ### 📋 Statusable - Declarative Status Management
151
+
152
+ Define derived statuses dynamically based on model attributes - no database columns needed!
153
+
154
+ **Key Benefits:**
155
+ - Declarative DSL with clear, readable conditions
156
+ - Statuses calculated in real-time from model attributes
157
+ - Reference other statuses in conditions
158
+ - Automatic method generation (`is_draft?`, `is_published?`)
159
+ - Thread-safe with immutable registry
160
+
161
+ **[📖 Full Documentation →](docs/statusable.md)**
162
+
163
+ ---
164
+
165
+ ### 🔐 Permissible - Declarative Permission Management
166
+
167
+ Define permissions dynamically based on model state and statuses - perfect for authorization logic!
168
+
169
+ **Key Benefits:**
170
+ - Declarative DSL following Statusable pattern
171
+ - Permissions calculated from model state
172
+ - Reference statuses in permission logic
173
+ - Automatic method generation (`permit_edit?`, `permit_delete?`)
174
+ - Thread-safe with immutable registry
175
+
176
+ **[📖 Full Documentation →](docs/permissible.md)**
177
+
178
+ ---
179
+
180
+ ### 🗄️ Archivable - Soft Delete with Archive Management
181
+
182
+ Soft-delete records with archive tracking, audit trails, and restoration capabilities.
183
+
184
+ **Key Benefits:**
185
+ - Opt-in activation: only enabled when explicitly configured
186
+ - Archive and restore methods with optional tracking
187
+ - Status methods: `archived?` and `active?`
188
+ - Semantic scopes: `archived`, `not_archived`, `archived_only`
189
+ - Helper predicates: `archived_today`, `archived_this_week`, `archived_recently`
190
+ - Optional default scope to hide archived records
191
+ - Migration generator with flexible options
192
+ - Thread-safe with immutable configuration
193
+
194
+ **[📖 Full Documentation →](docs/archivable.md)**
195
+
196
+ ---
197
+
198
+ ### ⬆️ Sortable - Type-Aware Sorting Scopes
199
+
200
+ Generate intelligent sorting scopes automatically with database-specific optimizations and NULL handling.
201
+
202
+ **Key Benefits:**
203
+ - Type-aware scope generation (string, numeric, datetime, boolean)
204
+ - Case-insensitive sorting for strings
205
+ - Database-specific NULLS FIRST/LAST support
206
+ - Sort by multiple fields with chaining
207
+ - Optimized queries with proper indexing support
208
+
209
+ **[📖 Full Documentation →](docs/sortable.md)**
210
+
211
+ ---
212
+
213
+ ### 🔍 Predicable - Advanced Query Scopes
214
+
215
+ Generate comprehensive predicate scopes for filtering and searching with support for all data types.
216
+
217
+ **Key Benefits:**
218
+ - Complete coverage: string, numeric, datetime, boolean, null predicates
219
+ - Type-safe predicates based on column type
220
+ - Case-insensitive string matching
221
+ - Range queries (between) for numerics and dates
222
+ - PostgreSQL array and JSONB support
223
+ - Chainable with standard ActiveRecord queries
224
+
225
+ **[📖 Full Documentation →](docs/predicable.md)**
226
+
227
+ ---
228
+
229
+ ### 🔎 Searchable - Unified Search Interface
230
+
231
+ Orchestrate Predicable and Sortable into a powerful, secure search interface with pagination and security.
232
+
233
+ **Key Benefits:**
234
+ - Unified API: single `search()` method for all operations
235
+ - OR conditions for complex logic
236
+ - Built-in pagination with DoS protection (max_per_page)
237
+ - Security enforcement with required predicates
238
+ - Default ordering configuration
239
+ - Strong parameters integration
240
+ - Type-safe validation of all parameters
241
+
242
+ **[📖 Full Documentation →](docs/searchable.md)**
243
+
244
+ ---
245
+
246
+ ## Version & Changelog
247
+
248
+ **Current Version:** 1.0.0
249
+
250
+ See [CHANGELOG.md](CHANGELOG.md) for version history and release notes.
251
+
252
+ ## Support & Community
253
+
254
+ - **Issues & Bugs:** [GitHub Issues](https://github.com/alessiobussolari/better_model/issues)
255
+ - **Source Code:** [GitHub Repository](https://github.com/alessiobussolari/better_model)
256
+ - **Documentation:** This README and detailed docs in `docs/` directory
257
+
258
+ ## Contributing
259
+
260
+ We welcome contributions! Here's how you can help:
261
+
262
+ ### Reporting Bugs
263
+
264
+ 1. Check if the issue already exists in [GitHub Issues](https://github.com/alessiobussolari/better_model/issues)
265
+ 2. Create a new issue with:
266
+ - Clear description of the problem
267
+ - Steps to reproduce
268
+ - Expected vs actual behavior
269
+ - Ruby/Rails versions
270
+ - Database adapter
271
+
272
+ ### Submitting Pull Requests
273
+
274
+ 1. Fork the repository
275
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
276
+ 3. Make your changes with tests
277
+ 4. Run the test suite (`bundle exec rake test`)
278
+ 5. Ensure RuboCop passes (`bundle exec rubocop`)
279
+ 6. Commit your changes (`git commit -m 'Add amazing feature'`)
280
+ 7. Push to the branch (`git push origin feature/amazing-feature`)
281
+ 8. Open a Pull Request
282
+
283
+ ### Development Setup
284
+
285
+ ```bash
286
+ # Clone your fork
287
+ git clone https://github.com/YOUR_USERNAME/better_model.git
288
+ cd better_model
289
+
290
+ # Install dependencies
291
+ bundle install
292
+
293
+ # Run tests
294
+ bundle exec rake test
295
+
296
+ # Run SimpleCov for coverage
297
+ bundle exec rake test # Coverage report in coverage/index.html
298
+
299
+ # Run RuboCop
300
+ bundle exec rubocop
301
+ ```
302
+
303
+ ### Code Guidelines
304
+
305
+ - Follow the existing code style (enforced by RuboCop Omakase)
306
+ - Write tests for new features
307
+ - Update documentation (README) for user-facing changes
308
+ - Keep pull requests focused (one feature/fix per PR)
309
+
310
+ ## License
311
+
312
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.pattern = "test/**/*_test.rb"
9
+ t.verbose = false
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Archivable - Sistema di archiviazione dichiarativa per modelli Rails
4
+ #
5
+ # Questo concern permette di archiviare e ripristinare record utilizzando un DSL
6
+ # semplice e dichiarativo, con supporto per predicati e scopes.
7
+ #
8
+ # SETUP RAPIDO:
9
+ # # Opzione 1: Generator automatico (raccomandato)
10
+ # rails g better_model:archivable Article --with-tracking
11
+ # rails db:migrate
12
+ #
13
+ # # Opzione 2: Migration manuale
14
+ # rails g migration AddArchivableToArticles archived_at:datetime archived_by_id:integer archive_reason:string
15
+ # rails db:migrate
16
+ #
17
+ # APPROCCIO OPT-IN: L'archiviazione non è attiva automaticamente. Devi chiamare
18
+ # esplicitamente `archivable do...end` nel tuo modello per attivarla.
19
+ #
20
+ # APPROCCIO IBRIDO: Usa i predicati esistenti (archived_at_present, archived_at_null, etc.)
21
+ # e fornisce alias semantici (archived, not_archived) per una migliore leggibilità.
22
+ #
23
+ # REQUISITI DATABASE:
24
+ # - archived_at (datetime) - REQUIRED (obbligatorio)
25
+ # - archived_by_id (integer) - OPTIONAL (per tracking utente)
26
+ # - archive_reason (string) - OPTIONAL (per motivazione)
27
+ #
28
+ # Esempio di utilizzo:
29
+ # class Article < ApplicationRecord
30
+ # include BetterModel
31
+ #
32
+ # # Attiva archivable (opt-in)
33
+ # archivable do
34
+ # skip_archived_by_default true # Opzionale: nascondi archiviati di default
35
+ # end
36
+ # end
37
+ #
38
+ # Utilizzo:
39
+ # article.archive! # Archive the record
40
+ # article.archive!(by: user) # Archive with user tracking
41
+ # article.archive!(reason: "Outdated") # Archive with reason
42
+ # article.restore! # Restore archived record
43
+ # article.archived? # Check if archived
44
+ # article.active? # Check if not archived
45
+ #
46
+ # # Scopes semantici
47
+ # Article.archived # Find archived records
48
+ # Article.not_archived # Find active records
49
+ # Article.archived_only # Bypass default scope
50
+ #
51
+ # # Predicati potenti (generati automaticamente)
52
+ # Article.archived_at_within(7.days) # Archived in last 7 days
53
+ # Article.archived_at_today # Archived today
54
+ # Article.archived_at_between(start, end) # Archived in range
55
+ #
56
+ # # Helper methods
57
+ # Article.archived_today # Alias for archived_at_today
58
+ # Article.archived_this_week # Alias for archived_at_this_week
59
+ # Article.archived_recently(7.days) # Alias for archived_at_within
60
+ #
61
+ # # Con Searchable
62
+ # Article.search({ archived_at_null: true, status_eq: "published" })
63
+ #
64
+ module BetterModel
65
+ module Archivable
66
+ extend ActiveSupport::Concern
67
+
68
+ included do
69
+ # Validazione ActiveRecord
70
+ unless ancestors.include?(ActiveRecord::Base)
71
+ raise ArgumentError, "BetterModel::Archivable can only be included in ActiveRecord models"
72
+ end
73
+
74
+ # Configurazione archivable (opt-in)
75
+ class_attribute :archivable_enabled, default: false
76
+ class_attribute :archivable_config, default: {}.freeze
77
+ end
78
+
79
+ class_methods do
80
+ # DSL per attivare e configurare archivable (OPT-IN)
81
+ #
82
+ # @example Attivazione base
83
+ # archivable
84
+ #
85
+ # @example Con configurazione
86
+ # archivable do
87
+ # skip_archived_by_default true
88
+ # end
89
+ def archivable(&block)
90
+ # Valida che archived_at esista
91
+ unless column_names.include?("archived_at")
92
+ raise ArgumentError,
93
+ "Archivable requires an 'archived_at' datetime column. " \
94
+ "Add it with: rails g migration AddArchivedAtTo#{table_name.classify.pluralize} archived_at:datetime"
95
+ end
96
+
97
+ # Attiva archivable
98
+ self.archivable_enabled = true
99
+
100
+ # Definisci predicati su archived_at (opt-in!)
101
+ predicates :archived_at unless predicable_field?(:archived_at)
102
+
103
+ # Definisci anche sort su archived_at (opt-in!)
104
+ sort :archived_at unless sortable_field?(:archived_at)
105
+
106
+ # Definisci gli scope alias (approccio ibrido)
107
+ scope :archived, -> { archived_at_present }
108
+ scope :not_archived, -> { archived_at_null }
109
+
110
+ # Configura se passato un blocco
111
+ if block_given?
112
+ configurator = ArchivableConfigurator.new(self)
113
+ configurator.instance_eval(&block)
114
+ self.archivable_config = configurator.to_h.freeze
115
+
116
+ # Applica default scope SOLO se configurato
117
+ if archivable_config[:skip_archived_by_default]
118
+ default_scope -> { not_archived }
119
+ end
120
+ end
121
+ end
122
+
123
+ # Trova SOLO record archiviati, bypassando default scope
124
+ #
125
+ # @return [ActiveRecord::Relation]
126
+ def archived_only
127
+ raise NotEnabledError unless archivable_enabled?
128
+ unscoped.archived
129
+ end
130
+
131
+ # Helper: alias per archived_at_today
132
+ def archived_today
133
+ raise NotEnabledError unless archivable_enabled?
134
+ archived_at_today
135
+ end
136
+
137
+ # Helper: alias per archived_at_this_week
138
+ def archived_this_week
139
+ raise NotEnabledError unless archivable_enabled?
140
+ archived_at_this_week
141
+ end
142
+
143
+ # Helper: alias per archived_at_within
144
+ #
145
+ # @param duration [ActiveSupport::Duration] Durata (es: 7.days)
146
+ # @return [ActiveRecord::Relation]
147
+ def archived_recently(duration = 7.days)
148
+ raise NotEnabledError unless archivable_enabled?
149
+ archived_at_within(duration)
150
+ end
151
+
152
+ # Verifica se archivable è attivo
153
+ #
154
+ # @return [Boolean]
155
+ def archivable_enabled?
156
+ archivable_enabled == true
157
+ end
158
+ end
159
+
160
+ # Metodi di istanza
161
+
162
+ # Archivia il record
163
+ #
164
+ # @param by [Integer, Object] ID utente o oggetto user (opzionale)
165
+ # @param reason [String] Motivo dell'archiviazione (opzionale)
166
+ # @return [self]
167
+ # @raise [NotEnabledError] se archivable non è attivo
168
+ # @raise [AlreadyArchivedError] se già archiviato
169
+ def archive!(by: nil, reason: nil)
170
+ raise NotEnabledError unless self.class.archivable_enabled?
171
+ raise AlreadyArchivedError, "Record is already archived" if archived?
172
+
173
+ self.archived_at = Time.current
174
+
175
+ # Set archived_by_id: accetta sia ID che oggetti con .id
176
+ if respond_to?(:archived_by_id=) && by.present?
177
+ self.archived_by_id = by.respond_to?(:id) ? by.id : by
178
+ end
179
+
180
+ self.archive_reason = reason if respond_to?(:archive_reason=)
181
+
182
+ save!(validate: false)
183
+ self
184
+ end
185
+
186
+ # Ripristina record archiviato
187
+ #
188
+ # @return [self]
189
+ # @raise [NotEnabledError] se archivable non è attivo
190
+ # @raise [NotArchivedError] se non archiviato
191
+ def restore!
192
+ raise NotEnabledError unless self.class.archivable_enabled?
193
+ raise NotArchivedError, "Record is not archived" unless archived?
194
+
195
+ self.archived_at = nil
196
+ self.archived_by_id = nil if respond_to?(:archived_by_id=)
197
+ self.archive_reason = nil if respond_to?(:archive_reason=)
198
+
199
+ save!(validate: false)
200
+ self
201
+ end
202
+
203
+ # Verifica se il record è archiviato
204
+ #
205
+ # @return [Boolean]
206
+ def archived?
207
+ return false unless self.class.archivable_enabled?
208
+ archived_at.present?
209
+ end
210
+
211
+ # Verifica se il record è attivo (non archiviato)
212
+ #
213
+ # @return [Boolean]
214
+ def active?
215
+ !archived?
216
+ end
217
+
218
+ # Override as_json per includere info archivio
219
+ #
220
+ # @param options [Hash] Opzioni as_json
221
+ # @option options [Boolean] :include_archive_info Include archive metadata
222
+ # @return [Hash]
223
+ def as_json(options = {})
224
+ result = super
225
+
226
+ if options[:include_archive_info] && self.class.archivable_enabled?
227
+ result["archive_info"] = {
228
+ "archived" => archived?,
229
+ "archived_at" => archived_at,
230
+ "archived_by_id" => (respond_to?(:archived_by_id) ? archived_by_id : nil),
231
+ "archive_reason" => (respond_to?(:archive_reason) ? archive_reason : nil)
232
+ }
233
+ end
234
+
235
+ result
236
+ end
237
+ end
238
+
239
+ # Errori custom
240
+ class ArchivableError < StandardError; end
241
+ class AlreadyArchivedError < ArchivableError; end
242
+ class NotArchivedError < ArchivableError; end
243
+
244
+ class NotEnabledError < ArchivableError
245
+ def initialize(msg = nil)
246
+ super(msg || "Archivable is not enabled. Add 'archivable do...end' to your model.")
247
+ end
248
+ end
249
+
250
+ # Configurator per archivable DSL
251
+ class ArchivableConfigurator
252
+ attr_reader :config
253
+
254
+ def initialize(model_class)
255
+ @model_class = model_class
256
+ @config = {
257
+ skip_archived_by_default: false
258
+ }
259
+ end
260
+
261
+ # Configura default scope per nascondere archiviati
262
+ #
263
+ # @param value [Boolean] true per nascondere archiviati di default
264
+ def skip_archived_by_default(value)
265
+ @config[:skip_archived_by_default] = !!value
266
+ end
267
+
268
+ def to_h
269
+ @config
270
+ end
271
+ end
272
+ end