active_model_persistence 0.2.0 → 0.3.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: 5c9021a01608e4ac248001eafc0a7c2265ab82666bc33add62e9a3f3586f6bbf
4
- data.tar.gz: 4225a766baeb076abce4a27cbde7d8c742f02e660632855198ba6dda7a20434f
3
+ metadata.gz: a36224415c3d71eed1f01b39413b896af90a3b808f15c081800f31114c6105d4
4
+ data.tar.gz: f77057857a7cd5a6687017d2c9228e9c60ceb38a1cce75484ce40d93259de9d9
5
5
  SHA512:
6
- metadata.gz: 8499b0d13d3acafcd5b45df2d8c3d22a171f2f428f7b1d6de961855af7d18b839c0be9c40163bbf74bb91fa5f9956e62e37a7bcfe691eeac3eb6057508abb412
7
- data.tar.gz: fa9bab5a614c233c7c505f7f66f181abf87787d51902322c88408fe8802334f701ef5b7742e01bfa80ab3352a4765e9b811da1ba6ee145e3a80496e80a3fbaa6
6
+ metadata.gz: '084f1a34f5dbff248ab739cbe3ae95be6209d696db4b4b63191b6b5c6d0ee2b3674127b035a4eb1aef5529ae5400c5bce48d762d30193277e25865f595346ffe'
7
+ data.tar.gz: 4cba73228e285a16226418fade9a753c7378201db8bbe622ab8de1e0afb9c484bfc99fbbf2b59e438c2b801dec4101025cd4174ad277aa285ae1a94b1c6ee0a8
data/.rubocop.yml CHANGED
@@ -8,6 +8,9 @@ AllCops:
8
8
  # RuboCop enforces rules depending on the oldest version of Ruby which
9
9
  # your project supports:
10
10
  TargetRubyVersion: 2.7
11
+ Exclude:
12
+ - bin/create-release
13
+ - vendor/**/*
11
14
 
12
15
  # The default max line length is 80 characters
13
16
  Layout/LineLength:
data/CHANGELOG.md CHANGED
@@ -6,3 +6,26 @@
6
6
  # Change Log
7
7
 
8
8
  The full change log is stored on [this project's GitHub releases page](https://github.com/jcouball/active_model_persistence/releases).
9
+
10
+ ## v0.3.0
11
+
12
+ * 3b93fae List all changes in CHANGELOG.md instead of just a link (#10)
13
+ * 76d8a0b Backfill previous released in the CHANGELOG.md (#9)
14
+ * 67da6ed Add experimental script to create release (#8)
15
+ * 7da26b6 Refactor #save and #save! (#7)
16
+ * 81e3730 Add the #update and #update! methods (#6)
17
+ * b4c5f20 Add .create! (#5)
18
+ * 436ee3d Use "module ClassMethods" instead of "do class_methods" for ActiveConcern (#4)
19
+ * 1be8b11 Run yard:audit before yard:coverage (#3)
20
+
21
+ See https://github.com/ruby-git/ruby-git/releases/tag/v0.3.0
22
+
23
+ ## v0.2.0
24
+
25
+ [Full Changelog](https://github.com/jcouball/active_model_persistence/compare/v0.1.0...v0.2.0)
26
+
27
+ * ca12c6d Implement save! (#1)
28
+
29
+ ## v0.1.0
30
+
31
+ Initial release
data/Rakefile CHANGED
@@ -3,7 +3,7 @@
3
3
  # The default task
4
4
 
5
5
  desc 'Run the same tasks that the CI build will run'
6
- task default: %w[spec rubocop yard yard:coverage yard:audit bundle:audit build]
6
+ task default: %w[spec rubocop yard yard:audit yard:coverage bundle:audit build]
7
7
 
8
8
  # Bundler Audit
9
9
 
@@ -0,0 +1,537 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run this script while in the root directory of the project with the default
5
+ # branch checked out.
6
+
7
+ require 'bump'
8
+ require 'English'
9
+ require 'fileutils'
10
+ require 'optparse'
11
+ require 'tempfile'
12
+
13
+ # TODO: Right now the default branch and the remote name are hard coded
14
+
15
+ class Options
16
+ attr_accessor :current_version, :next_version, :tag, :current_tag, :next_tag, :branch, :quiet
17
+
18
+ def initialize
19
+ yield self if block_given?
20
+ end
21
+
22
+ def release_type
23
+ raise 'release_type not set' if @release_type.nil?
24
+
25
+ @release_type
26
+ end
27
+
28
+ VALID_RELEASE_TYPES = %w[major minor patch].freeze
29
+
30
+ def release_type=(release_type)
31
+ unless VALID_RELEASE_TYPES.include?(release_type)
32
+ raise "release_type must be one of: #{VALID_RELEASE_TYPES.join(', ')}"
33
+ end
34
+
35
+ @release_type = release_type
36
+ end
37
+
38
+ def quiet
39
+ @quiet = false unless instance_variable_defined?(:@quiet)
40
+ @quiet
41
+ end
42
+
43
+ def current_version
44
+ @current_version ||= Bump::Bump.current
45
+ end
46
+
47
+ def next_version
48
+ current_version # Save the current version before bumping
49
+ @next_version ||= Bump::Bump.next_version(release_type)
50
+ end
51
+
52
+ def tag
53
+ @tag ||= "v#{next_version}"
54
+ end
55
+
56
+ def current_tag
57
+ @current_tag ||= "v#{current_version}"
58
+ end
59
+
60
+ def next_tag
61
+ tag
62
+ end
63
+
64
+ def branch
65
+ @branch ||= "release-#{tag}"
66
+ end
67
+
68
+ def default_branch
69
+ @default_branch ||= `git remote show '#{remote}'`.match(/HEAD branch: (.*?)$/)[1]
70
+ end
71
+
72
+ def remote
73
+ @remote ||= 'origin'
74
+ end
75
+
76
+ def to_s
77
+ <<~OUTPUT
78
+ release_type='#{release_type}'
79
+ current_version='#{current_version}'
80
+ next_version='#{next_version}'
81
+ tag='#{tag}'
82
+ branch='#{branch}'
83
+ quiet=#{quiet}
84
+ OUTPUT
85
+ end
86
+ end
87
+
88
+ class CommandLineParser
89
+ attr_reader :options
90
+
91
+ def initialize
92
+ @option_parser = OptionParser.new
93
+ define_options
94
+ @options = Options.new
95
+ end
96
+
97
+ def parse(args)
98
+ option_parser.parse!(remaining_args = args.dup)
99
+ parse_remaining_args(remaining_args)
100
+ # puts options unless options.quiet
101
+ options
102
+ end
103
+
104
+ private
105
+
106
+ attr_reader :option_parser
107
+
108
+ def parse_remaining_args(remaining_args)
109
+ error_with_usage('No release type specified') if remaining_args.empty?
110
+ @options.release_type = remaining_args.shift || nil
111
+ error_with_usage('Too many args') unless remaining_args.empty?
112
+ end
113
+
114
+ def error_with_usage(message)
115
+ warn <<~MESSAGE
116
+ ERROR: #{message}
117
+ #{option_parser}
118
+ MESSAGE
119
+ exit 1
120
+ end
121
+
122
+ def define_options
123
+ option_parser.banner = 'Usage: create_release --help | release-type'
124
+ option_parser.separator ''
125
+ option_parser.separator 'Options:'
126
+
127
+ define_quiet_option
128
+ define_help_option
129
+ end
130
+
131
+ def define_quiet_option
132
+ option_parser.on('-q', '--[no-]quiet', 'Do not show output') do |quiet|
133
+ options.quiet = quiet
134
+ end
135
+ end
136
+
137
+ def define_help_option
138
+ option_parser.on_tail('-h', '--help', 'Show this message') do
139
+ puts option_parser
140
+ exit 0
141
+ end
142
+ end
143
+ end
144
+
145
+ class ReleaseAssertions
146
+ attr_reader :options
147
+
148
+ def initialize(options)
149
+ @options = options
150
+ end
151
+
152
+ def make_assertions
153
+ bundle_is_up_to_date
154
+ in_git_repo
155
+ in_repo_toplevel_directory
156
+ on_default_branch
157
+ no_uncommitted_changes
158
+ local_and_remote_on_same_commit
159
+ tag_does_not_exist
160
+ branch_does_not_exist
161
+ docker_is_running
162
+ changelog_docker_container_exists
163
+ gh_command_exists
164
+ end
165
+
166
+ private
167
+
168
+ def gh_command_exists
169
+ print 'Checking that the gh command exists...'
170
+ `which gh > /dev/null 2>&1`
171
+ if $CHILD_STATUS.success?
172
+ puts 'OK'
173
+ else
174
+ error 'The gh command was not found'
175
+ end
176
+ end
177
+
178
+ def docker_is_running
179
+ print 'Checking that docker is installed and running...'
180
+ `docker info > /dev/null 2>&1`
181
+ if $CHILD_STATUS.success?
182
+ puts 'OK'
183
+ else
184
+ error 'Docker is not installed or not running'
185
+ end
186
+ end
187
+
188
+ def changelog_docker_container_exists
189
+ print 'Checking that the changelog docker container exists (might take time to build)...'
190
+ `docker build --file Dockerfile.changelog-rs --tag changelog-rs . 1>/dev/null`
191
+ if $CHILD_STATUS.success?
192
+ puts 'OK'
193
+ else
194
+ error 'Failed to build the changelog-rs docker container'
195
+ end
196
+ end
197
+
198
+ def bundle_is_up_to_date
199
+ print 'Checking that the bundle is up to date...'
200
+ if File.exist?('Gemfile.lock')
201
+ print 'Running bundle update...'
202
+ `bundle update --quiet`
203
+ if $CHILD_STATUS.success?
204
+ puts 'OK'
205
+ else
206
+ error 'bundle update failed'
207
+ end
208
+ else
209
+ print 'Running bundle install...'
210
+ `bundle install --quiet`
211
+ if $CHILD_STATUS.success?
212
+ puts 'OK'
213
+ else
214
+ error 'bundle install failed'
215
+ end
216
+ end
217
+ end
218
+
219
+ def in_git_repo
220
+ print 'Checking that you are in a git repo...'
221
+ `git rev-parse --is-inside-work-tree --quiet > /dev/null 2>&1`
222
+ if $CHILD_STATUS.success?
223
+ puts 'OK'
224
+ else
225
+ error 'You are not in a git repo'
226
+ end
227
+ end
228
+
229
+ def in_repo_toplevel_directory
230
+ print "Checking that you are in the repo's toplevel directory..."
231
+ toplevel_directory = `git rev-parse --show-toplevel`.chomp
232
+ if toplevel_directory == FileUtils.pwd
233
+ puts 'OK'
234
+ else
235
+ error "You are not in the repo's toplevel directory"
236
+ end
237
+ end
238
+
239
+ def on_default_branch
240
+ print 'Checking that you are on the default branch...'
241
+ current_branch = `git branch --show-current`.chomp
242
+ if current_branch == options.default_branch
243
+ puts 'OK'
244
+ else
245
+ error "You are not on the default branch '#{default_branch}'"
246
+ end
247
+ end
248
+
249
+ def no_uncommitted_changes
250
+ print 'Checking that there are no uncommitted changes...'
251
+ if `git status --porcelain | wc -l`.to_i.zero?
252
+ puts 'OK'
253
+ else
254
+ error 'There are uncommitted changes'
255
+ end
256
+ end
257
+
258
+ def no_staged_changes
259
+ print 'Checking that there are no staged changes...'
260
+ if `git diff --staged --name-only | wc -l`.to_i.zero?
261
+ puts 'OK'
262
+ else
263
+ error 'There are staged changes'
264
+ end
265
+ end
266
+
267
+ def local_and_remote_on_same_commit
268
+ print 'Checking that local and remote are on the same commit...'
269
+ local_commit = `git rev-parse HEAD`.chomp
270
+ remote_commit = `git ls-remote '#{options.remote}' '#{options.default_branch}' | cut -f 1`.chomp
271
+ if local_commit == remote_commit
272
+ puts 'OK'
273
+ else
274
+ error 'Local and remote are not on the same commit'
275
+ end
276
+ end
277
+
278
+ def local_tag_does_not_exist
279
+ print "Checking that local tag '#{options.tag}' does not exist..."
280
+
281
+ tags = `git tag --list "#{options.tag}"`.chomp
282
+ error 'Could not list tags' unless $CHILD_STATUS.success?
283
+
284
+ if tags.split.empty?
285
+ puts 'OK'
286
+ else
287
+ error "'#{options.tag}' already exists"
288
+ end
289
+ end
290
+
291
+ def remote_tag_does_not_exist
292
+ print "Checking that the remote tag '#{options.tag}' does not exist..."
293
+ `git ls-remote --tags --exit-code '#{options.remote}' #{options.tag} >/dev/null 2>&1`
294
+ if $CHILD_STATUS.success?
295
+ error "'#{options.tag}' already exists"
296
+ else
297
+ puts 'OK'
298
+ end
299
+ end
300
+
301
+ def tag_does_not_exist
302
+ local_tag_does_not_exist
303
+ remote_tag_does_not_exist
304
+ end
305
+
306
+ def local_branch_does_not_exist
307
+ print "Checking that local branch '#{options.branch}' does not exist..."
308
+
309
+ if `git branch --list "#{options.branch}" | wc -l`.to_i.zero?
310
+ puts 'OK'
311
+ else
312
+ error "'#{options.branch}' already exists."
313
+ end
314
+ end
315
+
316
+ def remote_branch_does_not_exist
317
+ print "Checking that the remote branch '#{options.branch}' does not exist..."
318
+ `git ls-remote --heads --exit-code '#{options.remote}' '#{options.branch}' >/dev/null 2>&1`
319
+ if $CHILD_STATUS.success?
320
+ error "'#{options.branch}' already exists"
321
+ else
322
+ puts 'OK'
323
+ end
324
+ end
325
+
326
+ def branch_does_not_exist
327
+ local_branch_does_not_exist
328
+ remote_branch_does_not_exist
329
+ end
330
+
331
+ def print(*args)
332
+ super unless options.quiet
333
+ end
334
+
335
+ def puts(*args)
336
+ super unless options.quiet
337
+ end
338
+
339
+ def error(message)
340
+ warn "ERROR: #{message}"
341
+ exit 1
342
+ end
343
+ end
344
+
345
+ class ReleaseCreator
346
+ attr_reader :options
347
+
348
+ def initialize(options)
349
+ @options = options
350
+ end
351
+
352
+ def create_release
353
+ create_branch
354
+ update_changelog
355
+ update_version
356
+ make_release_commit
357
+ create_tag
358
+ push_release_commit_and_tag
359
+ create_github_release
360
+ create_release_pull_request
361
+ end
362
+
363
+ private
364
+
365
+ def create_branch
366
+ print "Creating branch '#{options.branch}'..."
367
+ `git checkout -b "#{options.branch}" > /dev/null 2>&1`
368
+ if $CHILD_STATUS.success?
369
+ puts 'OK'
370
+ else
371
+ error "Could not create branch '#{options.branch}'" unless $CHILD_STATUS.success?
372
+ end
373
+ end
374
+
375
+ def update_changelog
376
+ print 'Updating CHANGELOG.md...'
377
+ changelog_lines = File.readlines('CHANGELOG.md')
378
+ first_entry = changelog_lines.index { |e| e =~ /^## / }
379
+ error 'Could not find changelog insertion point' unless first_entry
380
+ FileUtils.rm('CHANGELOG.md')
381
+ File.write('CHANGELOG.md', <<~CHANGELOG.chomp)
382
+ #{changelog_lines[0..first_entry - 1].join.chomp}
383
+ ## #{options.tag}
384
+
385
+ #{changelog(to: 'HEAD').lines[2..].join}
386
+
387
+ See https://github.com/ruby-git/ruby-git/releases/tag/#{options.tag}
388
+
389
+ #{changelog_lines[first_entry..].join}
390
+ CHANGELOG
391
+ `git add CHANGELOG.md`
392
+ if $CHILD_STATUS.success?
393
+ puts 'OK'
394
+ else
395
+ error 'Could not stage changes to CHANGELOG.md'
396
+ end
397
+ end
398
+
399
+ def update_version
400
+ print 'Updating version...'
401
+ message, status = Bump::Bump.run(options.release_type, commit: false)
402
+ error 'Could not bump version' unless status.zero?
403
+ version_file = Bump::Bump.file
404
+ `git add '#{version_file}'`
405
+ if $CHILD_STATUS.success?
406
+ puts 'OK'
407
+ else
408
+ error "Could not stage changes to the version file '#{version_file}'"
409
+ end
410
+ end
411
+
412
+ def make_release_commit
413
+ print 'Making release commit...'
414
+ `git commit -s -m 'Release #{options.tag}'`
415
+ error 'Could not make release commit' unless $CHILD_STATUS.success?
416
+ end
417
+
418
+ def create_tag
419
+ print "Creating tag '#{options.tag}'..."
420
+ `git tag '#{options.tag}'`
421
+ if $CHILD_STATUS.success?
422
+ puts 'OK'
423
+ else
424
+ error "Could not create tag '#{options.tag}'"
425
+ end
426
+ end
427
+
428
+ def push_release_commit_and_tag
429
+ print "Pushing branch '#{options.branch}' to remote..."
430
+ `git push --tags --set-upstream '#{options.remote}' '#{options.branch}' > /dev/null 2>&1`
431
+ if $CHILD_STATUS.success?
432
+ puts 'OK'
433
+ else
434
+ error 'Could not push release commit'
435
+ end
436
+ end
437
+
438
+ def changelog(from: options.current_tag, to: options.next_tag)
439
+ @changelog ||= begin
440
+ print "Generating changelog from #{from} to #{to}..."
441
+ pwd = FileUtils.pwd
442
+ command = "docker run --rm --volume '#{pwd}:/worktree' changelog-rs '#{from}' '#{to}'"
443
+ changelog = `#{command}`.chomp
444
+ if $CHILD_STATUS.success?
445
+ puts 'OK'
446
+ changelog.rstrip.lines[1..].join
447
+ else
448
+ error 'Could not generate the changelog'
449
+ end
450
+ end
451
+ end
452
+
453
+ def create_github_release
454
+ Tempfile.create do |f|
455
+ f.write changelog
456
+ f.close
457
+
458
+ print "Creating GitHub release '#{options.tag}'..."
459
+ tag = options.tag
460
+ `gh release create #{tag} --title 'Release #{tag}' --notes-file '#{f.path}' --target #{options.default_branch}`
461
+ if $CHILD_STATUS.success?
462
+ puts 'OK'
463
+ else
464
+ error 'Could not create release'
465
+ end
466
+ end
467
+ end
468
+
469
+ def create_release_pull_request
470
+ Tempfile.create do |f|
471
+ f.write <<~PR
472
+ ### Your checklist for this pull request
473
+ 🚨Please review the [guidelines for contributing](https://github.com/ruby-git/ruby-git/blob/#{options.default_branch}/CONTRIBUTING.md) to this repository.
474
+
475
+ - [X] Ensure all commits include DCO sign-off.
476
+ - [X] Ensure that your contributions pass unit testing.
477
+ - [X] Ensure that your contributions contain documentation if applicable.
478
+
479
+ ### Description
480
+ #{changelog}
481
+
482
+ ### Next Steps
483
+
484
+ DO NOT MERGE THIS PULL REQUEST VIA THE GITHUB UI
485
+
486
+ * Get someone to review and approve the release pull request
487
+ * Merge the pull request manually from the command line with the following commands:
488
+
489
+ ```Ruby
490
+ git checkout #{options.default_branch}
491
+ git merge --ff-only #{options.branch}
492
+ git push
493
+ ````
494
+ PR
495
+ f.close
496
+
497
+ print 'Creating GitHub pull request...'
498
+ `gh pr create --title 'Release #{options.tag}' --body-file '#{f.path}' --base '#{options.default_branch}'`
499
+ if $CHILD_STATUS.success?
500
+ puts 'OK'
501
+ else
502
+ error 'Could not create release pull request'
503
+ end
504
+ end
505
+ end
506
+
507
+ def error(message)
508
+ warn "ERROR: #{message}"
509
+ exit 1
510
+ end
511
+
512
+ def print(*args)
513
+ super unless options.quiet
514
+ end
515
+
516
+ def puts(*args)
517
+ super unless options.quiet
518
+ end
519
+ end
520
+
521
+ options = CommandLineParser.new.parse(ARGV)
522
+ ReleaseAssertions.new(options).make_assertions
523
+ puts unless options.quiet
524
+ ReleaseCreator.new(options).create_release
525
+
526
+ puts <<~MESSAGE unless options.quiet
527
+ Release '#{options.tag}' created successfully
528
+ See the release notes at https://github.com/jcouball/release_testing/releases
529
+
530
+ Next steps:
531
+ * Get someone to review and approve the release pull request
532
+ * Merge the pull request manually from the command line with the following commands:
533
+
534
+ git checkout #{options.default_branch}
535
+ git merge --ff-only #{options.branch}
536
+ git push
537
+ MESSAGE
@@ -56,7 +56,10 @@ module ActiveModelPersistence
56
56
  include ActiveModel::Attributes
57
57
  include ActiveModelPersistence::PrimaryKey
58
58
 
59
- class_methods do
59
+ # When this module is included in another class, ActiveSupport::Concern will
60
+ # make these class methods on that class.
61
+ #
62
+ module ClassMethods
60
63
  # Returns a hash of indexes for the model keyed by name
61
64
  #
62
65
  # @example
@@ -148,6 +151,8 @@ module ActiveModelPersistence
148
151
 
149
152
  # Defines the default options for a new ActiveModelPersistence::Index
150
153
  #
154
+ # @return [Hash] the default options
155
+ #
151
156
  # @api private
152
157
  #
153
158
  def default_index_options(index_name)
@@ -53,11 +53,16 @@ module ActiveModelPersistence
53
53
  include ActiveModelPersistence::PrimaryKey
54
54
  include ActiveModelPersistence::PrimaryKeyIndex
55
55
 
56
- class_methods do
57
- # Creates a new model object in to the object store
56
+ # When this module is included in another class, ActiveSupport::Concern will
57
+ # make these class methods on that class.
58
+ #
59
+ module ClassMethods
60
+ # Creates a new model object in to the object store and returns it
58
61
  #
59
62
  # Create a new model object passing `attributes` and `block` to `.new` and then calls `#save`.
60
63
  #
64
+ # The new model object is returned even if it could not be saved to the object store.
65
+ #
61
66
  # @param attributes [Hash, Array<Hash>] attributes
62
67
  #
63
68
  # The attributes to set on the model object. These are passed to the model's `.new` method.
@@ -94,6 +99,50 @@ module ActiveModelPersistence
94
99
  end
95
100
  end
96
101
 
102
+ # Creates a new model object in to the object store
103
+ #
104
+ # Raises an error if the object could not be created.
105
+ #
106
+ # Create a new model object passing `attributes` and `block` to `.new` and then calls `#save!`.
107
+ #
108
+ # @param attributes [Hash, Array<Hash>] attributes
109
+ #
110
+ # The attributes to set on the model object. These are passed to the model's `.new` method.
111
+ #
112
+ # Multiple model objects can be created by passing an array of attribute Hashes.
113
+ #
114
+ # @param block [Proc] options
115
+ #
116
+ # The block to pass to the model's `.new` method.
117
+ #
118
+ # @example
119
+ # m = ModelExample.new(id: 1, name: 'James')
120
+ # m.id #=> 1
121
+ # m.name #=> 'James'
122
+ #
123
+ # @example Multiple model objects can be created
124
+ # array_of_attributes = [
125
+ # { id: 1, name: 'James' },
126
+ # { id: 2, name: 'Frank' }
127
+ # ]
128
+ # objects = ModelExample.create(array_of_attributes)
129
+ # objects.class #=> Array
130
+ # objects.size #=> 2
131
+ # objects.first.id #=> 1
132
+ # objects.map(&:name) #=> ['James', 'Frank']
133
+ #
134
+ # @return [Object, Array<Object>] the model object or array of model objects created
135
+ #
136
+ # @raise [ModelError] if the model object could not be created
137
+ #
138
+ def create!(attributes = nil, &block)
139
+ if attributes.is_a?(Array)
140
+ attributes.collect { |attr| create!(attr, &block) }
141
+ else
142
+ new(attributes, &block).tap(&:save!)
143
+ end
144
+ end
145
+
97
146
  # Return all model objects that have been saved to the object store
98
147
  #
99
148
  # @example
@@ -128,7 +177,7 @@ module ActiveModelPersistence
128
177
  object_array.size
129
178
  end
130
179
 
131
- alias_method(:size, :count)
180
+ alias size count
132
181
 
133
182
  # Removes all model objects from the object store
134
183
  #
@@ -258,7 +307,7 @@ module ActiveModelPersistence
258
307
  # object.save
259
308
  # ModelExample.all.count #=> 1
260
309
  #
261
- # @param _options [Hash] save options (currently unused)
310
+ # @param options [Hash] save options (currently unused)
262
311
  # @param block [Proc] a block to call after the save
263
312
  #
264
313
  # @yield [self] a block to call after the save
@@ -267,12 +316,12 @@ module ActiveModelPersistence
267
316
  #
268
317
  # @return [Boolean] true if the model object was saved
269
318
  #
270
- def save(**_options, &block)
271
- return false if destroyed?
272
-
273
- result = new_record? ? _create(&block) : _update(&block)
274
- update_indexes
275
- result != false
319
+ def save(**options, &block)
320
+ save!(**options, &block)
321
+ rescue ModelError
322
+ false
323
+ else
324
+ true
276
325
  end
277
326
 
278
327
  # Calls #save and raises an error if #save returns false
@@ -293,8 +342,13 @@ module ActiveModelPersistence
293
342
  #
294
343
  # @return [Boolean] returns true or raises an error
295
344
  #
296
- def save!(**options, &block)
297
- save(**options, &block) || raise(ObjectNotSavedError.new('Failed to save the object', self))
345
+ def save!(**_options, &block)
346
+ raise ObjectDestroyedError if destroyed?
347
+ raise ObjectNotValidError unless valid?
348
+
349
+ new_record? ? _create(&block) : _update(&block)
350
+ update_indexes
351
+ true
298
352
  end
299
353
 
300
354
  # Deletes the object from the object store
@@ -328,10 +382,57 @@ module ActiveModelPersistence
328
382
  attributes == other.attributes
329
383
  end
330
384
 
385
+ # Updates the attributes of the model and saves it
386
+ #
387
+ # The attributes are updated from the passed in hash. If the object is invalid,
388
+ # the save will fail and false will be returned.
389
+ #
390
+ # @example
391
+ # object = ModelExample.create(id: 1, name: 'James')
392
+ # object.update(name: 'Frank')
393
+ # object.find(1).name #=> 'Frank'
394
+ #
395
+ # @param attributes [Hash] the attributes to update
396
+ #
397
+ # @return [Boolean] true if the model object was saved, otherwise false
398
+ #
399
+ def update(attributes)
400
+ update!(attributes)
401
+ rescue ModelError
402
+ false
403
+ else
404
+ true
405
+ end
406
+
407
+ # Updates just like #update but an exception is raised of the model is invalid
408
+ #
409
+ # @example
410
+ # object = ModelExample.create(id: 1, name: 'James')
411
+ # object.update!(id: nil) #=> raises ObjectNotSavedError
412
+ #
413
+ # @param attributes [Hash] the attributes to update
414
+ #
415
+ # @return [Boolean] true if the model object was saved, otherwise an error is raised
416
+ #
417
+ # @raise [ObjectNotValidError] if the model object is invalid
418
+ # @raise [ObjectDestroyedError] if the model object was previously destroyed
419
+ #
420
+ def update!(attributes)
421
+ raise ObjectDestroyedError if destroyed?
422
+
423
+ assign_attributes(attributes)
424
+ save!
425
+ end
426
+
331
427
  private
332
428
 
333
429
  # Creates a record with values matching those of the instance attributes
334
430
  # and returns its id.
431
+ #
432
+ # @return [Object] the primary_key of the created object
433
+ #
434
+ # @api private
435
+ #
335
436
  def _create
336
437
  return false unless primary_key?
337
438
  raise UniqueContraintError if primary_key_index.include?(primary_key)
@@ -345,10 +446,18 @@ module ActiveModelPersistence
345
446
  primary_key
346
447
  end
347
448
 
449
+ # Updates an object that is already in the object store
450
+ #
451
+ # @return [Boolean] true if the object was update successfully, otherwise raises a ModelError
452
+ #
453
+ # @api private
454
+ #
348
455
  def _update
349
456
  raise RecordNotFound unless primary_key_index.include?(primary_key)
350
457
 
351
458
  yield(self) if block_given?
459
+
460
+ true
352
461
  end
353
462
  end
354
463
  # rubocop:enable Metrics/BlockLength
@@ -38,7 +38,10 @@ module ActiveModelPersistence
38
38
  include ActiveModel::Model
39
39
  include ActiveModel::Attributes
40
40
 
41
- class_methods do
41
+ # When this module is included in another class, ActiveSupport::Concern will
42
+ # make these class methods on that class.
43
+ #
44
+ module ClassMethods
42
45
  # Identifies the attribute that the `primary_key` accessor maps to
43
46
  #
44
47
  # The primary key is 'id' by default.
@@ -16,7 +16,10 @@ module ActiveModelPersistence
16
16
  include ActiveModelPersistence::PrimaryKey
17
17
  include ActiveModelPersistence::Indexable
18
18
 
19
- class_methods do
19
+ # When this module is included in another class, ActiveSupport::Concern will
20
+ # make these class methods on that class.
21
+ #
22
+ module ClassMethods
20
23
  # Finds an object in the :primary_key index whose primary matches the given value
21
24
  #
22
25
  # @example
@@ -37,10 +40,10 @@ module ActiveModelPersistence
37
40
  find_by_primary_key(primary_key_value).first
38
41
  end
39
42
 
40
- private
41
-
42
43
  # Create the primary key index
43
44
  #
45
+ # @return [void]
46
+ #
44
47
  # @api private
45
48
  #
46
49
  def self.extended(base)
@@ -49,6 +52,12 @@ module ActiveModelPersistence
49
52
  end
50
53
 
51
54
  included do
55
+ # Returns the primary key index
56
+ #
57
+ # @return [ActiveModelPersistence::Index]
58
+ #
59
+ # @api private
60
+ #
52
61
  def primary_key_index
53
62
  self.class.indexes[:primary_key]
54
63
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ActiveModelPersistence
4
4
  # The version of the active_model_persistence gem
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
@@ -18,31 +18,10 @@ module ActiveModelPersistence
18
18
  class UniqueConstraintError < ModelError; end
19
19
 
20
20
  # Raised when trying to save an invalid object
21
- class ObjectNotSavedError < ModelError
22
- # The object that was not saved
23
- #
24
- # @example
25
- # object = Object.new
26
- # error = ObjectNotSavedError.new('Invalid object', object)
27
- # error.object == object #=> true
28
- #
29
- # @return [Object] The object that was not saved
30
- #
31
- attr_reader :object
21
+ class ObjectNotValidError < ModelError; end
32
22
 
33
- # Create a new error
34
- #
35
- # @example
36
- # ObjectNotSavedError.new('Invalid object', self)
37
- #
38
- # @param message [String] The error message
39
- # @param object [Object] The object that was not saved
40
- #
41
- def initialize(message = nil, object = nil)
42
- @object = object
43
- super(message)
44
- end
45
- end
23
+ # Raised when trying to save! or update! an object that has already been destroyed
24
+ class ObjectDestroyedError < ModelError; end
46
25
  end
47
26
 
48
27
  require_relative 'active_model_persistence/index'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_model_persistence
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Couball
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-19 00:00:00.000000000 Z
11
+ date: 2022-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -197,6 +197,7 @@ files:
197
197
  - Rakefile
198
198
  - active_model_persistence.gemspec
199
199
  - bin/console
200
+ - bin/create-release
200
201
  - bin/setup
201
202
  - lib/active_model_persistence.rb
202
203
  - lib/active_model_persistence/index.rb