From 27b73c06324bce885f8dc3d8ac6886bf1e84e889 Mon Sep 17 00:00:00 2001 From: Guillaume Hain Date: Fri, 12 Jan 2018 11:41:12 +0100 Subject: [PATCH] Add support for Nobrainer (RethinkDB) * Add persistence for NoBrainer * Add tests for NoBrainer * Show the ORM version when running the tests * Add a Dockerfile and a docker-compose.yml file allowing to run from anywhere very easily the test suite with `docker-compose run --rm aasm\' * Moves the Mongoid database name definition in the gem loader (avoiding code repetition) * Update the CHANGELOG.md and the README.md files * Add Rails generator for NoBrainer --- .travis.yml | 5 + Appraisals | 53 +++-- CHANGELOG.md | 3 + Dockerfile | 44 ++++ README.md | 24 ++- docker-compose.yml | 40 ++++ gemfiles/rails_4.2_nobrainer.gemfile | 9 + gemfiles/rails_5.0_nobrainer.gemfile | 9 + lib/aasm/persistence.rb | 3 + .../persistence/no_brainer_persistence.rb | 105 ++++++++++ lib/generators/nobrainer/aasm_generator.rb | 28 +++ lib/motion-aasm.rb | 1 + spec/generators/no_brainer_generator_spec.rb | 29 +++ .../nobrainer/complex_no_brainer_example.rb | 36 ++++ .../nobrainer/invalid_persistor_no_brainer.rb | 39 ++++ spec/models/nobrainer/no_scope_no_brainer.rb | 21 ++ .../nobrainer/nobrainer_relationships.rb | 25 +++ .../nobrainer/silent_persistor_no_brainer.rb | 39 ++++ .../nobrainer/simple_new_dsl_nobrainer.rb | 25 +++ spec/models/nobrainer/simple_no_brainer.rb | 23 ++ spec/models/nobrainer/validator_no_brainer.rb | 98 +++++++++ spec/spec_helpers/active_record.rb | 1 + spec/spec_helpers/dynamoid.rb | 10 +- spec/spec_helpers/mongoid.rb | 19 ++ spec/spec_helpers/nobrainer.rb | 15 ++ spec/spec_helpers/redis.rb | 5 +- .../mongoid_persistence_multiple_spec.rb | 4 - .../persistence/mongoid_persistence_spec.rb | 4 - .../no_brainer_persistence_multiple_spec.rb | 198 ++++++++++++++++++ .../no_brainer_persistence_spec.rb | 158 ++++++++++++++ test/minitest_helper.rb | 4 +- 31 files changed, 1037 insertions(+), 40 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 gemfiles/rails_4.2_nobrainer.gemfile create mode 100644 gemfiles/rails_5.0_nobrainer.gemfile create mode 100644 lib/aasm/persistence/no_brainer_persistence.rb create mode 100644 lib/generators/nobrainer/aasm_generator.rb create mode 100644 spec/generators/no_brainer_generator_spec.rb create mode 100644 spec/models/nobrainer/complex_no_brainer_example.rb create mode 100644 spec/models/nobrainer/invalid_persistor_no_brainer.rb create mode 100644 spec/models/nobrainer/no_scope_no_brainer.rb create mode 100644 spec/models/nobrainer/nobrainer_relationships.rb create mode 100644 spec/models/nobrainer/silent_persistor_no_brainer.rb create mode 100644 spec/models/nobrainer/simple_new_dsl_nobrainer.rb create mode 100644 spec/models/nobrainer/simple_no_brainer.rb create mode 100644 spec/models/nobrainer/validator_no_brainer.rb create mode 100644 spec/spec_helpers/nobrainer.rb create mode 100644 spec/unit/persistence/no_brainer_persistence_multiple_spec.rb create mode 100644 spec/unit/persistence/no_brainer_persistence_spec.rb diff --git a/.travis.yml b/.travis.yml index 5dba605..8441ab9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,12 +11,17 @@ services: - mongodb - redis-server +addons: + rethinkdb: '2.3.6' + gemfile: - gemfiles/rails_3.2.gemfile - gemfiles/rails_4.0.gemfile - gemfiles/rails_4.2.gemfile - gemfiles/rails_4.2_mongoid_5.gemfile + - gemfiles/rails_4.2_nobrainer.gemfile - gemfiles/rails_5.0.gemfile + - gemfiles/rails_5.0_nobrainer.gemfile before_script: - mkdir /tmp/dynamodb diff --git a/Appraisals b/Appraisals index cf6a3cb..b8b95ce 100644 --- a/Appraisals +++ b/Appraisals @@ -1,47 +1,58 @@ appraise 'rails_3.2' do - gem 'rails', '~>3.2.22' - gem 'mongoid', '~>3.1' + gem 'rails', '~> 3.2.22' + gem 'mongoid', '~> 3.1' gem 'sequel' - gem 'bson_ext', :platforms => :ruby + gem 'bson_ext', platforms: :ruby gem 'test-unit', '~> 3.0' - gem 'activerecord-jdbcsqlite3-adapter', '1.3.24', :platforms => :jruby + gem 'activerecord-jdbcsqlite3-adapter', '1.3.24', platforms: :jruby end appraise 'rails_4.0' do - gem 'mime-types', '~> 2', :platforms => [:ruby_19, :jruby] + gem 'mime-types', '~> 2', platforms: %i[ruby_19 jruby] gem 'rails', '4.0.13' - gem 'mongoid', '~>4.0' + gem 'mongoid', '~> 4.0' gem 'sequel' - gem 'dynamoid', '~> 1', :platforms => :ruby - gem 'aws-sdk', '~>2', :platforms => :ruby + gem 'dynamoid', '~> 1', platforms: :ruby + gem 'aws-sdk', '~> 2', platforms: :ruby gem 'redis-objects' - gem 'activerecord-jdbcsqlite3-adapter', '1.3.24', :platforms => :jruby + gem 'activerecord-jdbcsqlite3-adapter', '1.3.24', platforms: :jruby end appraise 'rails_4.2' do - gem 'nokogiri', '1.6.8.1', :platforms => [:ruby_19] - gem 'mime-types', '~> 2', :platforms => [:ruby_19, :jruby] + gem 'nokogiri', '1.6.8.1', platforms: %i[ruby_19] + gem 'mime-types', '~> 2', platforms: %i[ruby_19 jruby] gem 'rails', '4.2.5' - gem 'mongoid', '~>4.0' + gem 'mongoid', '~> 4.0' gem 'sequel' - gem 'dynamoid', '~> 1', :platforms => :ruby - gem 'aws-sdk', '~>2', :platforms => :ruby + gem 'dynamoid', '~> 1', platforms: :ruby + gem 'aws-sdk', '~> 2', platforms: :ruby gem 'redis-objects' - gem 'activerecord-jdbcsqlite3-adapter', '1.3.24', :platforms => :jruby + gem 'activerecord-jdbcsqlite3-adapter', '1.3.24', platforms: :jruby +end + +appraise 'rails_4.2_nobrainer' do + gem 'rails', '4.2.5' + gem 'nobrainer', '~> 0.33.0' + gem 'activerecord-jdbcsqlite3-adapter', '1.3.24', platforms: :jruby end appraise 'rails_4.2_mongoid_5' do - gem 'mime-types', '~> 2', :platforms => [:ruby_19, :jruby] + gem 'mime-types', '~> 2', platforms: %i[ruby_19 jruby] gem 'rails', '4.2.5' - gem 'mongoid', '~>5.0' - gem 'activerecord-jdbcsqlite3-adapter', '1.3.24', :platforms => :jruby + gem 'mongoid', '~> 5.0' + gem 'activerecord-jdbcsqlite3-adapter', '1.3.24', platforms: :jruby end appraise 'rails_5.0' do gem 'rails', '5.0.0' - gem 'mongoid', '~>6.0' + gem 'mongoid', '~> 6.0' gem 'sequel' - gem 'dynamoid', '~> 1', :platforms => :ruby - gem 'aws-sdk', '~>2', :platforms => :ruby + gem 'dynamoid', '~> 1', platforms: :ruby + gem 'aws-sdk', '~> 2', platforms: :ruby gem 'redis-objects' end + +appraise 'rails_5.0_nobrainer' do + gem 'rails', '5.0.0' + gem 'nobrainer', '~> 0.33.0' +end diff --git a/CHANGELOG.md b/CHANGELOG.md index 400fa89..d044246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # CHANGELOG ## unreleased + +* Add support for Nobrainer (RethinkDB) + ## 4.12.3 * Add to AASM fire(event) and fire!(event) methods [#494](https://github.com/aasm/aasm/pull/494), thanks to [slayer](https://github.com/slayer) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ec5a630 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +FROM ruby:2.3.4-slim + +LABEL maintainer="AASM" + +ENV DEBIAN_FRONTEND noninteractive + +# ~~~~ System locales ~~~~ +RUN apt-get update && apt-get install -y locales && \ + dpkg-reconfigure locales && \ + locale-gen C.UTF-8 && \ + /usr/sbin/update-locale LANG=C.UTF-8 && \ + echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen && \ + locale-gen + +# Set default locale for the environment +ENV LC_ALL C.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US.UTF-8 +ENV APP_HOME /application + +# ~~~~ Application dependencies ~~~~ +RUN apt-get update +RUN apt-get install -y libsqlite3-dev \ + build-essential \ + git + +# ~~~~ Bundler ~~~~ +RUN gem install bundler + +WORKDIR $APP_HOME +RUN mkdir -p $APP_HOME/lib/aasm/ + +COPY Gemfile* $APP_HOME/ +COPY *.gemspec $APP_HOME/ +COPY lib/aasm/version.rb $APP_HOME/lib/aasm/ + +ENV BUNDLE_GEMFILE=$APP_HOME/Gemfile \ + BUNDLE_JOBS=8 \ + BUNDLE_PATH=/bundle + +RUN bundle install + +# ~~~~ Import application ~~~~ +COPY . $APP_HOME diff --git a/README.md b/README.md index c587118..15f4418 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ - [Sequel](#sequel) - [Dynamoid](#dynamoid) - [Mongoid](#mongoid) + - [Nobrainer](#nobrainer) - [Redis](#redis) - [Automatic Scopes](#automatic-scopes) - [Transaction support](#transaction-support) @@ -52,10 +53,8 @@ This package contains AASM, a library for adding finite state machines to Ruby classes. AASM started as the *acts_as_state_machine* plugin but has evolved into a more generic library -that no longer targets only ActiveRecord models. It currently provides adapters for -[ActiveRecord](http://api.rubyonrails.org/classes/ActiveRecord/Base.html), -and [Mongoid](http://mongoid.org/) but it can be used for any Ruby class, no matter what -parent class it has (if any). +that no longer targets only ActiveRecord models. It currently provides adapters for many +ORMs but it can be used for any Ruby class, no matter what parent class it has (if any). ## Upgrade from version 3 to 4 @@ -825,6 +824,23 @@ class Job end ``` +### NoBrainer + +AASM also supports persistence to [RethinkDB](https://www.rethinkdb.com/) +if you're using [Nobrainer](http://nobrainer.io/). +Make sure to include NoBrainer::Document before you include AASM. + +```ruby +class Job + include NoBrainer::Document + include AASM + field :aasm_state + aasm do + ... + end +end +``` + ### Redis AASM also supports persistence in Redis via diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..701e57c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +version: "2" + +services: + aasm: + image: aasm/aasm + build: . + command: bash -c 'bundle exec appraisal install && bundle exec appraisal rspec' + environment: + - DYNAMODB_HOST=dynamodb + - DYNAMODB_PORT=8000 + - MONGODB_HOST=mongo + - MONGODB_PORT=27017 + - RAILS_ENV=development + - REDIS_HOST=redis + - REDIS_PORT=6379 + - RETHINKDB_DB=rethinkdb_test + - RETHINKDB_HOST=rethinkdb + - RETHINKDB_PORT=28015 + depends_on: + - dynamodb + - mongo + - redis + - rethinkdb + volumes: + - .:/application + volumes_from: + - bundle + bundle: + image: aasm/aasm + command: echo Bundler data container + volumes: + - /bundle + dynamodb: + image: cnadiminti/dynamodb-local:2017-02-16 + mongo: + image: mongo:3.6.1 + redis: + image: redis:4.0.6-alpine + rethinkdb: + image: rethinkdb:2.3.6 diff --git a/gemfiles/rails_4.2_nobrainer.gemfile b/gemfiles/rails_4.2_nobrainer.gemfile new file mode 100644 index 0000000..9e98000 --- /dev/null +++ b/gemfiles/rails_4.2_nobrainer.gemfile @@ -0,0 +1,9 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "sqlite3", platforms: :ruby +gem "rails", "5.1.3" +gem "nobrainer", "~>0.33.0" + +gemspec path: "../" diff --git a/gemfiles/rails_5.0_nobrainer.gemfile b/gemfiles/rails_5.0_nobrainer.gemfile new file mode 100644 index 0000000..9e98000 --- /dev/null +++ b/gemfiles/rails_5.0_nobrainer.gemfile @@ -0,0 +1,9 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "sqlite3", platforms: :ruby +gem "rails", "5.1.3" +gem "nobrainer", "~>0.33.0" + +gemspec path: "../" diff --git a/lib/aasm/persistence.rb b/lib/aasm/persistence.rb index 4e549eb..852f441 100644 --- a/lib/aasm/persistence.rb +++ b/lib/aasm/persistence.rb @@ -12,6 +12,9 @@ module AASM elsif hierarchy.include?("Mongoid::Document") require_persistence :mongoid include_persistence base, :mongoid + elsif hierarchy.include?("NoBrainer::Document") + require_persistence :no_brainer + include_persistence base, :no_brainer elsif hierarchy.include?("Sequel::Model") require_persistence :sequel include_persistence base, :sequel diff --git a/lib/aasm/persistence/no_brainer_persistence.rb b/lib/aasm/persistence/no_brainer_persistence.rb new file mode 100644 index 0000000..cbe2677 --- /dev/null +++ b/lib/aasm/persistence/no_brainer_persistence.rb @@ -0,0 +1,105 @@ +require 'aasm/persistence/orm' +module AASM + module Persistence + module NoBrainerPersistence + # This method: + # + # * extends the model with ClassMethods + # * includes InstanceMethods + # + # Adds + # + # before_validation :aasm_ensure_initial_state + # + # As a result, it doesn't matter when you define your methods - the following 2 are equivalent + # + # class Foo + # include NoBrainer::Document + # def aasm_write_state(state) + # "bar" + # end + # include AASM + # end + # + # class Foo + # include NoBrainer::Document + # include AASM + # def aasm_write_state(state) + # "bar" + # end + # end + # + def self.included(base) + base.send(:include, AASM::Persistence::Base) + base.send(:include, AASM::Persistence::ORM) + base.send(:include, AASM::Persistence::NoBrainerPersistence::InstanceMethods) + base.extend AASM::Persistence::NoBrainerPersistence::ClassMethods + + base.after_initialize :aasm_ensure_initial_state + end + + module ClassMethods + def aasm_create_scope(state_machine_name, scope_name) + scope_options = lambda { + where(aasm(state_machine_name).attribute_name.to_sym => scope_name.to_s) + } + send(:scope, scope_name, scope_options) + end + end + + module InstanceMethods + + private + + def aasm_save + self.save + end + + def aasm_raise_invalid_record + raise NoBrainer::Error::DocumentInvalid.new(self) + end + + def aasm_supports_transactions? + false + end + + def aasm_update_column(attribute_name, value) + write_attribute(attribute_name, value) + save(validate: false) + + true + end + + def aasm_read_attribute(name) + read_attribute(name) + end + + def aasm_write_attribute(name, value) + write_attribute(name, value) + end + + # Ensures that if the aasm_state column is nil and the record is new + # that the initial state gets populated before validation on create + # + # foo = Foo.new + # foo.aasm_state # => nil + # foo.valid? + # foo.aasm_state # => "open" (where :open is the initial state) + # + # + # foo = Foo.find(:first) + # foo.aasm_state # => 1 + # foo.aasm_state = nil + # foo.valid? + # foo.aasm_state # => nil + # + def aasm_ensure_initial_state + AASM::StateMachineStore.fetch(self.class, true).machine_names.each do |name| + aasm_column = self.class.aasm(name).attribute_name + aasm(name).enter_initial_state if read_attribute(aasm_column).blank? + end + end + end # InstanceMethods + end + end # Persistence +end # AASM diff --git a/lib/generators/nobrainer/aasm_generator.rb b/lib/generators/nobrainer/aasm_generator.rb new file mode 100644 index 0000000..f00f285 --- /dev/null +++ b/lib/generators/nobrainer/aasm_generator.rb @@ -0,0 +1,28 @@ +require 'rails/generators/named_base' +require 'generators/aasm/orm_helpers' + +module NoBrainer + module Generators + class AASMGenerator < Rails::Generators::NamedBase + include AASM::Generators::OrmHelpers + namespace 'nobrainer:aasm' + argument :column_name, type: :string, default: 'aasm_state' + + def generate_model + invoke 'nobrainer:model', [name] unless model_exists? + end + + def inject_aasm_content + inject_into_file model_path, model_contents, after: "include NoBrainer::Document::Timestamps\n" if model_exists? + end + + def inject_field_types + inject_into_file model_path, migration_data, after: "include NoBrainer::Document::Timestamps\n" if model_exists? + end + + def migration_data + " field :#{column_name}" + end + end + end +end diff --git a/lib/motion-aasm.rb b/lib/motion-aasm.rb index 314bfcd..7d2d9ca 100644 --- a/lib/motion-aasm.rb +++ b/lib/motion-aasm.rb @@ -16,6 +16,7 @@ exclude_files = [ 'aasm/persistence/active_record_persistence.rb', 'aasm/persistence/dynamoid_persistence.rb', 'aasm/persistence/mongoid_persistence.rb', + 'aasm/persistence/no_brainer_persistence.rb', 'aasm/persistence/sequel_persistence.rb', 'aasm/persistence/redis_persistence.rb' ] diff --git a/spec/generators/no_brainer_generator_spec.rb b/spec/generators/no_brainer_generator_spec.rb new file mode 100644 index 0000000..8dc08e2 --- /dev/null +++ b/spec/generators/no_brainer_generator_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +if defined?(NoBrainer::Document) + require 'generator_spec' + require 'generators/nobrainer/aasm_generator' + + describe NoBrainer::Generators::AASMGenerator, type: :generator do + destination File.expand_path('../../../tmp', __FILE__) + + before(:all) do + prepare_destination + end + + it 'creates model with aasm block for default column_name' do + run_generator %w[user] + assert_file 'app/models/user.rb', /include AASM\n\n aasm do\n end\n/ + end + + it 'creates model with aasm block for custom column_name' do + run_generator %w[user state] + assert_file 'app/models/user.rb', /aasm :column => 'state' do\n end\n/ + end + + it 'creates model with aasm block for namespaced model' do + run_generator %w[Admin::User state] + assert_file 'app/models/admin/user.rb', /aasm :column => 'state' do\n end\n/ + end + end +end diff --git a/spec/models/nobrainer/complex_no_brainer_example.rb b/spec/models/nobrainer/complex_no_brainer_example.rb new file mode 100644 index 0000000..06a8600 --- /dev/null +++ b/spec/models/nobrainer/complex_no_brainer_example.rb @@ -0,0 +1,36 @@ +class ComplexNoBrainerExample + include NoBrainer::Document + include AASM + + field :left, type: String + field :right, type: String + + aasm :left, column: 'left' do + state :one, initial: true + state :two + state :three + + event :increment do + transitions from: :one, to: :two + transitions from: :two, to: :three + end + event :reset do + transitions from: :three, to: :one + end + end + + aasm :right, column: 'right' do + state :alpha, initial: true + state :beta + state :gamma + + event :level_up do + transitions from: :alpha, to: :beta + transitions from: :beta, to: :gamma + end + event :level_down do + transitions from: :gamma, to: :beta + transitions from: :beta, to: :alpha + end + end +end diff --git a/spec/models/nobrainer/invalid_persistor_no_brainer.rb b/spec/models/nobrainer/invalid_persistor_no_brainer.rb new file mode 100644 index 0000000..ee01632 --- /dev/null +++ b/spec/models/nobrainer/invalid_persistor_no_brainer.rb @@ -0,0 +1,39 @@ +class InvalidPersistorNoBrainer + include NoBrainer::Document + include AASM + + field :name + field :status + + aasm :left, column: :status, skip_validation_on_save: true do + state :sleeping, initial: true + state :running + event :run do + transitions to: :running, from: :sleeping + end + event :sleep do + transitions to: :sleeping, from: :running + end + end + validates_presence_of :name +end + +class MultipleInvalidPersistorNoBrainer + include NoBrainer::Document + include AASM + + field :name + field :status + + aasm :left, column: :status, skip_validation_on_save: true do + state :sleeping, initial: true + state :running + event :run do + transitions to: :running, from: :sleeping + end + event :sleep do + transitions to: :sleeping, from: :running + end + end + validates_presence_of :name +end diff --git a/spec/models/nobrainer/no_scope_no_brainer.rb b/spec/models/nobrainer/no_scope_no_brainer.rb new file mode 100644 index 0000000..c689cb7 --- /dev/null +++ b/spec/models/nobrainer/no_scope_no_brainer.rb @@ -0,0 +1,21 @@ +class NoScopeNoBrainer + include NoBrainer::Document + include AASM + + field :status, type: String + + aasm create_scopes: false, column: :status do + state :ignored_scope + end +end + +class NoScopeNoBrainerMultiple + include NoBrainer::Document + include AASM + + field :status, type: String + + aasm :left, create_scopes: false, column: :status do + state :ignored_scope + end +end diff --git a/spec/models/nobrainer/nobrainer_relationships.rb b/spec/models/nobrainer/nobrainer_relationships.rb new file mode 100644 index 0000000..246bb30 --- /dev/null +++ b/spec/models/nobrainer/nobrainer_relationships.rb @@ -0,0 +1,25 @@ +class Parent + include NoBrainer::Document + include AASM + + field :status, type: String + has_many :childs + + aasm column: :status do + state :unknown_scope + state :new + end +end + +class Child + include NoBrainer::Document + include AASM + + field :status, type: String + belongs_to :parent + + aasm column: :status do + state :unknown_scope + state :new + end +end diff --git a/spec/models/nobrainer/silent_persistor_no_brainer.rb b/spec/models/nobrainer/silent_persistor_no_brainer.rb new file mode 100644 index 0000000..d8ee976 --- /dev/null +++ b/spec/models/nobrainer/silent_persistor_no_brainer.rb @@ -0,0 +1,39 @@ +class SilentPersistorNoBrainer + include NoBrainer::Document + include AASM + + field :name + field :status + + aasm :left, column: :status, whiny_persistence: false do + state :sleeping, initial: true + state :running + event :run do + transitions to: :running, from: :sleeping + end + event :sleep do + transitions to: :sleeping, from: :running + end + end + validates_presence_of :name +end + +class MultipleSilentPersistorNoBrainer + include NoBrainer::Document + include AASM + + field :name + field :status + + aasm :left, column: :status, whiny_persistence: false do + state :sleeping, initial: true + state :running + event :run do + transitions to: :running, from: :sleeping + end + event :sleep do + transitions to: :sleeping, from: :running + end + end + validates_presence_of :name +end diff --git a/spec/models/nobrainer/simple_new_dsl_nobrainer.rb b/spec/models/nobrainer/simple_new_dsl_nobrainer.rb new file mode 100644 index 0000000..1448de4 --- /dev/null +++ b/spec/models/nobrainer/simple_new_dsl_nobrainer.rb @@ -0,0 +1,25 @@ +class SimpleNewDslNoBrainer + include NoBrainer::Document + include AASM + + field :status, type: String + + aasm column: :status + aasm do + state :unknown_scope, initial: true + state :new + end +end + +class SimpleNewDslNoBrainerMultiple + include NoBrainer::Document + include AASM + + field :status, type: String + + aasm :left, column: :status + aasm :left do + state :unknown_scope, initial: true + state :new + end +end diff --git a/spec/models/nobrainer/simple_no_brainer.rb b/spec/models/nobrainer/simple_no_brainer.rb new file mode 100644 index 0000000..d317a53 --- /dev/null +++ b/spec/models/nobrainer/simple_no_brainer.rb @@ -0,0 +1,23 @@ +class SimpleNoBrainer + include NoBrainer::Document + include AASM + + field :status, type: String + + aasm column: :status do + state :unknown_scope, :another_unknown_scope + state :new + end +end + +class SimpleNoBrainerMultiple + include NoBrainer::Document + include AASM + + field :status, type: String + + aasm :left, column: :status do + state :unknown_scope, :another_unknown_scope + state :new + end +end diff --git a/spec/models/nobrainer/validator_no_brainer.rb b/spec/models/nobrainer/validator_no_brainer.rb new file mode 100644 index 0000000..0b8674d --- /dev/null +++ b/spec/models/nobrainer/validator_no_brainer.rb @@ -0,0 +1,98 @@ +class ValidatorNoBrainer + include NoBrainer::Document + include AASM + + field :name + field :status + + attr_accessor :invalid + + validate do |_| + errors.add(:validator, 'invalid') if invalid + end + + aasm column: :status, whiny_persistence: true do + before_all_transactions :before_all_transactions + after_all_transactions :after_all_transactions + + state :sleeping, initial: true + state :running + state :failed, after_enter: :fail + + event :run, after_commit: :change_name! do + transitions to: :running, from: :sleeping + end + + event :sleep do + after_commit do |name| + change_name_on_sleep name + end + transitions to: :sleeping, from: :running + end + + event :fail do + transitions to: :failed, from: %i[sleeping running] + end + end + + validates_presence_of :name + + def change_name! + self.name = 'name changed' + save! + end + + def change_name_on_sleep(name) + self.name = name + save! + end + + def fail + raise StandardError, 'failed on purpose' + end +end + +class MultipleValidatorNoBrainer + include NoBrainer::Document + include AASM + + field :name + field :status + + attr_accessor :invalid + + aasm :left, column: :status, whiny_persistence: true do + state :sleeping, initial: true + state :running + state :failed, after_enter: :fail + + event :run, after_commit: :change_name! do + transitions to: :running, from: :sleeping + end + event :sleep do + after_commit do |name| + change_name_on_sleep name + end + transitions to: :sleeping, from: :running + end + event :fail do + transitions to: :failed, from: %i[sleeping running] + end + end + + validates_presence_of :name + + def change_name! + self.name = 'name changed' + save! + end + + def change_name_on_sleep(name) + self.name = name + save! + end + + def fail + raise StandardError, 'failed on purpose' + end +end diff --git a/spec/spec_helpers/active_record.rb b/spec/spec_helpers/active_record.rb index 56a5a6b..e29844f 100644 --- a/spec/spec_helpers/active_record.rb +++ b/spec/spec_helpers/active_record.rb @@ -1,6 +1,7 @@ # encoding: utf-8 begin require 'active_record' + puts "active_record gem found, running ActiveRecord specs \e[32m#{'✔'}\e[0m" rescue LoadError puts "active_record gem not found, not running ActiveRecord specs \e[31m#{'✖'}\e[0m" diff --git a/spec/spec_helpers/dynamoid.rb b/spec/spec_helpers/dynamoid.rb index ccc7eb9..5c72efe 100644 --- a/spec/spec_helpers/dynamoid.rb +++ b/spec/spec_helpers/dynamoid.rb @@ -1,4 +1,5 @@ # encoding: utf-8 + begin require 'dynamoid' require 'aws-sdk-resources' @@ -7,14 +8,15 @@ begin ENV['ACCESS_KEY'] ||= 'abcd' ENV['SECRET_KEY'] ||= '1234' - Aws.config.update({ + Aws.config.update( region: 'us-west-2', credentials: Aws::Credentials.new(ENV['ACCESS_KEY'], ENV['SECRET_KEY']) - }) + ) Dynamoid.configure do |config| - config.namespace = "dynamoid_tests" - config.endpoint = 'http://127.0.0.1:30180' + config.namespace = 'dynamoid_tests' + config.endpoint = "http://#{ENV['DYNAMODB_HOST'] || '127.0.0.1'}:" \ + "#{ENV['DYNAMODB_PORT'] || 30180}" config.warn_on_scan = false end diff --git a/spec/spec_helpers/mongoid.rb b/spec/spec_helpers/mongoid.rb index baa8990..03f345c 100644 --- a/spec/spec_helpers/mongoid.rb +++ b/spec/spec_helpers/mongoid.rb @@ -1,7 +1,26 @@ # encoding: utf-8 + begin require 'mongoid' puts "mongoid gem found, running mongoid specs \e[32m#{'✔'}\e[0m" + + if Mongoid::VERSION.to_f <= 5 + Mongoid::Config.sessions = { + default: { + database: "mongoid_#{Process.pid}", + hosts: ["#{ENV['MONGODB_HOST'] || 'localhost'}:" \ + "#{ENV['MONGODB_PORT'] || 27017}"] + } + } + else + Mongoid::Config.send(:clients=, { + default: { + database: "mongoid_#{Process.pid}", + hosts: ["#{ENV['MONGODB_HOST'] || 'localhost'}:" \ + "#{ENV['MONGODB_PORT'] || 27017}"] + } + }) + end rescue LoadError puts "mongoid gem not found, not running mongoid specs \e[31m#{'✖'}\e[0m" end diff --git a/spec/spec_helpers/nobrainer.rb b/spec/spec_helpers/nobrainer.rb new file mode 100644 index 0000000..d2656fd --- /dev/null +++ b/spec/spec_helpers/nobrainer.rb @@ -0,0 +1,15 @@ +# encoding: utf-8 + +begin + require 'nobrainer' + + NoBrainer.configure do |config| + config.app_name = :aasm + config.environment = :test + config.warn_on_active_record = false + end + + puts "nobrainer #{Gem.loaded_specs['nobrainer'].version} gem found, running nobrainer specs \e[32m#{'✔'}\e[0m" +rescue LoadError + puts "nobrainer gem not found, not running nobrainer specs \e[31m#{'✖'}\e[0m" +end diff --git a/spec/spec_helpers/redis.rb b/spec/spec_helpers/redis.rb index 2e1ab1a..45fbeb5 100644 --- a/spec/spec_helpers/redis.rb +++ b/spec/spec_helpers/redis.rb @@ -1,9 +1,12 @@ # encoding: utf-8 + begin require 'redis-objects' + require 'redis/objects/version' puts "redis-objects gem found, running Redis specs \e[32m#{'✔'}\e[0m" - Redis.current = Redis.new(host: '127.0.0.1', port: 6379) + Redis.current = Redis.new(host: (ENV['REDIS_HOST'] || '127.0.0.1'), + port: (ENV['REDIS_PORT'] || 6379)) RSpec.configure do |c| c.before(:each) do diff --git a/spec/unit/persistence/mongoid_persistence_multiple_spec.rb b/spec/unit/persistence/mongoid_persistence_multiple_spec.rb index 368e900..47f6b9e 100644 --- a/spec/unit/persistence/mongoid_persistence_multiple_spec.rb +++ b/spec/unit/persistence/mongoid_persistence_multiple_spec.rb @@ -10,10 +10,6 @@ if defined?(Mongoid::Document) before(:all) do # if you want to see the statements while running the spec enable the following line # Mongoid.logger = Logger.new(STDERR) - - Mongoid.configure do |config| - config.connect_to "mongoid_#{Process.pid}" - end end after do diff --git a/spec/unit/persistence/mongoid_persistence_spec.rb b/spec/unit/persistence/mongoid_persistence_spec.rb index 05a60a3..1f5257c 100644 --- a/spec/unit/persistence/mongoid_persistence_spec.rb +++ b/spec/unit/persistence/mongoid_persistence_spec.rb @@ -10,10 +10,6 @@ if defined?(Mongoid::Document) before(:all) do # if you want to see the statements while running the spec enable the following line # Mongoid.logger = Logger.new(STDERR) - - Mongoid.configure do |config| - config.connect_to "mongoid_#{Process.pid}" - end end after do diff --git a/spec/unit/persistence/no_brainer_persistence_multiple_spec.rb b/spec/unit/persistence/no_brainer_persistence_multiple_spec.rb new file mode 100644 index 0000000..694c097 --- /dev/null +++ b/spec/unit/persistence/no_brainer_persistence_multiple_spec.rb @@ -0,0 +1,198 @@ +require 'spec_helper' + +if defined?(NoBrainer::Document) + describe 'nobrainer' do + Dir[File.dirname(__FILE__) + '/../../models/nobrainer/*.rb'].sort.each do |f| + require File.expand_path(f) + end + + before(:all) do + # if you want to see the statements while running the spec enable the + # following line + # NoBrainer.configure do |config| + # config.logger = Logger.new(STDERR) + # end + end + + after do + NoBrainer.purge! + end + + describe 'named scopes with the old DSL' do + context 'Does not already respond_to? the scope name' do + it 'should add a scope for each state' do + expect(SimpleNoBrainerMultiple).to respond_to(:unknown_scope) + expect(SimpleNoBrainerMultiple).to respond_to(:another_unknown_scope) + + expect(SimpleNoBrainerMultiple.unknown_scope.class).to eq(NoBrainer::Criteria) + expect(SimpleNoBrainerMultiple.another_unknown_scope.class).to eq(NoBrainer::Criteria) + end + end + + context 'Already respond_to? the scope name' do + it 'should not add a scope' do + expect(SimpleNoBrainerMultiple).to respond_to(:new) + expect(SimpleNoBrainerMultiple.new.class).to eq(SimpleNoBrainerMultiple) + end + end + + end + + describe 'named scopes with the new DSL' do + context 'Does not already respond_to? the scope name' do + it 'should add a scope' do + expect(SimpleNewDslNoBrainerMultiple).to respond_to(:unknown_scope) + expect(SimpleNewDslNoBrainerMultiple.unknown_scope.class).to eq(NoBrainer::Criteria) + end + end + + context 'Already respond_to? the scope name' do + it 'should not add a scope' do + expect(SimpleNewDslNoBrainerMultiple).to respond_to(:new) + expect(SimpleNewDslNoBrainerMultiple.new.class).to eq(SimpleNewDslNoBrainerMultiple) + end + end + + it 'does not create scopes if requested' do + expect(NoScopeNoBrainerMultiple).not_to respond_to(:ignored_scope) + end + end + + describe 'instance methods' do + let(:simple) { SimpleNewDslNoBrainerMultiple.new } + + it 'should initialize the aasm state on instantiation' do + expect(SimpleNewDslNoBrainerMultiple.new.status).to eql 'unknown_scope' + expect(SimpleNewDslNoBrainerMultiple.new.aasm(:left).current_state).to eql :unknown_scope + end + end + + describe 'transitions with persistence' do + it 'should work for valid models' do + valid_object = MultipleValidatorNoBrainer.create(name: 'name') + expect(valid_object).to be_sleeping + valid_object.status = :running + expect(valid_object).to be_running + end + + it 'should not store states for invalid models' do + validator = MultipleValidatorNoBrainer.create(name: 'name') + expect(validator).to be_valid + expect(validator).to be_sleeping + + validator.name = nil + expect(validator).not_to be_valid + expect { validator.run! }.to raise_error(NoBrainer::Error::DocumentInvalid) + expect(validator).to be_sleeping + + validator.reload + expect(validator).not_to be_running + expect(validator).to be_sleeping + + validator.name = 'another name' + expect(validator).to be_valid + expect(validator.run!).to be_truthy + expect(validator).to be_running + + validator.reload + expect(validator).to be_running + expect(validator).not_to be_sleeping + end + + it 'should not store states for invalid models silently if configured' do + validator = MultipleSilentPersistorNoBrainer.create(name: 'name') + expect(validator).to be_valid + expect(validator).to be_sleeping + + validator.name = nil + expect(validator).not_to be_valid + expect(validator.run!).to be_falsey + expect(validator).to be_sleeping + + validator.reload + expect(validator).not_to be_running + expect(validator).to be_sleeping + + validator.name = 'another name' + expect(validator).to be_valid + expect(validator.run!).to be_truthy + expect(validator).to be_running + + validator.reload + expect(validator).to be_running + expect(validator).not_to be_sleeping + end + + it 'should store states for invalid models if configured' do + persistor = MultipleInvalidPersistorNoBrainer.create(name: 'name') + expect(persistor).to be_valid + expect(persistor).to be_sleeping + + persistor.name = nil + expect(persistor).not_to be_valid + expect(persistor.run!).to be_truthy + expect(persistor).to be_running + + persistor = MultipleInvalidPersistorNoBrainer.find(persistor.id) + persistor.valid? + expect(persistor).to be_valid + expect(persistor).to be_running + expect(persistor).not_to be_sleeping + + persistor.reload + expect(persistor).to be_running + expect(persistor).not_to be_sleeping + end + end + + describe 'complex example' do + it 'works' do + record = ComplexNoBrainerExample.new + expect_aasm_states record, :one, :alpha + + record.save! + expect_aasm_states record, :one, :alpha + record.reload + expect_aasm_states record, :one, :alpha + + record.increment! + expect_aasm_states record, :two, :alpha + record.reload + expect_aasm_states record, :two, :alpha + + record.level_up! + expect_aasm_states record, :two, :beta + record.reload + expect_aasm_states record, :two, :beta + + record.increment! + expect { record.increment! }.to raise_error(AASM::InvalidTransition) + expect_aasm_states record, :three, :beta + record.reload + expect_aasm_states record, :three, :beta + + record.level_up! + expect_aasm_states record, :three, :gamma + record.reload + expect_aasm_states record, :three, :gamma + + record.level_down # without saving + expect_aasm_states record, :three, :beta + record.reload + expect_aasm_states record, :three, :gamma + + record.level_down # without saving + expect_aasm_states record, :three, :beta + record.reset! + expect_aasm_states record, :one, :beta + end + + def expect_aasm_states(record, left_state, right_state) + expect(record.aasm(:left).current_state).to eql left_state.to_sym + expect(record.left).to eql left_state.to_s + expect(record.aasm(:right).current_state).to eql right_state.to_sym + expect(record.right).to eql right_state.to_s + end + end + end +end diff --git a/spec/unit/persistence/no_brainer_persistence_spec.rb b/spec/unit/persistence/no_brainer_persistence_spec.rb new file mode 100644 index 0000000..5da86ce --- /dev/null +++ b/spec/unit/persistence/no_brainer_persistence_spec.rb @@ -0,0 +1,158 @@ +require 'spec_helper' + +if defined?(NoBrainer::Document) + describe 'nobrainer' do + Dir[File.dirname(__FILE__) + '/../../models/nobrainer/*.rb'].sort.each do |f| + require File.expand_path(f) + end + + before(:all) do + # if you want to see the statements while running the spec enable the + # following line + # NoBrainer.configure do |config| + # config.logger = Logger.new(STDERR) + # end + end + + after do + NoBrainer.purge! + end + + describe 'named scopes with the old DSL' do + context 'Does not already respond_to? the scope name' do + it 'should add a scope for each state' do + expect(SimpleNoBrainer).to respond_to(:unknown_scope) + expect(SimpleNoBrainer).to respond_to(:another_unknown_scope) + + expect(SimpleNoBrainer.unknown_scope.class).to eq(NoBrainer::Criteria) + expect(SimpleNoBrainer.another_unknown_scope.class).to eq(NoBrainer::Criteria) + end + end + + context 'Already respond_to? the scope name' do + it 'should not add a scope' do + expect(SimpleNoBrainer).to respond_to(:new) + expect(SimpleNoBrainer.new.class).to eq(SimpleNoBrainer) + end + end + end + + describe 'named scopes with the new DSL' do + context 'Does not already respond_to? the scope name' do + it 'should add a scope' do + expect(SimpleNewDslNoBrainer).to respond_to(:unknown_scope) + expect(SimpleNewDslNoBrainer.unknown_scope.class).to eq(NoBrainer::Criteria) + end + end + + context 'Already respond_to? the scope name' do + it 'should not add a scope' do + expect(SimpleNewDslNoBrainer).to respond_to(:new) + expect(SimpleNewDslNoBrainer.new.class).to eq(SimpleNewDslNoBrainer) + end + end + + it 'does not create scopes if requested' do + expect(NoScopeNoBrainer).not_to respond_to(:ignored_scope) + end + end + + describe 'instance methods' do + let(:simple) { SimpleNewDslNoBrainer.new } + + it 'should initialize the aasm state on instantiation' do + expect(SimpleNewDslNoBrainer.new.status).to eql 'unknown_scope' + expect(SimpleNewDslNoBrainer.new.aasm.current_state).to eql :unknown_scope + end + end + + describe 'relations object' do + it 'should load relations object ids' do + parent = Parent.create + child_1 = Child.create(parent_id: parent.id) + child_2 = Child.create(parent_id: parent.id) + expect(parent.childs.pluck(:id, :status).map(&:id)).to eql [child_1.id, child_2.id] + end + end + + describe 'transitions with persistence' do + it 'should work for valid models' do + valid_object = ValidatorNoBrainer.create(name: 'name') + expect(valid_object).to be_sleeping + valid_object.status = :running + expect(valid_object).to be_running + end + + it 'should not store states for invalid models' do + validator = ValidatorNoBrainer.create(name: 'name') + expect(validator).to be_valid + expect(validator).to be_sleeping + + validator.name = nil + expect(validator).not_to be_valid + expect { validator.run! }.to raise_error(NoBrainer::Error::DocumentInvalid) + expect(validator).to be_sleeping + + validator.reload + expect(validator).not_to be_running + expect(validator).to be_sleeping + + validator.name = 'another name' + expect(validator).to be_valid + expect(validator.run!).to be_truthy + expect(validator).to be_running + + validator.reload + expect(validator).to be_running + expect(validator).not_to be_sleeping + end + + it 'should not store states for invalid models silently if configured' do + validator = SilentPersistorNoBrainer.create(name: 'name') + expect(validator).to be_valid + expect(validator).to be_sleeping + + validator.name = nil + expect(validator).not_to be_valid + expect(validator.run!).to be_falsey + expect(validator).to be_sleeping + + validator.reload + expect(validator).not_to be_running + expect(validator).to be_sleeping + + validator.name = 'another name' + expect(validator).to be_valid + expect(validator.run!).to be_truthy + expect(validator).to be_running + + validator.reload + expect(validator).to be_running + expect(validator).not_to be_sleeping + end + + it 'should store states for invalid models if configured' do + persistor = InvalidPersistorNoBrainer.create(name: 'name') + expect(persistor).to be_valid + expect(persistor).to be_sleeping + + persistor.name = nil + + expect(persistor).not_to be_valid + expect(persistor.run!).to be_truthy + expect(persistor).to be_running + + persistor = InvalidPersistorNoBrainer.find(persistor.id) + + persistor.valid? + expect(persistor).to be_valid + expect(persistor).to be_running + expect(persistor).not_to be_sleeping + + persistor.reload + expect(persistor).to be_running + expect(persistor).not_to be_sleeping + end + end + end +end diff --git a/test/minitest_helper.rb b/test/minitest_helper.rb index 72c9bb4..88f45aa 100644 --- a/test/minitest_helper.rb +++ b/test/minitest_helper.rb @@ -34,8 +34,8 @@ begin }) Dynamoid.configure do |config| - config.namespace = "dynamoid_tests" - config.endpoint = 'http://127.0.0.1:30180' + config.namespace = 'dynamoid_tests' + config.endpoint = "http://#{ENV['DYNAMODB_HOST'] || '127.0.0.1'}:30180" config.warn_on_scan = false end