├── .gitignore ├── .ruby-version ├── .travis.yml ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── Procfile ├── README.md ├── Rakefile ├── bin └── rspec ├── gemfiles └── mongoid_5.gemfile ├── lib ├── autoinc.rb └── autoinc │ ├── incrementor.rb │ └── version.rb ├── mongoid-autoinc.gemspec └── spec ├── autoinc_spec.rb ├── incrementor_spec.rb ├── models.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .DS_Store 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.2.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - gem update --system 3 | - gem update bundler 4 | 5 | services: mongodb 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Third-party patches are essential for keeping mongoid-autoinc great. 4 | We want to keep it as easy as possible to contribute changes that get things working in your environment. 5 | There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. 6 | 7 | Follow the styleguide when changing code: 8 | [80beans Styleguide](https://gist.github.com/suweller/b896eb9e66fc6ab3640d) 9 | 10 | ## Making Changes 11 | 12 | * Create an issue so we can discuss the patch and prevent duplication of effort. 13 | * Create a topic branch from where you want to base your work. 14 | * This is usually the `master` branch. 15 | * Only target release branches if you are certain your fix must be on that 16 | branch. 17 | * To quickly create a topic branch based on master; `git branch 18 | my_contribution master` then checkout the new branch with `git 19 | checkout my_contribution`. Please avoid working directly on the 20 | `master` branch. 21 | * Make commits of logical units. 22 | * Check for unnecessary whitespace with `git diff --check` before committing. 23 | * Make sure your commit messages are in the proper format. 24 | 25 | ``` 26 | Clarify CONTRIBUTING with an example 27 | 28 | Without this patch applied the example commit message in the CONTRIBUTING 29 | document is not a concrete example. This is a problem because the 30 | contributor is left to imagine what the commit message should look like 31 | based on a description rather than an example. This patch fixes the 32 | problem by making the example concrete and imperative. 33 | 34 | The first line is a real life imperative statement with a ticket number 35 | from our issue tracker. The body describes the behavior without the patch, 36 | why this is a problem, and how the patch fixes the problem when applied. 37 | 38 | Closes issue #11 39 | ``` 40 | 41 | * Reference the issue created earlier like in the above example commit. 42 | * Make sure you have added the necessary tests for your changes. 43 | * Run _all_ the tests to assure nothing else was accidentally broken. 44 | * Create a pull-request. 45 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 80beans 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. 21 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | MongoDB: mongod run --config /opt/homebrew/etc/mongod.conf 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mongoid-autoinc 2 | 3 | A mongoid plugin to add auto incrementing fields to your documents. 4 | 5 | [![Inline docs]( 6 | http://inch-ci.org/github/suweller/mongoid-autoinc.svg?branch=master&style=flat 7 | )](http://inch-ci.org/github/suweller/mongoid-autoinc) 8 | [![Code Climate]( 9 | http://img.shields.io/codeclimate/github/suweller/mongoid-autoinc.svg?style=flat 10 | )](https://codeclimate.com/github/suweller/mongoid-autoinc) 11 | [![Build Status]( 12 | http://img.shields.io/travis/suweller/mongoid-autoinc.svg?style=flat 13 | )](https://travis-ci.org/suweller/mongoid-autoinc) 14 | 15 | ## Installation 16 | 17 | in gemfile: 18 | 19 | ``` ruby 20 | gem 'mongoid-autoinc' 21 | ``` 22 | 23 | in class: 24 | 25 | ``` ruby 26 | require 'autoinc' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ``` ruby 32 | # app/models/user.rb 33 | class User 34 | include Mongoid::Document 35 | include Mongoid::Autoinc 36 | field :name 37 | field :number, type: Integer 38 | 39 | increments :number 40 | end 41 | 42 | user = User.create(name: 'Dr. Percival "Perry" Ulysses Cox') 43 | user.id # BSON::ObjectId('4d1d150d30f2246bc6000001') 44 | user.number # 1 45 | 46 | another_user = User.create(name: 'Bob Kelso') 47 | another_user.number # 2 48 | ``` 49 | 50 | ### Scopes 51 | 52 | You can scope on document fields. For example: 53 | 54 | ``` ruby 55 | class PatientFile 56 | include Mongoid::Document 57 | include Mongoid::Autoinc 58 | 59 | field :name 60 | field :number, type: Integer 61 | 62 | increments :number, scope: :patient_id 63 | 64 | belongs_to :patient 65 | 66 | end 67 | ``` 68 | 69 | Scope can also be a Proc: 70 | 71 | ``` ruby 72 | increments :number, scope: -> { patient.name } 73 | ``` 74 | 75 | ### Custom Increment Trigger 76 | 77 | You can trigger the assignment of an increment field manually by passing: 78 | `auto: false` to the increment field. 79 | This allows for more flexible assignment of your increment number: 80 | 81 | ``` ruby 82 | class Intern 83 | include Mongoid::Document 84 | include Mongoid::Autoinc 85 | 86 | field :name 87 | field :number 88 | 89 | increments :number, auto: false 90 | 91 | after_save :assign_number_to_jd 92 | 93 | protected 94 | 95 | def assign_number_to_jd 96 | assign!(:number) if number.blank? && name == 'J.D.' 97 | end 98 | 99 | end 100 | ``` 101 | 102 | ### Custom Model Name 103 | 104 | You can override the model name used to generate the autoincrement keys. This can be useful 105 | when working with subclasses or namespaces. 106 | 107 | ``` ruby 108 | class Intern 109 | include Mongoid::Document 110 | include Mongoid::Autoinc 111 | 112 | field :name 113 | field :number 114 | 115 | increments :number, model_name => :foo 116 | end 117 | ``` 118 | 119 | ### Seeds 120 | 121 | You can use a seed to start the incrementing field at a given value. The first 122 | document created will start at 'seed + 1'. 123 | 124 | ``` ruby 125 | class Vehicle 126 | include Mongoid::Document 127 | include Mongoid::Autoinc 128 | 129 | field :model 130 | field :vin 131 | 132 | increments :vin, seed: 1000 133 | 134 | end 135 | 136 | car = Vehicle.new(model: "Coupe") 137 | car.vin # 1001 138 | ``` 139 | 140 | ### Step 141 | 142 | The step option can be used to specify the amount to increment the field every 143 | time a new document is created. If no step is specified, it will increment by 144 | 1. 145 | 146 | ``` ruby 147 | class Ticket 148 | include Mongoid::Document 149 | include Mongoid::Autoinc 150 | 151 | field :number 152 | 153 | increments :number, step: 5 154 | 155 | end 156 | ``` 157 | ``` ruby 158 | first_ticket = Ticket.new 159 | first_ticket.number # 5 160 | second_ticket = Ticket.new 161 | second_ticket.number # 10 162 | ``` 163 | 164 | The step option can also be a Proc: 165 | 166 | ``` ruby 167 | increments :number, step: -> { 1 + rand(10) } 168 | ``` 169 | 170 | ### Development 171 | 172 | ``` 173 | $ gem install bundler (if you don't have it) 174 | $ bundle install 175 | $ bundle exec spec 176 | ``` 177 | 178 | ## Contributing 179 | 180 | * Fork and create a topic branch. 181 | * Follow the 182 | [80beans styleguide](https://gist.github.com/b896eb9e66fc6ab3640d). 183 | Basically the [rubystyleguide](https://github.com/bbatsov/ruby-style-guide/) 184 | with some minor changes. 185 | * Submit a pull request 186 | 187 | ## Contributions 188 | 189 | Thanks to Johnny Shields (@johnnyshields) for implementing proc support to scopes 190 | And to Marcus Gartner (@mgartner) for implementing the seed functionality 191 | 192 | Kris Martin (@krismartin) and Johnny Shields (@johnnyshields) for adding the 193 | overwritten model name feature 194 | 195 | ## Copyright 196 | 197 | See LICENSE for details 198 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | task :default do 4 | require 'rspec' 5 | require File.expand_path('../spec/spec_helper', __FILE__) 6 | system 'bundle exec rspec spec' 7 | end 8 | 9 | task :publish do 10 | NAME = 'mongoid-autoinc' 11 | VERSION_FILE = 'lib/autoinc/version.rb' 12 | 13 | def build_and_push_gem 14 | puts '# Building gem' 15 | puts `gem build #{NAME}.gemspec` 16 | puts '# Publishing Gem' 17 | puts `gem push #{NAME}-#{gem_version}.gem` 18 | end 19 | 20 | def create_and_push_tag 21 | begin 22 | puts `git fetch tags` 23 | puts `git commit -am 'Bump to #{version} [ci skip]'` 24 | puts "# Creating tag #{version}" 25 | puts `git tag #{version}` 26 | puts `git push origin #{version}` 27 | puts `git push origin master` 28 | rescue 29 | raise "Tag: '#{version}' already exists" 30 | end 31 | end 32 | 33 | def changes 34 | git_status_to_array(`git status --short --untracked-files`) 35 | end 36 | 37 | def gem_version 38 | Mongoid::Autoinc::VERSION 39 | end 40 | 41 | def version 42 | @version ||= 'v' << gem_version 43 | end 44 | 45 | def git_status_to_array(changes) 46 | changes.split("\n").each { |change| change.gsub!(/^.. /,'') } 47 | end 48 | 49 | raise '$EDITOR should be set' unless ENV['EDITOR'] 50 | raise 'Branch should hold no uncommitted file change)' unless changes.empty? 51 | raise 'Your editor exited with non-zero status' unless system("$EDITOR #{VERSION_FILE}") 52 | raise "Expected change in: #{VERSION_FILE}" unless changes.member?(VERSION_FILE) 53 | 54 | load File.expand_path(VERSION_FILE) 55 | create_and_push_tag 56 | build_and_push_gem 57 | end 58 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("rspec-core", "rspec") 28 | -------------------------------------------------------------------------------- /gemfiles/mongoid_5.gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'mongoid', '~> 5.0' 4 | gem 'rspec' 5 | gem 'activesupport' 6 | gem 'rake' 7 | -------------------------------------------------------------------------------- /lib/autoinc.rb: -------------------------------------------------------------------------------- 1 | require 'autoinc/incrementor' 2 | 3 | # namespace all +Mongoid::Autoinc+ functionality to +Mongoid+ to reduce 4 | # possible clashes with other gems. 5 | module Mongoid 6 | # Include module to allow defining of autoincrementing fields. 7 | # 8 | # @example 9 | # class Invoice 10 | # include Mongoid::Document 11 | # include Mongoid::Autoinc 12 | # 13 | # field :number, type: Integer 14 | # increments :number 15 | # end 16 | module Autoinc 17 | extend ActiveSupport::Concern 18 | 19 | AlreadyAssignedError = Class.new(StandardError) 20 | AutoIncrementsError = Class.new(StandardError) 21 | 22 | included { before_create(:update_auto_increments) } 23 | 24 | # +Mongoid::Autoinc+ class methods to allow for autoincrementing fields. 25 | module ClassMethods 26 | # Returns all incrementing fields of the document 27 | # 28 | # @example 29 | # class Invoice 30 | # include Mongoid::Document 31 | # include Mongoid::Autoinc 32 | # 33 | # field :number, type: Integer 34 | # increments :number 35 | # end 36 | # Invoice.incrementing_fields # => {number: {auto: true}} 37 | # 38 | # @return [ Hash ] +Hash+ with fields and their autoincrement options 39 | def incrementing_fields 40 | if superclass.respond_to?(:incrementing_fields) 41 | @incrementing_fields ||= superclass.incrementing_fields.dup 42 | else 43 | @incrementing_fields ||= {} 44 | end 45 | end 46 | 47 | # Set an autoincrementing field for a +Mongoid::Document+ 48 | # 49 | # @param [ Symbol ] field The name of the field to apply autoincrement to 50 | # @param [ Hash ] options The options to pass to that field 51 | # 52 | # @example 53 | # class Invoice 54 | # include Mongoid::Document 55 | # include Mongoid::Autoinc 56 | # 57 | # field :number, type: Integer 58 | # increments :number 59 | # end 60 | # 61 | # @example 62 | # class User 63 | # include Mongoid::Document 64 | # include Mongoid::Autoinc 65 | # 66 | # field :number, type: Integer 67 | # increments :number, auto: false 68 | # end 69 | def increments(field, options = {}) 70 | incrementing_fields[field] = options.reverse_merge!(auto: true) 71 | attr_protected(field) if respond_to?(:attr_protected) 72 | end 73 | end 74 | 75 | # Manually assign the next number to the passed autoinc field. 76 | # 77 | # @raise [ Mongoid::Autoinc::AutoIncrementsError ] When `auto: true` is set 78 | # in the increments call for `field` 79 | # @raise [ AlreadyAssignedError ] When called more then once. 80 | # 81 | # @return [ Fixnum ] The assigned number 82 | def assign!(field) 83 | options = self.class.incrementing_fields[field] 84 | fail AutoIncrementsError if options[:auto] 85 | fail AlreadyAssignedError if send(field).present? 86 | increment!(field, options) 87 | end 88 | 89 | # Sets autoincrement values for all autoincrement fields. 90 | # 91 | # @return [ true ] 92 | def update_auto_increments 93 | self.class.incrementing_fields.each do |field, options| 94 | increment!(field, options) if options[:auto] 95 | end && true 96 | end 97 | 98 | # Set autoincrement value for the passed autoincrement field, 99 | # using the passed options 100 | # 101 | # @param [ Symbol ] field Field to set the autoincrement value for. 102 | # @param [ Hash ] options Options to pass through to the serializer. 103 | # 104 | # @return [ true ] The value of `write_attribute` 105 | def increment!(field, options) 106 | options = options.dup 107 | model_name = (options.delete(:model_name) || self.class.model_name).to_s 108 | options[:scope] = evaluate_scope(options[:scope]) if options[:scope] 109 | options[:step] = evaluate_step(options[:step]) if options[:step] 110 | write_attribute( 111 | field.to_sym, 112 | Mongoid::Autoinc::Incrementor.new(model_name, field, options).inc 113 | ) 114 | end 115 | 116 | # Asserts the validity of the passed scope 117 | # 118 | # @param [ Object ] scope The +Symbol+ or +Proc+ to evaluate 119 | # 120 | # @raise [ ArgumentError ] When +scope+ is not a +Symbol+ or +Proc+ 121 | # 122 | # @return [ Object ] The scope of the autoincrement call 123 | def evaluate_scope(scope) 124 | return send(scope) if scope.is_a? Symbol 125 | return instance_exec(&scope) if scope.is_a? Proc 126 | fail ArgumentError, 'scope is not a Symbol or a Proc' 127 | end 128 | 129 | # Returns the number to add to the current increment 130 | # 131 | # @param [ Object ] step The +Integer+ to be returned 132 | # or +Proc+ to be evaluated 133 | # 134 | # @raise [ ArgumentError ] When +step+ is not an +Integer+ or +Proc+ 135 | # 136 | # @return [ Integer ] The number to add to the current increment 137 | def evaluate_step(step) 138 | return step if step.is_a? Integer 139 | return evaluate_step_proc(step) if step.is_a? Proc 140 | fail ArgumentError, 'step is not an Integer or a Proc' 141 | end 142 | 143 | # Executes a proc and returns its +Integer+ value 144 | # 145 | # @param [ Proc ] step_proc The +Proc+ to call 146 | # 147 | # @raise [ ArgumentError ] When +step_proc+ does not evaluate to +Integer+ 148 | # 149 | # @return [ Integer ] The number to add to the current increment 150 | def evaluate_step_proc(step_proc) 151 | result = instance_exec(&step_proc) 152 | return result if result.is_a? Integer 153 | fail 'step Proc does not evaluate to an Integer' 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /lib/autoinc/incrementor.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module Autoinc 3 | # Object which wraps the mongodb operations needed to allow for 4 | # autoincrementing fields in +Mongoid::Document+ models. 5 | class Incrementor 6 | # The name of the autoincrementing model. 7 | attr_reader(:model_name) 8 | 9 | # The name of the field of the autoincrementing model. 10 | attr_reader(:field_name) 11 | 12 | # The constraint, allowing for more then one series on the same 13 | # +model_name+ +field_name+ combination. 14 | attr_reader(:scope_key) 15 | 16 | # The mongo connection to the autoincrement counters collection. 17 | attr_reader(:collection) 18 | 19 | # The autoincrement offset. 20 | attr_reader(:seed) 21 | 22 | # How the next autoincrement number should be calculated. 23 | attr_reader(:step) 24 | 25 | # Creates a new incrementor object for the passed +field_name+ 26 | # 27 | # @param [ String ] model_name Part of the name of the increment +key+ 28 | # @param [ String ] field_name The name of the +field+ to increment 29 | # @param [ Hash ] options Options to pass to the incrementer 30 | # 31 | def initialize(model_name, field_name, options = {}) 32 | @model_name = model_name.to_s 33 | @field_name = field_name.to_s 34 | @scope_key = options.fetch(:scope, nil) 35 | @step = options.fetch(:step, 1) 36 | @seed = options.fetch(:seed, nil) 37 | @collection = ::Mongoid.default_client['auto_increment_counters'] 38 | create if @seed && !exists? 39 | end 40 | 41 | # Returns the increment key 42 | # 43 | # @return [ String ] The key to increment 44 | def key 45 | return "#{model_name.underscore}_#{field_name}" if scope_key.blank? 46 | "#{model_name.underscore}_#{field_name}_#{scope_key}" 47 | end 48 | 49 | # Increments the +value+ of the +key+ and returns it using an atomic op 50 | # 51 | # @return [ Integer ] The next value of the incrementor 52 | def inc 53 | find.find_one_and_update({'$inc' => {c: step}}, upsert: true, return_document: :after).fetch('c') 54 | end 55 | 56 | private 57 | 58 | # Find the incrementor document, using the +key+ id. 59 | # 60 | # @return [ Hash ] The persisted version of this incrementor. 61 | def find 62 | collection.find(_id: key) 63 | end 64 | 65 | # Persists the incrementor using +key+ as id and +seed+ as value of +c+. 66 | # 67 | def create 68 | collection.insert_one(_id: key, c: seed) 69 | end 70 | 71 | # Checks if the incrementor is persisted 72 | # 73 | # @return [ true, false ] If the incrementor is already persisted. 74 | def exists? 75 | find.count > 0 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/autoinc/version.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module Autoinc 3 | VERSION = '6.0.4'.freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /mongoid-autoinc.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'autoinc/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'mongoid-autoinc' 7 | s.version = Mongoid::Autoinc::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ['Robert Beekman', 'Steven Weller', 'Jacob Vosmaer'] 10 | s.email = %w(robert@80beans.com steven@80beans.com jacob@80beans.com) 11 | s.homepage = 'https://github.com/suweller/mongoid-autoinc' 12 | s.summary = %q(Add auto incrementing fields to mongoid documents) 13 | s.description = %q(Think auto incrementing field from SQL for mongoid.) 14 | s.files = Dir.glob("lib/**/*") + %w(README.md) 15 | s.license = 'MIT' 16 | s.executables = s.files.grep(%r(^bin/)) { |f| File.basename(f) } 17 | s.test_files = s.files.grep(%r(^(test|spec|features)/)) 18 | s.require_path = 'lib' 19 | 20 | s.add_dependency 'mongoid', ['>= 6.0', '< 9.0'] 21 | 22 | s.add_development_dependency 'rake' 23 | s.add_development_dependency 'foreman' 24 | s.add_development_dependency 'rspec' 25 | s.add_development_dependency 'pry' 26 | end 27 | -------------------------------------------------------------------------------- /spec/autoinc_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Mongoid::Autoinc' do 4 | after { User.delete_all } 5 | 6 | context 'class methods' do 7 | subject { User } 8 | 9 | it { should respond_to(:increments) } 10 | it { should respond_to(:incrementing_fields) } 11 | 12 | describe '.incrementing_fields' do 13 | context 'for User' do 14 | subject { User.incrementing_fields } 15 | it { should match_array(number: {auto: true}) } 16 | end 17 | 18 | context 'for SpecialUser' do 19 | subject { SpecialUser.incrementing_fields } 20 | it { should match_array(number: {auto: true}) } 21 | end 22 | 23 | context 'for PatientFile' do 24 | subject { PatientFile.incrementing_fields } 25 | it { should match_array(file_number: {scope: :name, auto: true}) } 26 | end 27 | 28 | context 'for Operation' do 29 | let(:scope) { subject[:op_number][:scope] } 30 | subject { Operation.incrementing_fields } 31 | it { should match_array(op_number: {scope: scope, auto: true}) } 32 | it { expect(scope).to be_a Proc } 33 | end 34 | 35 | context 'for Vehicle' do 36 | subject { Vehicle.incrementing_fields } 37 | it { should match_array(vin: {seed: 1000, auto: true}) } 38 | end 39 | 40 | context 'for Ticket' do 41 | subject { Ticket.incrementing_fields } 42 | it { should match_array(number: {step: 2, auto: true}) } 43 | end 44 | 45 | context 'for LotteryTicket' do 46 | let(:step) { subject[:number][:step] } 47 | subject { LotteryTicket.incrementing_fields } 48 | it { should match_array(number: {step: step, auto: true}) } 49 | it { expect(step).to be_a Proc } 50 | end 51 | end 52 | 53 | describe '.increments' do 54 | before { allow(SpecialUser).to receive(:attr_protected) } 55 | specify { expect(SpecialUser).to receive(:attr_protected).with(:foo) } 56 | after { SpecialUser.increments(:foo) } 57 | end 58 | end 59 | 60 | context 'instance methods' do 61 | let(:incrementor) { Object.new } 62 | before { allow(incrementor).to receive(:inc).and_return(1) } 63 | 64 | context 'without scope' do 65 | let(:user) { User.new(name: 'Dr. Cox') } 66 | subject { User.new } 67 | 68 | it { should respond_to(:update_auto_increments) } 69 | 70 | it 'calls the autoincrementor' do 71 | expect(Mongoid::Autoinc::Incrementor).to receive(:new) 72 | .with('User', :number, {auto: true}) 73 | .and_return(incrementor) 74 | user.save! 75 | end 76 | 77 | describe 'writing the attribute' do 78 | before do 79 | allow(Mongoid::Autoinc::Incrementor).to receive(:new) 80 | .and_return(incrementor) 81 | end 82 | 83 | it 'writes the returned incrementor attribute' do 84 | expect { user.save! }.to change(user, :number).from(nil).to(1) 85 | end 86 | end 87 | 88 | describe '#assign!' do 89 | it 'raises AutoIncrementsError' do 90 | expect { subject.assign!(:number) } 91 | .to raise_error(Mongoid::Autoinc::AutoIncrementsError) 92 | end 93 | end 94 | end 95 | 96 | context 'with scope as symbol' do 97 | let(:patient_file) { PatientFile.new(name: 'Dr. Cox') } 98 | 99 | it 'should call the autoincrementor' do 100 | expect(Mongoid::Autoinc::Incrementor).to receive(:new) 101 | .with('PatientFile', :file_number, {scope: 'Dr. Cox', auto: true}) 102 | .and_return(incrementor) 103 | patient_file.save! 104 | end 105 | end 106 | 107 | context 'with scope as proc' do 108 | let(:user) { User.new(name: 'Dr. Cox') } 109 | let(:operation) { Operation.new(name: 'Heart Transplant', user: user) } 110 | 111 | it 'calls the autoincrementor' do 112 | expect(Mongoid::Autoinc::Incrementor).to receive(:new) 113 | .with('User', :number, {auto: true}) 114 | .and_return(incrementor) 115 | expect(Mongoid::Autoinc::Incrementor).to receive(:new) 116 | .with('Operation', :op_number, {scope: 'Dr. Cox', auto: true}) 117 | .and_return(incrementor) 118 | user.save! 119 | operation.save! 120 | end 121 | end 122 | 123 | context 'without auto' do 124 | subject { Intern.new } 125 | 126 | it 'should not call the autoincrementor' do 127 | expect(Mongoid::Autoinc::Incrementor).to_not receive(:new) 128 | subject.save! 129 | end 130 | 131 | describe '#assign!' do 132 | it 'calls the autoincrementor' do 133 | expect(Mongoid::Autoinc::Incrementor).to receive(:new) 134 | .with('Intern', :number, {auto: false}) 135 | .and_return(incrementor) 136 | subject.assign!(:number) 137 | end 138 | 139 | it 'raises when called more than once per document' do 140 | subject.assign!(:number) 141 | expect { subject.assign!(:number) } 142 | .to raise_error(Mongoid::Autoinc::AlreadyAssignedError) 143 | end 144 | end 145 | 146 | context 'class with overwritten model name' do 147 | subject { Intern.new } 148 | before do 149 | allow(Intern).to receive(:model_name) 150 | .and_return('PairOfScrubs') 151 | end 152 | 153 | it 'calls the autoincrementor' do 154 | expect(Mongoid::Autoinc::Incrementor).to receive(:new) 155 | .with('PairOfScrubs', :number, {auto: false}) 156 | .and_return(incrementor) 157 | subject.assign!(:number) 158 | end 159 | end 160 | end 161 | 162 | context 'with seed' do 163 | let(:vehicle) { Vehicle.new(model: 'Coupe') } 164 | it 'calls the autoincrementor with the seed value' do 165 | expect(Mongoid::Autoinc::Incrementor).to receive(:new) 166 | .with('Vehicle', :vin, {seed: 1000, auto: true}) 167 | .and_return(incrementor) 168 | vehicle.save! 169 | end 170 | end 171 | 172 | context 'with Integer step' do 173 | let(:ticket) { Ticket.new } 174 | it 'calls the autoincrementor with the options hash' do 175 | expect(Mongoid::Autoinc::Incrementor).to receive(:new) 176 | .with('Ticket', :number, {step: 2, auto: true}) 177 | .and_return(incrementor) 178 | ticket.save! 179 | end 180 | end 181 | 182 | context 'with Proc step' do 183 | let(:lottery_ticket) { LotteryTicket.new(start: 10) } 184 | it 'calls the autoincrementor with the options hash' do 185 | expect(Mongoid::Autoinc::Incrementor).to receive(:new) 186 | .with('LotteryTicket', :number, {step: 11, auto: true}) 187 | .and_return(incrementor) 188 | lottery_ticket.save! 189 | end 190 | end 191 | 192 | context 'with model_name' do 193 | let(:stethoscope) { Stethoscope.new } 194 | it 'calls the autoincrementor with the options hash' do 195 | expect(Mongoid::Autoinc::Incrementor).to receive(:new) 196 | .with('stetho', :number, {auto: true}) 197 | .and_return(incrementor) 198 | stethoscope.save! 199 | end 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /spec/incrementor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Mongoid::Autoinc::Incrementor' do 4 | let(:klass) { Mongoid::Autoinc::Incrementor } 5 | let(:options) { {scope: '123', seed: 100, step: 2} } 6 | let(:incrementor) { klass.new('User', :number, options) } 7 | 8 | describe 'model name' do 9 | subject { incrementor.model_name } 10 | it { should eq('User') } 11 | end 12 | 13 | describe 'field name' do 14 | subject { incrementor.field_name } 15 | it { should eq('number') } 16 | end 17 | 18 | describe 'scope key' do 19 | subject { incrementor.scope_key } 20 | it { should eq('123') } 21 | end 22 | 23 | describe 'seed' do 24 | subject { incrementor.seed } 25 | it { should eq(100) } 26 | end 27 | 28 | describe 'step' do 29 | subject { incrementor.step } 30 | it { should eq(2) } 31 | end 32 | 33 | describe '#key' do 34 | subject { incrementor.key } 35 | 36 | it { should eq('user_number_123') } 37 | 38 | context 'without scope' do 39 | subject { klass.new('User', :number).key } 40 | it { should eq('user_number')} 41 | end 42 | 43 | context 'for a subclass' do 44 | let(:incrementor) { klass.new('SpecialUser', :number, options) } 45 | it { should eq('special_user_number_123') } 46 | end 47 | end 48 | 49 | describe '#inc' do 50 | subject { incrementor.inc } 51 | 52 | describe 'generating incrementing numbers' do 53 | before { User.delete_all } 54 | it 'increments the number for each document' do 55 | (1..10).each do |i| 56 | expect(User.create!(name: 'Bob Kelso').number).to eq(i) 57 | end 58 | end 59 | end 60 | 61 | context 'with a seed value' do 62 | before { Vehicle.delete_all } 63 | it 'starts the incrementor at the seed value' do 64 | (1..10).each do |i| 65 | expect(Vehicle.create(model: 'Coupe').vin).to eq(1000 + i) 66 | end 67 | end 68 | end 69 | 70 | context 'with a step Integer value' do 71 | before { Ticket.delete_all } 72 | it 'increments according to the step value' do 73 | (1..10).each do |i| 74 | expect(Ticket.create.number).to eq(2 * i) 75 | end 76 | end 77 | end 78 | 79 | context 'with a step Proc value' do 80 | before { LotteryTicket.delete_all } 81 | it 'increments according to the step value' do 82 | expect(LotteryTicket.create(start: 10).number).to eq(11) 83 | expect(LotteryTicket.create(start: 30).number).to eq(42) 84 | expect(LotteryTicket.create.number).to eq(43) 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/models.rb: -------------------------------------------------------------------------------- 1 | class AutoincDocument 2 | include Mongoid::Document 3 | include Mongoid::Autoinc 4 | end 5 | 6 | class User < AutoincDocument 7 | field :name 8 | field :number 9 | has_many :operations 10 | increments :number 11 | end 12 | SpecialUser = Class.new(User) 13 | 14 | class PatientFile < AutoincDocument 15 | field :name 16 | field :file_number 17 | increments :file_number, scope: :name 18 | end 19 | 20 | class Operation < AutoincDocument 21 | field :name 22 | field :op_number 23 | belongs_to :user 24 | increments :op_number, scope: -> { user.name } 25 | end 26 | 27 | class Intern < AutoincDocument 28 | field :name 29 | field :number 30 | increments :number, auto: false 31 | end 32 | 33 | class Vehicle < AutoincDocument 34 | field :model 35 | field :vin 36 | increments :vin, seed: 1000 37 | end 38 | 39 | class Ticket < AutoincDocument 40 | field :number 41 | increments :number, step: 2 42 | end 43 | 44 | class LotteryTicket < AutoincDocument 45 | field :start, type: Integer, default: 0 46 | field :number 47 | increments :number, step: -> { start + 1 } 48 | end 49 | 50 | class Stethoscope < AutoincDocument 51 | field :number 52 | increments :number, model_name: :stetho 53 | end 54 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'mongoid' 3 | Mongoid.configure { |config| config.connect_to('mongoid_autoinc_test') } 4 | 5 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 6 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 7 | require 'autoinc' 8 | require 'models' 9 | 10 | RSpec.configure do |config| 11 | config.mock_with :rspec 12 | config.after(:suite) { Mongoid.purge! } 13 | end 14 | --------------------------------------------------------------------------------