active_model_persistence 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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