├── VERSION ├── .rspec ├── .rvmrc ├── .document ├── .travis.yml ├── lib ├── mv │ └── core │ │ ├── constraint │ │ ├── index.rb │ │ ├── builder │ │ │ ├── trigger.rb │ │ │ ├── base.rb │ │ │ ├── factory.rb │ │ │ └── index.rb │ │ ├── trigger.rb │ │ ├── description.rb │ │ ├── base.rb │ │ └── factory.rb │ │ ├── validators │ │ ├── array_validator.rb │ │ ├── valid_validator.rb │ │ └── integers_array_validator.rb │ │ ├── validation │ │ ├── exclusion.rb │ │ ├── inclusion.rb │ │ ├── active_model_presenter │ │ │ ├── absence.rb │ │ │ ├── presence.rb │ │ │ ├── uniqueness.rb │ │ │ ├── exclusion.rb │ │ │ ├── inclusion.rb │ │ │ ├── length.rb │ │ │ ├── base.rb │ │ │ └── factory.rb │ │ ├── builder │ │ │ ├── uniqueness.rb │ │ │ ├── absence.rb │ │ │ ├── presence.rb │ │ │ ├── custom.rb │ │ │ ├── inclusion.rb │ │ │ ├── exclusion.rb │ │ │ ├── base.rb │ │ │ ├── factory.rb │ │ │ └── length.rb │ │ ├── custom.rb │ │ ├── presence.rb │ │ ├── absence.rb │ │ ├── base_collection.rb │ │ ├── uniqueness.rb │ │ ├── factory.rb │ │ ├── length.rb │ │ └── base.rb │ │ ├── active_record │ │ ├── migration_decorator.rb │ │ ├── migration │ │ │ └── command_recorder_decorator.rb │ │ ├── connection_adapters │ │ │ ├── table_decorator.rb │ │ │ ├── table_definition_decorator.rb │ │ │ └── abstract_adapter_decorator.rb │ │ ├── schema_decorator.rb │ │ ├── base_decorator.rb │ │ └── schema_dumper_decorator.rb │ │ ├── route │ │ ├── index.rb │ │ ├── base.rb │ │ └── trigger.rb │ │ ├── migration │ │ ├── operations │ │ │ ├── drop_table.rb │ │ │ ├── remove_column.rb │ │ │ ├── factory.rb │ │ │ ├── list.rb │ │ │ ├── rename_table.rb │ │ │ ├── rename_column.rb │ │ │ ├── add_column.rb │ │ │ └── change_column.rb │ │ └── base.rb │ │ ├── presenter │ │ ├── constraint │ │ │ └── description.rb │ │ └── validation │ │ │ └── base.rb │ │ ├── services │ │ ├── uninstall.rb │ │ ├── compare_constraints.rb │ │ ├── parse_validation_options.rb │ │ ├── create_constraints.rb │ │ ├── delete_constraints.rb │ │ ├── create_migration_validators_table.rb │ │ ├── load_constraints.rb │ │ ├── show_constraints.rb │ │ ├── compare_constraint_arrays.rb │ │ ├── synchronize_constraints.rb │ │ └── say_constraints_diff.rb │ │ ├── router.rb │ │ ├── error.rb │ │ ├── db │ │ ├── migration_validator.rb │ │ └── helpers │ │ │ ├── table_validators.rb │ │ │ └── column_validators.rb │ │ └── railtie.rb └── mv-core.rb ├── Guardfile ├── spec ├── factories │ └── migration_validator.rb ├── mv │ └── core │ │ ├── active_record │ │ ├── migration │ │ │ └── command_recorder_decorator_spec.rb │ │ ├── schema_decorator_spec.rb │ │ ├── connection_adapters │ │ │ ├── table_decorator_spec.rb │ │ │ └── table_definition_decorator_spec.rb │ │ ├── schema_dumper_decorator_spec.rb │ │ └── base_decorator_spec.rb │ │ ├── migration │ │ └── operations │ │ │ ├── remove_column_spec.rb │ │ │ ├── rename_column_spec.rb │ │ │ ├── drop_table_spec.rb │ │ │ ├── factory_spec.rb │ │ │ ├── list_spec.rb │ │ │ ├── rename_table_spec.rb │ │ │ ├── add_column_spec.rb │ │ │ └── change_column_spec.rb │ │ ├── constraint │ │ ├── description_spec.rb │ │ ├── factory_spec.rb │ │ ├── builder │ │ │ ├── trigger_spec.rb │ │ │ ├── factory_spec.rb │ │ │ └── index_spec.rb │ │ ├── index_spec.rb │ │ └── trigger_spec.rb │ │ ├── presenter │ │ └── constraint │ │ │ └── description_spec.rb │ │ ├── error_spec.rb │ │ ├── services │ │ ├── create_migration_validators_table_spec.rb │ │ ├── uninstall_spec.rb │ │ ├── create_constraints_spec.rb │ │ ├── delete_constraints_spec.rb │ │ ├── compare_constraint_arrays_spec.rb │ │ ├── show_constraints_spec.rb │ │ ├── compare_constraints_spec.rb │ │ ├── synchronize_constraints_spec.rb │ │ ├── parse_validation_options_spec.rb │ │ ├── load_constraints_spec.rb │ │ └── say_constraints_diff_spec.rb │ │ ├── validation │ │ ├── active_model_presenter │ │ │ ├── absence_spec.rb │ │ │ ├── presence_spec.rb │ │ │ ├── uniqueness_spec.rb │ │ │ ├── exclusion_spec.rb │ │ │ ├── inclusion_spec.rb │ │ │ ├── length_spec.rb │ │ │ └── factory_spec.rb │ │ ├── builder │ │ │ ├── presence_spec.rb │ │ │ ├── absence_spec.rb │ │ │ ├── custom_spec.rb │ │ │ ├── uniqueness_spec.rb │ │ │ ├── factory_spec.rb │ │ │ ├── inclusion_spec.rb │ │ │ ├── exclusion_spec.rb │ │ │ └── length_spec.rb │ │ └── factory_spec.rb │ │ ├── router_spec.rb │ │ └── db │ │ ├── helpers │ │ └── table_validators_spec.rb │ │ └── migration_validator_spec.rb └── spec_helper.rb ├── Gemfile ├── .gitignore ├── LICENSE.txt ├── Rakefile └── Gemfile.lock /VERSION: -------------------------------------------------------------------------------- 1 | 3.0.0 -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm ruby-2.0.0@mv --create 2 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | install: bundle install --without test --jobs=3 3 | rvm: 4 | - "2.2.5" 5 | script: bundle exec rspec spec 6 | -------------------------------------------------------------------------------- /lib/mv/core/constraint/index.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Constraint 6 | class Index < Base 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rspec, cmd: 'bundle exec rspec', all_after_pass: true, failed_mode: :focus do 2 | watch(%r{^spec/mv/.+_spec\.rb$}) 3 | watch(%r{^lib/mv/(.+)\.rb$}) { |m| "spec/mv/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { "spec" } 5 | end 6 | 7 | -------------------------------------------------------------------------------- /lib/mv/core/validators/array_validator.rb: -------------------------------------------------------------------------------- 1 | class ArrayValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, value) 3 | record.errors.add(:attribute, 'must support conversion to Array (respond to :to_a method)') unless value.respond_to?(:to_a) 4 | end 5 | end -------------------------------------------------------------------------------- /lib/mv/core/constraint/builder/trigger.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Constraint 6 | module Builder 7 | class Trigger < Base 8 | delegate :event, :update?, to: :constraint 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/mv/core/validators/valid_validator.rb: -------------------------------------------------------------------------------- 1 | class ValidValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, value) 3 | if value.invalid? 4 | value.errors.full_messages.each do |message| 5 | record.errors.add(:validation, message) 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/exclusion.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base_collection' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | class Exclusion < BaseCollection 7 | protected 8 | 9 | def default_message 10 | 'is reserved' 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mv/core/validation/inclusion.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base_collection' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | class Inclusion < BaseCollection 7 | protected 8 | 9 | def default_message 10 | 'is not included in the list' 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/factories/migration_validator.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/db/migration_validator' 2 | 3 | FactoryGirl.define do 4 | factory :migration_validator, class: Mv::Core::Db::MigrationValidator do 5 | table_name :table_name 6 | column_name :column_name 7 | validation_type :uniqueness 8 | options({ as: :trigger }) 9 | end 10 | end -------------------------------------------------------------------------------- /lib/mv/core/active_record/migration_decorator.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/migration/base' 2 | 3 | module Mv 4 | module Core 5 | module ActiveRecord 6 | module MigrationDecorator 7 | def exec_migration(conn, direction) 8 | super 9 | Mv::Core::Migration::Base.execute() 10 | end 11 | end 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/active_model_presenter/absence.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module ActiveModelPresenter 7 | class Absence < Base 8 | def validation_name 9 | :absence 10 | end 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mv/core/active_record/migration/command_recorder_decorator.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module ActiveRecord 4 | module Migration 5 | module CommandRecorderDecorator 6 | def validates(*args, &block) 7 | record(:validates, args, &block) 8 | end 9 | end 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/active_model_presenter/presence.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module ActiveModelPresenter 7 | class Presence < Base 8 | def validation_name 9 | :presence 10 | end 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mv/core/active_record/connection_adapters/table_decorator.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module ActiveRecord 4 | module ConnectionAdapters 5 | module TableDecorator 6 | def validates column_name, opts 7 | @base.validates(name, column_name, opts) 8 | end 9 | end 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/active_model_presenter/uniqueness.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module ActiveModelPresenter 7 | class Uniqueness < Base 8 | def validation_name 9 | :uniqueness 10 | end 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mv/core/route/index.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Route 4 | class Index 5 | attr_reader :validation 6 | 7 | def initialize(validation) 8 | @validation = validation 9 | end 10 | 11 | def route 12 | [Mv::Core::Constraint::Description.new(validation.index_name, :index)] 13 | end 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/mv/core/active_record/schema_decorator.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/services/create_migration_validators_table' 2 | 3 | module Mv 4 | module Core 5 | module ActiveRecord 6 | module SchemaDecorator 7 | 8 | def define *args 9 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 10 | super 11 | Mv::Core::Migration::Base.execute() 12 | end 13 | end 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/active_model_presenter/exclusion.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module ActiveModelPresenter 7 | class Exclusion < Base 8 | def option_names 9 | super + [:in] 10 | end 11 | 12 | def validation_name 13 | :exclusion 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/mv/core/validation/active_model_presenter/inclusion.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module ActiveModelPresenter 7 | class Inclusion < Base 8 | def option_names 9 | super + [:in] 10 | end 11 | 12 | def validation_name 13 | :inclusion 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/mv/core/constraint/trigger.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Constraint 6 | class Trigger < Base 7 | attr_reader :event 8 | 9 | def initialize description 10 | super 11 | @event = @description.options[:event].try(:to_sym) 12 | end 13 | 14 | def update? 15 | event == :update 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mv/core/validation/active_model_presenter/length.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module ActiveModelPresenter 7 | class Length < Base 8 | def option_names 9 | super + [:is, :in, :within, :maximum, :minimum, :too_short, :too_long] 10 | end 11 | 12 | def validation_name 13 | :length 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/mv/core/migration/operations/drop_table.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/db/helpers/table_validators' 2 | 3 | module Mv 4 | module Core 5 | module Migration 6 | module Operations 7 | class DropTable 8 | include Mv::Core::Db::Helpers::TableValidators 9 | 10 | def initialize(table_name) 11 | self.table_name = table_name 12 | end 13 | 14 | def execute 15 | delete_table_validators 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /spec/mv/core/active_record/migration/command_recorder_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mv::Core::ActiveRecord::Migration::CommandRecorderDecorator do 4 | let(:recorder) { ::ActiveRecord::Migration::CommandRecorder.new } 5 | 6 | describe "#validates" do 7 | subject { recorder.validates :table_name, :column_name, length: { is: 4 } } 8 | 9 | it "should record validates action" do 10 | expect(recorder).to receive(:record).with(:validates, [:table_name, :column_name, {:length=>{:is=>4}}]) 11 | subject 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/mv/core/validators/integers_array_validator.rb: -------------------------------------------------------------------------------- 1 | class IntegersArrayValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, value) 3 | if value.respond_to?(:to_a) 4 | record.errors.add(:attribute, 5 | 'must contain positive integers only') unless value.to_a.all?{|v| v.kind_of?(Integer) && v >=0 } 6 | 7 | else 8 | record.errors.add(:attribute, 9 | 'must support conversion to Array (respond to :to_a method)') unless value.respond_to?(:to_a) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mv/core/validation/builder/uniqueness.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module Builder 7 | class Uniqueness < Base 8 | def conditions 9 | res = "NOT EXISTS(SELECT #{column_name} 10 | FROM #{table_name} 11 | WHERE #{column_reference} = #{column_name})" 12 | 13 | [{statement: apply_allow_nil_and_blank(res).squish, message: message}] 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'mv-core' 2 | require 'rspec' 3 | require 'rspec/its' 4 | require 'shoulda' 5 | require 'factory_girl' 6 | 7 | ::ActiveRecord::Migration.verbose = false 8 | 9 | require 'coveralls' 10 | Coveralls.wear! 11 | 12 | FactoryGirl.find_definitions 13 | 14 | RSpec.configure do |config| 15 | config.include FactoryGirl::Syntax::Methods 16 | 17 | config.before :each do 18 | ::ActiveRecord::Base.remove_connection if ::ActiveRecord::Base.connected? 19 | ::ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'railties', '~> 5.0' 4 | gem 'activerecord', '~> 5.0' 5 | gem 'i18n', '~> 0.7', github: 'svenfuchs/i18n' 6 | 7 | group :development do 8 | gem 'jeweler', '~> 2.0' 9 | gem 'sqlite3', '~> 1.3' 10 | gem 'guard-rspec', '~> 4.5', require: false 11 | gem 'rspec', '~> 3.1' 12 | gem 'rspec-its', '~> 1.1' 13 | gem 'shoulda', '~> 3.5' 14 | gem 'factory_girl', '~> 4.5' 15 | gem 'coveralls', '~> 0.7', require: false 16 | end 17 | 18 | group :test do 19 | gem 'pry-byebug' 20 | gem 'rb-fsevent' 21 | gem 'terminal-notifier-guard' 22 | end 23 | 24 | -------------------------------------------------------------------------------- /lib/mv/core/migration/operations/remove_column.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/db/helpers/column_validators' 2 | 3 | module Mv 4 | module Core 5 | module Migration 6 | module Operations 7 | class RemoveColumn 8 | include Mv::Core::Db::Helpers::ColumnValidators 9 | 10 | def initialize(table_name, column_name) 11 | self.table_name = table_name 12 | self.column_name = column_name 13 | end 14 | 15 | def execute 16 | delete_column_validator 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /lib/mv/core/migration/operations/factory.rb: -------------------------------------------------------------------------------- 1 | require_relative 'add_column' 2 | require_relative 'change_column' 3 | require_relative 'drop_table' 4 | require_relative 'remove_column' 5 | require_relative 'rename_column' 6 | require_relative 'rename_table' 7 | 8 | module Mv 9 | module Core 10 | module Migration 11 | module Operations 12 | class Factory 13 | def create_operation operation_name, *args 14 | "Mv::Core::Migration::Operations::#{operation_name.to_s.camelize}".constantize.new(*args) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mv/core/route/base.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/constraint/description' 2 | 3 | module Mv 4 | module Core 5 | module Route 6 | class Base 7 | attr_reader :validation 8 | 9 | def initialize(validation) 10 | @validation = validation 11 | end 12 | 13 | def route 14 | [Mv::Core::Constraint::Description.new(validation.index_name, :index)] 15 | end 16 | 17 | protected 18 | 19 | def description(name, type, opts = {}) 20 | Mv::Core::Constraint::Description.new(name, type, opts) 21 | end 22 | end 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/builder/absence.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module Builder 7 | class Absence < Base 8 | def conditions 9 | null_stmt = "#{column_reference} #{allow_nil ? 'IS' : 'IS NOT'} NULL" 10 | blank_stmt = "LENGTH(TRIM(#{column_reference})) #{allow_blank ? '=' : '>'} 0" 11 | join_stmt = allow_nil && allow_blank ? 'OR' : 'AND' 12 | 13 | [{ statement: [null_stmt, join_stmt, blank_stmt].join(' '), message: message }] 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/mv/core/validation/builder/presence.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module Builder 7 | class Presence < Base 8 | def conditions 9 | null_stmt = "#{column_reference} #{allow_nil ? 'IS' : 'IS NOT'} NULL" 10 | blank_stmt = "LENGTH(TRIM(#{column_reference})) #{allow_blank ? '=' : '>'} 0" 11 | join_stmt = allow_nil || allow_blank ? 'OR' : 'AND' 12 | 13 | [{ statement: [null_stmt, join_stmt, blank_stmt].join(' '), message: message }] 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/mv/core/migration/operations/list.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Migration 4 | module Operations 5 | class List 6 | attr_reader :operations 7 | 8 | def initialize() 9 | @operations = [] 10 | end 11 | 12 | def add_operation(operation) 13 | operations << operation 14 | end 15 | 16 | def execute 17 | operations.each(&:execute) 18 | operations.clear 19 | end 20 | 21 | def tables 22 | operations.collect(&:table_name) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/mv/core/migration/operations/rename_table.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/db/helpers/table_validators' 2 | 3 | module Mv 4 | module Core 5 | module Migration 6 | module Operations 7 | class RenameTable 8 | include Mv::Core::Db::Helpers::TableValidators 9 | 10 | attr_reader :new_table_name 11 | 12 | def initialize(table_name, new_table_name) 13 | self.table_name = table_name 14 | 15 | @new_table_name = new_table_name 16 | end 17 | 18 | def execute 19 | update_table_validators(new_table_name) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /spec/mv/core/migration/operations/remove_column_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/migration/operations/remove_column' 4 | 5 | describe Mv::Core::Migration::Operations::RemoveColumn do 6 | subject(:remove_column) do 7 | described_class.new(:table_name, :column_name) 8 | end 9 | 10 | describe "#initialize" do 11 | its(:table_name) { is_expected.to eq(:table_name) } 12 | its(:column_name) { is_expected.to eq(:column_name) } 13 | end 14 | 15 | describe "#execute" do 16 | it "calls delete column method" do 17 | expect(remove_column).to receive(:delete_column_validator) 18 | remove_column.execute 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/builder/custom.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module Builder 7 | class Custom < Base 8 | delegate :statement, to: :validation 9 | 10 | def conditions 11 | [{ 12 | statement: apply_allow_nil_and_blank("(#{preprocess_statement})"), 13 | message: message 14 | }] 15 | end 16 | 17 | private 18 | 19 | def preprocess_statement 20 | statement.gsub(/\{\s*#{column_name}\s*\}/, column_reference.to_s) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/mv/core/constraint/description.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Constraint 4 | class Description 5 | include Comparable 6 | 7 | attr_reader :name, :type, :options 8 | 9 | def initialize name, type, options = {} 10 | @name = name.to_sym 11 | @type = type.to_sym 12 | @options = options.inject({}) do |res, (name, value)| 13 | res[name.to_sym] = value.to_s 14 | res 15 | end 16 | end 17 | 18 | def <=> other_info 19 | [name, type, options] <=> [other_info.name, other_info.type, other_info.options] 20 | end 21 | end 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /lib/mv/core/presenter/constraint/description.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Presenter 4 | module Constraint 5 | class Description 6 | attr_reader :description 7 | 8 | delegate :name, :type, :options, to: :description 9 | 10 | def initialize(description) 11 | @description = description 12 | end 13 | 14 | def to_s 15 | params = [ 16 | "\"#{name}\"", 17 | options.collect{|key, value| "#{key}: :#{value}"} 18 | ].flatten.join(', ') 19 | 20 | "#{type}(#{params})" 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /lib/mv/core/services/uninstall.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/services/delete_constraints' 2 | 3 | module Mv 4 | module Core 5 | module Services 6 | class Uninstall 7 | def execute 8 | if db.data_source_exists?(:migration_validators) 9 | Mv::Core::Services::DeleteConstraints.new.execute 10 | 11 | ::ActiveRecord::Migration.say_with_time('drop migration_validators table') do 12 | db.drop_table(:migration_validators) 13 | end 14 | end 15 | end 16 | 17 | private 18 | 19 | def db 20 | ::ActiveRecord::Base.connection 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/mv/core/migration/operations/rename_column.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/db/helpers/column_validators' 2 | 3 | module Mv 4 | module Core 5 | module Migration 6 | module Operations 7 | class RenameColumn 8 | include Mv::Core::Db::Helpers::ColumnValidators 9 | 10 | attr_reader :new_column_name 11 | 12 | def initialize(table_name, column_name, new_column_name) 13 | self.table_name = table_name 14 | self.column_name = column_name 15 | 16 | @new_column_name = new_column_name 17 | end 18 | 19 | def execute 20 | rename_column(new_column_name) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/custom.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | class Custom < Base 7 | attr_reader :statement 8 | 9 | validates :statement, presence: true 10 | 11 | def initialize(table_name, column_name, opts) 12 | opts = opts.is_a?(Hash) ? opts : { statement: opts } 13 | 14 | super(table_name, column_name, opts) 15 | 16 | @statement = opts.with_indifferent_access[:statement] 17 | end 18 | 19 | def to_a 20 | super + [statement.to_s] 21 | end 22 | 23 | protected 24 | 25 | def default_message 26 | 'is invalid' 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/mv/core/constraint/description_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/constraint/description' 4 | 5 | describe Mv::Core::Constraint::Description do 6 | subject { described_class.new(:name, :type, { event: :create })} 7 | 8 | describe '#initialize' do 9 | its(:name) { is_expected.to eq(:name) } 10 | its(:type) { is_expected.to eq(:type) } 11 | its(:options) { is_expected.to eq({event: "create"}) } 12 | end 13 | 14 | describe '#<=>' do 15 | it { is_expected.to eq(described_class.new('name', 'type', { 'event' => 'create' }))} 16 | it { is_expected.to eq(described_class.new(:name, :type, { event: :create }))} 17 | it { is_expected.not_to eq(described_class.new('name', 'type', { 'event' => 'update' }))} 18 | end 19 | end -------------------------------------------------------------------------------- /lib/mv/core/router.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/constraint/description' 2 | 3 | require 'mv/core/route/trigger' 4 | require 'mv/core/route/index' 5 | 6 | module Mv 7 | module Core 8 | class Router 9 | include Singleton 10 | 11 | def route validation 12 | routing_table[validation.as.to_sym].new(validation).route 13 | end 14 | 15 | def define_route as, klass 16 | routing_table[as.to_sym] = klass 17 | end 18 | 19 | private 20 | 21 | def routing_table 22 | @routing_table ||= { 23 | trigger: Mv::Core::Route::Trigger, 24 | index: Mv::Core::Route::Index 25 | } 26 | end 27 | 28 | class << self 29 | delegate :route, :define_route, to: :instance 30 | end 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /lib/mv/core/migration/operations/add_column.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/db/helpers/column_validators' 2 | 3 | module Mv 4 | module Core 5 | module Migration 6 | module Operations 7 | class AddColumn 8 | include Mv::Core::Db::Helpers::ColumnValidators 9 | 10 | attr_reader :opts 11 | 12 | def initialize(table_name, column_name, opts = nil) 13 | self.table_name = table_name 14 | self.column_name = column_name 15 | @opts = opts || {} 16 | end 17 | 18 | def execute 19 | opts.each do |validation_type, validator_opts| 20 | create_column_validator(validation_type, validator_opts) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /lib/mv/core/active_record/base_decorator.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/validation/active_model_presenter/factory' 2 | 3 | module Mv 4 | module Core 5 | module ActiveRecord 6 | module BaseDecorator 7 | def self.included(base) 8 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 9 | Mv::Core::Db::MigrationValidator.where(table_name: base.table_name).each do |validator| 10 | presenter = Mv::Core::Validation::ActiveModelPresenter::Factory.create_presenter(validator.validation) 11 | 12 | base.validates(presenter.column_name, presenter.options) if presenter 13 | end 14 | end 15 | 16 | def enforce_migration_validations 17 | include BaseDecorator 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/mv/core/validation/presence.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | class Presence < Base 7 | include ActiveModel::Validations 8 | 9 | validate :nil_and_blank_can_not_be_both_allowed 10 | 11 | def initialize(table_name, column_name, opts) 12 | super(table_name, column_name, opts == true ? {} : opts) 13 | end 14 | 15 | protected 16 | 17 | def default_message 18 | "can't be blank" 19 | end 20 | 21 | private 22 | 23 | def nil_and_blank_can_not_be_both_allowed 24 | if allow_blank && allow_nil 25 | errors.add(:allow_blank, 'can not be allowed when nil is allowed') 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/mv/core/active_record/schema_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mv::Core::ActiveRecord::SchemaDecorator do 4 | subject(:define) { 5 | ::ActiveRecord::Schema.define(version: 20150112161454) do 6 | create_table :table_name do |t| 7 | t.string :column_name 8 | end 9 | end 10 | } 11 | 12 | it "creates migration validators table" do 13 | expect{ define }.to change{::ActiveRecord::Base.connection.data_source_exists?(:migration_validators)}.from(false).to(true) 14 | end 15 | 16 | it "calls original method" do 17 | expect{ define }.to change{::ActiveRecord::Base.connection.data_source_exists?(:table_name)}.from(false).to(true) 18 | end 19 | 20 | it "executes migration" do 21 | expect(Mv::Core::Migration::Base).to receive(:execute) 22 | define 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/mv/core/migration/operations/rename_column_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/migration/operations/rename_column' 4 | 5 | describe Mv::Core::Migration::Operations::RenameColumn do 6 | subject(:rename_column) do 7 | described_class.new(:table_name, :column_name, :new_column_name) 8 | end 9 | 10 | describe "#initialize" do 11 | its(:table_name) { is_expected.to eq(:table_name) } 12 | its(:column_name) { is_expected.to eq(:column_name) } 13 | its(:new_column_name) { is_expected.to eq(:new_column_name) } 14 | end 15 | 16 | describe "#execute" do 17 | subject(:execute) { rename_column.execute } 18 | 19 | it "should call rename_column method internally" do 20 | expect(rename_column).to receive(:rename_column).with(:new_column_name) 21 | execute 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /lib/mv/core/constraint/base.rb: -------------------------------------------------------------------------------- 1 | require_relative 'description' 2 | 3 | module Mv 4 | module Core 5 | module Constraint 6 | class Base 7 | include Comparable 8 | 9 | attr_reader :description, :validations 10 | 11 | delegate :name, :type, :options, to: :description 12 | 13 | def initialize description 14 | @description = description 15 | @validations = [] 16 | end 17 | 18 | def <=> other_constraint 19 | [self.class.name, description, validations.sort] <=> [other_constraint.class.name, other_constraint.description, other_constraint.validations.sort] 20 | end 21 | 22 | def create 23 | end 24 | 25 | def delete 26 | end 27 | 28 | def update new_constraint 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/mv/core/active_record/connection_adapters/table_definition_decorator.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/migration/base' 2 | require 'mv/core/services/parse_validation_options' 3 | 4 | module Mv 5 | module Core 6 | module ActiveRecord 7 | module ConnectionAdapters 8 | module TableDefinitionDecorator 9 | def column column_name, type, opts = {} 10 | Mv::Core::Migration::Base.add_column(name, column_name, params(opts)) 11 | 12 | super 13 | end 14 | 15 | def validates column_name, opts 16 | Mv::Core::Migration::Base.change_column(name, column_name, opts) 17 | end 18 | 19 | private 20 | 21 | def params opts 22 | Mv::Core::Services::ParseValidationOptions.new(opts).execute 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /spec/mv/core/migration/operations/drop_table_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/migration/operations/drop_table' 5 | 6 | describe Mv::Core::Migration::Operations::DropTable do 7 | subject(:drop_table_operation) do 8 | described_class.new(:table_name) 9 | end 10 | 11 | before do 12 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 13 | end 14 | 15 | describe "#initialize" do 16 | its(:table_name) { is_expected.to eq(:table_name) } 17 | end 18 | 19 | describe "#execute" do 20 | subject(:execute) { drop_table_operation.execute } 21 | 22 | it "should call delete_table_validators method" do 23 | expect(drop_table_operation).to receive(:delete_table_validators).and_call_original 24 | execute 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /lib/mv/core/constraint/factory.rb: -------------------------------------------------------------------------------- 1 | require_relative 'trigger' 2 | require_relative 'index' 3 | 4 | module Mv 5 | module Core 6 | module Constraint 7 | class Factory 8 | include Singleton 9 | 10 | def create_constraint description 11 | factory_map[description.type.to_sym].new(description) 12 | end 13 | 14 | def register_constraint type, klass 15 | factory_map[type.to_sym] = klass 16 | end 17 | 18 | class << self 19 | delegate :create_constraint, :register_constraint, to: :instance 20 | end 21 | 22 | private 23 | 24 | def factory_map 25 | @factory_map ||= { 26 | trigger: Mv::Core::Constraint::Trigger, 27 | index: Mv::Core::Constraint::Index 28 | } 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/mv/core/error.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | class Error < StandardError 4 | attr_reader :table_name, :column_name, :validation_type, :options, :error 5 | 6 | def initialize(opts = {}) 7 | opts = opts.with_indifferent_access 8 | 9 | @table_name = opts[:table_name] 10 | @column_name = opts[:column_name] 11 | @validation_type = opts[:validation_type] 12 | @options = opts[:options] 13 | @error = opts[:error] 14 | 15 | super [ 16 | @table_name ? "table: '#{@table_name}'" : nil, 17 | @column_name ? "column: '#{@column_name}'" : nil, 18 | @validation_type ? "validation: '#{@validation_type}'" : nil, 19 | @options ? "options: '#{@options}'" : nil, 20 | @error ? "error: '#{@error}'" : nil, 21 | ].compact.join(', ') 22 | end 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /spec/mv/core/migration/operations/factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/migration/operations/factory' 4 | require 'mv/core/migration/operations/add_column' 5 | 6 | describe Mv::Core::Migration::Operations::Factory do 7 | subject(:operations_factory) { described_class.new } 8 | 9 | describe "#create_operation" do 10 | subject { operations_factory.create_operation(:add_column, :table_name, :column_name, length: { id: 5 }) } 11 | 12 | it "should create AddColumn operation instance" do 13 | expect(Mv::Core::Migration::Operations::AddColumn).to receive(:new).with( 14 | :table_name, :column_name, length: { id: 5 } 15 | ) 16 | 17 | subject 18 | end 19 | 20 | it "should return newly created operations" do 21 | expect(subject).to be_kind_of(Mv::Core::Migration::Operations::AddColumn) 22 | end 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /lib/mv/core/route/trigger.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Route 4 | class Trigger 5 | attr_reader :validation 6 | 7 | def initialize(validation) 8 | @validation = validation 9 | end 10 | 11 | def route 12 | [validation.create? && Mv::Core::Constraint::Description.new(validation.create_trigger_name, 13 | :trigger, 14 | { event: :create }), 15 | validation.update? && Mv::Core::Constraint::Description.new(validation.update_trigger_name, 16 | :trigger, 17 | { event: :update })].select(&:present?) 18 | end 19 | end 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /lib/mv/core/db/migration_validator.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/validation/factory' 2 | require 'mv/core/constraint/description' 3 | require 'mv/core/validators/valid_validator' 4 | 5 | module Mv 6 | module Core 7 | module Db 8 | class MigrationValidator < ::ActiveRecord::Base 9 | serialize :options, Hash 10 | 11 | validates :table_name, presence: true 12 | validates :column_name, presence: true 13 | validates :validation_type, presence: true 14 | 15 | validates :validation, valid: true 16 | 17 | def validation 18 | Mv::Core::Validation::Factory.create_validation(table_name, 19 | column_name, 20 | validation_type, 21 | options) 22 | end 23 | end 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /lib/mv/core/services/compare_constraints.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Services 4 | class CompareConstraints 5 | attr_reader :old_constraint, :new_constraint 6 | 7 | def initialize(old_constraint, new_constraint) 8 | @old_constraint = old_constraint 9 | @new_constraint = new_constraint 10 | end 11 | 12 | def execute 13 | { 14 | deleted: old_validations - new_validations, 15 | added: new_validations - old_validations 16 | }.delete_if{ |key, value| value.blank? } 17 | end 18 | 19 | private 20 | 21 | def old_validations 22 | @old_validations ||= old_constraint.try(:validations) || [] 23 | end 24 | 25 | def new_validations 26 | @new_validations ||= new_constraint.try(:validations) || [] 27 | end 28 | end 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /lib/mv/core/services/parse_validation_options.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/validation/factory' 2 | 3 | module Mv 4 | module Core 5 | module Services 6 | class ParseValidationOptions 7 | attr_reader :opts 8 | 9 | def initialize(opts) 10 | @opts = opts || {} 11 | end 12 | 13 | def execute 14 | validates = opts.delete(:validates) || opts.delete("validates") 15 | 16 | return validates if validates.is_a?(Hash) 17 | return { custom: validates } if validates.present? 18 | 19 | Mv::Core::Validation::Factory.registered_validations.inject({}) do |res, validation_type| 20 | validation_options = opts.delete(validation_type.to_sym) || opts.delete(validation_type.to_s) 21 | res[validation_type] = validation_options if validation_options 22 | res 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/mv/core/migration/operations/change_column.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/db/helpers/column_validators' 2 | 3 | module Mv 4 | module Core 5 | module Migration 6 | module Operations 7 | class ChangeColumn 8 | include Mv::Core::Db::Helpers::ColumnValidators 9 | 10 | attr_reader :opts 11 | 12 | def initialize(table_name, column_name, opts = nil) 13 | self.table_name = table_name 14 | self.column_name = column_name 15 | @opts = opts || {} 16 | end 17 | 18 | def execute 19 | if opts.present? 20 | opts.each do |validation_type, validator_opts| 21 | update_column_validator(validation_type, validator_opts) 22 | end 23 | elsif column_validators.exists? 24 | delete_column_validator 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /lib/mv/core/active_record/schema_dumper_decorator.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/db/migration_validator' 2 | require 'mv/core/services/create_migration_validators_table' 3 | require 'mv/core/presenter/validation/base' 4 | 5 | module Mv 6 | module Core 7 | module ActiveRecord 8 | module SchemaDumperDecorator 9 | def self.prepended(mod) 10 | mod.ignore_tables << 'migration_validators' 11 | end 12 | 13 | def dump(stream) 14 | Mv::Core::Services::CreateMigrationValidatorsTable.new(@connection).execute 15 | super 16 | end 17 | 18 | def trailer(stream) 19 | Mv::Core::Db::MigrationValidator.all.each do |migration_validator| 20 | stream.puts("#{Mv::Core::Presenter::Validation::Base.new(migration_validator.validation)}".prepend(' ')) 21 | end 22 | 23 | stream.puts('') 24 | super 25 | end 26 | end 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /spec/mv/core/presenter/constraint/description_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/presenter/constraint/description' 4 | 5 | describe Mv::Core::Presenter::Constraint::Description do 6 | let(:description) { Mv::Core::Constraint::Description.new(:trg_name, :trigger, opts)} 7 | 8 | subject(:presenter) { described_class.new(description) } 9 | 10 | describe "#initialize" do 11 | let(:opts) { {} } 12 | 13 | its(:description) { is_expected.to eq(description) } 14 | end 15 | 16 | describe "#to_s" do 17 | subject { "#{presenter}" } 18 | 19 | describe "when options hash is empty" do 20 | let(:opts) { {} } 21 | 22 | it { is_expected.to eq("trigger(\"trg_name\")") } 23 | end 24 | 25 | describe "when options hash is not empty" do 26 | let(:opts) { { event: :create } } 27 | 28 | it { is_expected.to eq("trigger(\"trg_name\", event: :create)") } 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/absence.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | class Absence < Base 7 | include ActiveModel::Validations 8 | 9 | validate :nil_and_blank_can_not_be_both_denied 10 | 11 | def initialize(table_name, column_name, opts) 12 | super(table_name, column_name, opts == true ? {} : opts) 13 | end 14 | 15 | protected 16 | 17 | def default_allow_nil 18 | true 19 | end 20 | 21 | def default_allow_blank 22 | true 23 | end 24 | 25 | def default_message 26 | 'must be blank' 27 | end 28 | 29 | private 30 | 31 | def nil_and_blank_can_not_be_both_denied 32 | if !(allow_blank || allow_nil) 33 | errors.add(:allow_blank, 'can not be denied when nil is denied') 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | 4 | # rdoc generated 5 | rdoc 6 | 7 | # yard generated 8 | doc 9 | .yardoc 10 | 11 | # bundler 12 | .bundle 13 | 14 | # jeweler generated 15 | pkg 16 | 17 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 18 | # 19 | # * Create a file at ~/.gitignore 20 | # * Include files you want ignored 21 | # * Run: git config --global core.excludesfile ~/.gitignore 22 | # 23 | # After doing this, these files will be ignored in all your git projects, 24 | # saving you from having to 'pollute' every project you touch with them 25 | # 26 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 27 | # 28 | # For MacOS: 29 | # 30 | #.DS_Store 31 | # 32 | # For TextMate 33 | #*.tmproj 34 | #tmtags 35 | # 36 | # For emacs: 37 | #*~ 38 | #\#* 39 | #.\#* 40 | # 41 | #For vim: 42 | *.swp 43 | 44 | #Coveralls.io 45 | .coveralls.yml 46 | 47 | .ruby-version 48 | .pryrc 49 | 50 | vendor/** 51 | -------------------------------------------------------------------------------- /lib/mv/core/validation/base_collection.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/validators/array_validator' 2 | require_relative 'base' 3 | 4 | module Mv 5 | module Core 6 | module Validation 7 | class BaseCollection < Base 8 | include ActiveModel::Validations 9 | 10 | attr_reader :in 11 | 12 | validates :in, presence: true, array: true 13 | 14 | def initialize(table_name, column_name, opts) 15 | opts = opts.is_a?(Hash) ? opts : { in: opts } 16 | 17 | super(table_name, column_name, opts) 18 | 19 | @in = opts.with_indifferent_access[:in] 20 | end 21 | 22 | def to_a 23 | prepared_in = self.in.is_a?(Range) ? range_to_a : self.in.try(:sort) 24 | super + [prepared_in] 25 | end 26 | 27 | protected 28 | 29 | def range_to_a 30 | min = self.in.min 31 | max = self.in.exclude_end? ? self.in.last - 0.000000001.second : self.in.last 32 | 33 | [min, max] 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/mv/core/migration/operations/list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/migration/operations/list' 4 | 5 | describe Mv::Core::Migration::Operations::List do 6 | subject(:list) { described_class.new } 7 | 8 | describe "#initialize" do 9 | its(:operations) { is_expected.to eq([]) } 10 | end 11 | 12 | describe "#execute" do 13 | let(:operation) { double(execute: true) } 14 | 15 | before do 16 | list.add_operation(operation) 17 | end 18 | 19 | it "calls execute on all operations list" do 20 | expect(operation).to receive(:execute).once 21 | list.execute 22 | end 23 | 24 | it "removes all operations after execute" do 25 | expect{ list.execute }.to change(list.operations, :count).to(0) 26 | end 27 | end 28 | 29 | describe "#tables" do 30 | it "gathers all operation tables" do 31 | operation = double(table_name: :table_name) 32 | list.add_operation(operation) 33 | 34 | expect(list.tables).to eq([:table_name]) 35 | end 36 | end 37 | 38 | end -------------------------------------------------------------------------------- /lib/mv/core/db/helpers/table_validators.rb: -------------------------------------------------------------------------------- 1 | require_relative '../migration_validator' 2 | 3 | module Mv 4 | module Core 5 | module Db 6 | module Helpers 7 | module TableValidators 8 | attr_accessor :table_name 9 | 10 | def table_validators 11 | Mv::Core::Db::MigrationValidator.where(table_name: table_name) 12 | end 13 | 14 | def delete_table_validators 15 | delete_validators(table_validators) 16 | end 17 | 18 | def update_table_validators new_table_name 19 | table_validators.update_all(table_name: new_table_name) 20 | end 21 | 22 | private 23 | 24 | def say(message) 25 | ::ActiveRecord::Migration.say(message, true) 26 | end 27 | 28 | def delete_validators validators 29 | validators.each do |validator| 30 | validator.destroy 31 | end 32 | validators.length 33 | end 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/mv/core/active_record/connection_adapters/table_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mv::Core::ActiveRecord::ConnectionAdapters::TableDecorator do 4 | let(:conn) { ::ActiveRecord::Base.connection } 5 | 6 | before do 7 | conn.class.send( 8 | :prepend, Mv::Core::ActiveRecord::ConnectionAdapters::AbstractAdapterDecorator 9 | ) 10 | 11 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 12 | end 13 | 14 | describe "#validates" do 15 | before do 16 | conn.create_table :table_name, id: false do |t| 17 | t.string :column_name 18 | end 19 | end 20 | 21 | subject :change_table do 22 | conn.change_table :table_name do |t| 23 | t.validates :column_name, length: { is: 5 } 24 | end 25 | end 26 | 27 | it "calls migration change_column method" do 28 | expect(Mv::Core::Migration::Base).to receive(:change_column).with( 29 | :table_name, :column_name, length: { is: 5 } 30 | ).at_least(:once) 31 | change_table 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /spec/mv/core/migration/operations/rename_table_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/migration/operations/rename_table' 5 | 6 | describe Mv::Core::Migration::Operations::RenameTable do 7 | subject(:rename_table_operation) do 8 | described_class.new(:table_name, :new_table_name) 9 | end 10 | 11 | before do 12 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 13 | end 14 | 15 | describe "#initialize" do 16 | its(:table_name) { is_expected.to eq(:table_name) } 17 | its(:new_table_name) { is_expected.to eq(:new_table_name) } 18 | end 19 | 20 | describe "#execute" do 21 | subject(:execute) { rename_table_operation.execute } 22 | 23 | it "should call delete_table_validators method" do 24 | expect(rename_table_operation).to receive(:update_table_validators) 25 | .with(:new_table_name) 26 | .and_call_original 27 | execute 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Valeriy Prokopchuk 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 | -------------------------------------------------------------------------------- /lib/mv/core/services/create_constraints.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/services/load_constraints' 2 | 3 | module Mv 4 | module Core 5 | module Services 6 | class CreateConstraints 7 | attr_reader :tables 8 | 9 | def initialize(tables) 10 | @tables = tables.present? ? tables : Mv::Core::Db::MigrationValidator.pluck(:table_name) 11 | end 12 | 13 | def execute 14 | constraints_comparizon = Mv::Core::Services::CompareConstraintArrays.new([], existing_constraints) 15 | .execute 16 | Mv::Core::Services::SynchronizeConstraints.new(constraints_comparizon[:added], 17 | constraints_comparizon[:updated], 18 | constraints_comparizon[:deleted]) 19 | .execute 20 | end 21 | 22 | private 23 | 24 | def existing_constraints 25 | Mv::Core::Services::LoadConstraints.new(tables).execute 26 | end 27 | end 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /lib/mv/core/services/delete_constraints.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/services/load_constraints' 2 | 3 | module Mv 4 | module Core 5 | module Services 6 | class DeleteConstraints 7 | attr_reader :tables 8 | 9 | def initialize(tables = []) 10 | @tables = tables.present? ? tables : Mv::Core::Db::MigrationValidator.pluck(:table_name) 11 | end 12 | 13 | def execute 14 | constraints_comparizon = Mv::Core::Services::CompareConstraintArrays.new(existing_constraints, []) 15 | .execute 16 | Mv::Core::Services::SynchronizeConstraints.new(constraints_comparizon[:added], 17 | constraints_comparizon[:updated], 18 | constraints_comparizon[:deleted]) 19 | .execute 20 | end 21 | 22 | private 23 | 24 | def existing_constraints 25 | Mv::Core::Services::LoadConstraints.new(tables).execute 26 | end 27 | end 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /lib/mv/core/services/create_migration_validators_table.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Services 4 | class CreateMigrationValidatorsTable 5 | attr_reader :db 6 | delegate :create_table, :data_source_exists?, :add_index, to: :db 7 | 8 | def initialize db = ::ActiveRecord::Base.connection 9 | @db = db 10 | end 11 | 12 | def execute 13 | unless data_source_exists?(:migration_validators) 14 | ::ActiveRecord::Migration.say_with_time('initialize migration_validators table') do 15 | create_table(:migration_validators) do |table| 16 | table.string :table_name, null: false 17 | table.string :column_name, null: false 18 | table.string :validation_type, null: false 19 | table.string :options 20 | end 21 | 22 | add_index(:migration_validators, 23 | [:table_name, :column_name, :validation_type], 24 | name: 'unique_idx_on_migration_validators') 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/mv/core/constraint/builder/base.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/validation/builder/factory' 2 | 3 | module Mv 4 | module Core 5 | module Constraint 6 | module Builder 7 | class Base 8 | attr_reader :constraint 9 | 10 | delegate :name, to: :constraint 11 | 12 | def initialize(constraint) 13 | @constraint = constraint 14 | end 15 | 16 | def create 17 | end 18 | 19 | def delete 20 | end 21 | 22 | def update new_constraint_builder 23 | end 24 | 25 | def self.validation_builders_factory 26 | @validation_builders_factory ||= Mv::Core::Validation::Builder::Factory.new 27 | end 28 | 29 | def validation_builders 30 | @validation_builders ||= constraint.validations.collect do |validation| 31 | self.class.validation_builders_factory.create_builder(validation) 32 | end 33 | end 34 | 35 | protected 36 | 37 | 38 | def db 39 | ::ActiveRecord::Base.connection 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /spec/mv/core/error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/error' 4 | 5 | describe Mv::Core::Error do 6 | describe "#initialization" do 7 | describe "default" do 8 | subject(:error) do 9 | described_class.new() 10 | end 11 | 12 | its(:message) { is_expected.to be_blank } 13 | end 14 | describe "full" do 15 | subject(:error) do 16 | described_class.new(table_name: :table_name, 17 | column_name: :column_name, 18 | validation_type: :uniqueness, 19 | options: { as: :trigger}, 20 | error: "Some error happened") 21 | end 22 | 23 | its(:table_name) { is_expected.to eq(:table_name) } 24 | its(:column_name) { is_expected.to eq(:column_name) } 25 | its(:validation_type) { is_expected.to eq(:uniqueness) } 26 | its(:options) { is_expected.to eq('as' => :trigger)} 27 | its(:message) { 28 | is_expected.to eq( 29 | "table: 'table_name', column: 'column_name', validation: 'uniqueness', options: '{\"as\"=>:trigger}', error: 'Some error happened'" 30 | ) 31 | } 32 | end 33 | end 34 | 35 | end -------------------------------------------------------------------------------- /spec/mv/core/services/create_migration_validators_table_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | 5 | describe Mv::Core::Services::CreateMigrationValidatorsTable do 6 | describe "#execute" do 7 | subject(:create_migration_validators_table) { described_class.new.execute } 8 | 9 | describe "when table does not exist" do 10 | it "creates it" do 11 | expect { subject }.to change{ 12 | ActiveRecord::Base.connection.data_source_exists?(:migration_validators) 13 | }.from(false).to(true) 14 | end 15 | 16 | it "says that table is created" do 17 | expect(::ActiveRecord::Migration).to receive(:say_with_time).with('initialize migration_validators table') 18 | subject 19 | end 20 | end 21 | 22 | describe "when table already exist" do 23 | before do 24 | create_migration_validators_table 25 | end 26 | 27 | it "does not raise an error" do 28 | expect{ subject }.not_to raise_error 29 | end 30 | 31 | it "says nothing" do 32 | expect(::ActiveRecord::Migration).not_to receive(:say_with_time) 33 | subject 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/mv/core/validation/uniqueness.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | class Uniqueness < Base 7 | include ActiveModel::Validations 8 | 9 | attr_reader :index_name 10 | 11 | validates :index_name, absence: { message: 'allowed when :as == :index' }, unless: :index? 12 | 13 | def initialize(table_name, column_name, opts) 14 | opts = opts == true ? {} : opts 15 | 16 | super(table_name, column_name, opts) 17 | 18 | @index_name = opts.with_indifferent_access[:index_name] || default_index_name 19 | end 20 | 21 | def to_a 22 | super + [index_name.to_s] 23 | end 24 | 25 | protected 26 | 27 | def available_as 28 | super + [:index] 29 | end 30 | 31 | def default_as 32 | :index 33 | end 34 | 35 | def default_index_name 36 | "idx_mv_#{table_name}_#{column_name}_uniq" if index? 37 | end 38 | 39 | def default_message 40 | 'is not unique' 41 | end 42 | 43 | private 44 | 45 | def index? 46 | as.to_sym == :index 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/mv/core/constraint/builder/factory.rb: -------------------------------------------------------------------------------- 1 | require_relative 'index' 2 | require_relative 'trigger' 3 | 4 | module Mv 5 | module Core 6 | module Constraint 7 | module Builder 8 | class Factory 9 | include Singleton 10 | 11 | def create_builder constraint 12 | factory_map[constraint.class].new(constraint) 13 | end 14 | 15 | def register_builder constraint_class, builder_class 16 | factory_map[constraint_class] = builder_class 17 | end 18 | 19 | def register_builders opts 20 | opts.each do |constraint_class, builder_class| 21 | register_builder(constraint_class, builder_class) 22 | end 23 | end 24 | 25 | class << self 26 | delegate :create_builder, :register_builder, :register_builders, to: :instance 27 | end 28 | 29 | 30 | private 31 | 32 | def factory_map 33 | @factory_map ||= { 34 | Mv::Core::Constraint::Index => Mv::Core::Constraint::Builder::Index, 35 | Mv::Core::Constraint::Trigger => Mv::Core::Constraint::Builder::Trigger 36 | } 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/mv/core/services/load_constraints.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/db/migration_validator' 2 | require 'mv/core/router' 3 | require 'mv/core/constraint/factory' 4 | 5 | module Mv 6 | module Core 7 | module Services 8 | class LoadConstraints 9 | attr_reader :tables 10 | 11 | def initialize(tables) 12 | @tables = tables 13 | end 14 | 15 | def execute 16 | res = [] 17 | 18 | Mv::Core::Db::MigrationValidator.where(table_name: tables).each do |migration_validator| 19 | validation = migration_validator.validation 20 | descriptions = Mv::Core::Router.route(validation) 21 | 22 | descriptions.each do |description| 23 | add_constraint(res, description).validations << validation 24 | end 25 | end 26 | 27 | return res 28 | end 29 | 30 | private 31 | 32 | def add_constraint(constraints_list, description) 33 | res = constraints_list.find{|constraint| constraint.description == description} 34 | 35 | unless res 36 | res = Mv::Core::Constraint::Factory.create_constraint(description) 37 | constraints_list << res 38 | end 39 | 40 | res 41 | end 42 | end 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/active_model_presenter/base.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Validation 4 | module ActiveModelPresenter 5 | class Base 6 | attr_reader :validation 7 | 8 | delegate :column_name, to: :validation 9 | 10 | def initialize(validation) 11 | @validation = validation 12 | end 13 | 14 | def options 15 | unless @options 16 | @options = option_names.inject({}) do |res, option_name| 17 | option_value = validation.send(option_name) 18 | res[option_name] = option_value if user_options.has_key?(option_name) && 19 | !option_value.nil? 20 | res 21 | end 22 | 23 | @options = true if @options.blank? 24 | @options = { validation_name => @options } 25 | end 26 | 27 | @options 28 | end 29 | 30 | def option_names 31 | [:on, :allow_blank, :allow_nil, :message] 32 | end 33 | 34 | 35 | private 36 | 37 | def user_options 38 | @user_options ||= validation.options.symbolize_keys 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /lib/mv/core/services/show_constraints.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/services/load_constraints' 2 | require 'mv/core/presenter/constraint/description' 3 | require 'mv/core/presenter/validation/base' 4 | 5 | module Mv 6 | module Core 7 | module Services 8 | class ShowConstraints 9 | attr_reader :tables 10 | 11 | def initialize(tables) 12 | @tables = tables.present? ? tables : Mv::Core::Db::MigrationValidator.pluck(:table_name) 13 | end 14 | 15 | def execute 16 | existing_constraints.each do |constraint| 17 | say_with_time("#{Mv::Core::Presenter::Constraint::Description.new(constraint.description)}") do 18 | constraint.validations.each do |validation| 19 | say("#{Mv::Core::Presenter::Validation::Base.new(validation)}") 20 | end 21 | end 22 | end 23 | end 24 | 25 | private 26 | 27 | def say_with_time msg, &block 28 | ::ActiveRecord::Migration::say_with_time(msg, &block) 29 | end 30 | 31 | def say msg 32 | ::ActiveRecord::Migration::say(msg, true) 33 | end 34 | 35 | def existing_constraints 36 | Mv::Core::Services::LoadConstraints.new(tables).execute 37 | end 38 | end 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /spec/mv/core/active_record/schema_dumper_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mv::Core::ActiveRecord::SchemaDumperDecorator do 4 | before do 5 | ::ActiveRecord::Migration.verbose = false 6 | end 7 | 8 | let(:out) { StringIO.new() } 9 | let(:dump) { ::ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, out) } 10 | 11 | describe "when migration_validators table exists" do 12 | let(:migration_validator) { create(:migration_validator) } 13 | 14 | before do 15 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 16 | migration_validator 17 | 18 | dump 19 | end 20 | 21 | subject { out.string } 22 | 23 | it { is_expected.to include("#{Mv::Core::Presenter::Validation::Base.new(migration_validator.validation)}")} 24 | it { is_expected.not_to include("create_table \"migration_validators\"") } 25 | end 26 | 27 | describe "when migration_validators table does not exist" do 28 | subject { dump } 29 | 30 | it "should not raise an error" do 31 | expect{ subject }.not_to raise_error 32 | end 33 | 34 | it "should create migration_validators table" do 35 | expect{ subject }.to change{ ::ActiveRecord::Base.connection.data_source_exists?(:migration_validators) }.from(false).to(true) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/mv/core/services/uninstall_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/uninstall' 4 | 5 | describe Mv::Core::Services::Uninstall do 6 | let(:migration_validator) { 7 | create(:migration_validator, table_name: :table_name, 8 | validation_type: :uniqueness, 9 | options: { as: :index }) 10 | } 11 | 12 | before do 13 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 14 | migration_validator 15 | end 16 | 17 | subject(:service) { described_class.new } 18 | 19 | describe "#execute" do 20 | subject { service.execute } 21 | 22 | it "removes constraints" do 23 | expect_any_instance_of(Mv::Core::Constraint::Builder::Index).to receive(:delete) 24 | 25 | subject 26 | end 27 | 28 | it "removes migration_validators table" do 29 | expect{ subject }.to change{::ActiveRecord::Base.connection.data_source_exists?(:migration_validators)}.from(true).to(false) 30 | end 31 | 32 | describe "when migration validators table does not exist" do 33 | before do 34 | ::ActiveRecord::Base.connection.drop_table(:migration_validators) 35 | end 36 | 37 | it "should not raise an error" do 38 | expect{ subject }.not_to raise_error 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/mv/core/constraint/factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/constraint/factory' 4 | 5 | describe Mv::Core::Constraint::Factory do 6 | describe "#create_constraint" do 7 | subject { described_class.create_constraint(description) } 8 | 9 | describe "by default" do 10 | describe "trigger" do 11 | let(:description) { 12 | Mv::Core::Constraint::Description.new(:update_trigger_name, :trigger) 13 | } 14 | 15 | it { is_expected.to be_instance_of(Mv::Core::Constraint::Trigger) } 16 | end 17 | 18 | describe "index" do 19 | let(:description) { 20 | Mv::Core::Constraint::Description.new(:update_trigger_name, :index) 21 | } 22 | 23 | it { is_expected.to be_instance_of(Mv::Core::Constraint::Index) } 24 | end 25 | end 26 | 27 | describe "when custom constraint provided" do 28 | let(:klass) { Class.new(Mv::Core::Constraint::Trigger) } 29 | 30 | let(:description) { 31 | Mv::Core::Constraint::Description.new(:update_trigger_name, :trigger) 32 | } 33 | 34 | before { described_class.register_constraint(:trigger, klass) } 35 | 36 | it { is_expected.to be_instance_of(klass) } 37 | 38 | after { described_class.register_constraint(:trigger, Mv::Core::Constraint::Trigger)} 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /spec/mv/core/constraint/builder/trigger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/constraint/trigger' 5 | require 'mv/core/constraint/builder/trigger' 6 | 7 | describe Mv::Core::Constraint::Builder::Trigger do 8 | before do 9 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 10 | Mv::Core::Db::MigrationValidator.delete_all 11 | end 12 | 13 | let(:trigger_description) { Mv::Core::Constraint::Description.new(:idx_mv_table_name, :trigger, opts) } 14 | let(:trigger_constraint) { Mv::Core::Constraint::Trigger.new(trigger_description) } 15 | let(:uniqueness) { 16 | Mv::Core::Validation::Uniqueness.new(:table_name, 17 | :column_name, 18 | as: :trigger, 19 | update_trigger_name: :trg_mv_table_name) 20 | } 21 | 22 | before do 23 | trigger_constraint.validations << uniqueness 24 | end 25 | 26 | subject(:trigger_builder) { described_class.new(trigger_constraint) } 27 | 28 | describe "#initialization" do 29 | let(:opts) { { event: :create } } 30 | 31 | its(:constraint) { is_expected.to eq(trigger_constraint) } 32 | its(:event) { is_expected.to eq(:create) } 33 | its(:update?) { is_expected.to be_falsey } 34 | end 35 | 36 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/builder/inclusion.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module Builder 7 | class Inclusion < Base 8 | delegate :in, to: :validation 9 | 10 | def conditions 11 | [{ 12 | statement: apply_allow_nil_and_blank(apply_in(column_reference)), 13 | message: message 14 | }] 15 | end 16 | 17 | protected 18 | 19 | def db_value value 20 | return value if value.is_a?(Integer) or value.is_a?(Float) 21 | return "'#{value.to_s}'" if value.is_a?(String) 22 | raise Mv::Core::Error.new(table_name: table_name, 23 | column_name: column_name, 24 | validation_type: :inclusion, 25 | options: { in: value }, 26 | error: "#{value.class} is not supported as :in value") 27 | end 28 | 29 | def apply_in stmt 30 | if self.in.is_a?(Range) 31 | "#{stmt} BETWEEN #{db_value(self.in.min)} AND #{db_value(self.in.max)}" 32 | else 33 | prepared_in = self.in.to_a.collect{ |v| db_value(v) } 34 | 35 | "#{stmt} IN (#{prepared_in.join(', ')})" 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/mv-core.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | require 'active_record' 3 | 4 | require 'mv/core/active_record/connection_adapters/abstract_adapter_decorator' 5 | require 'mv/core/active_record/connection_adapters/table_definition_decorator' 6 | require 'mv/core/active_record/connection_adapters/table_decorator' 7 | require 'mv/core/active_record/schema_dumper_decorator' 8 | require 'mv/core/active_record/schema_decorator' 9 | require 'mv/core/active_record/base_decorator' 10 | require 'mv/core/active_record/migration_decorator' 11 | require 'mv/core/active_record/migration/command_recorder_decorator' 12 | require 'mv/core/railtie' 13 | 14 | ActiveSupport.on_load(:active_record) do 15 | ::ActiveRecord::ConnectionAdapters::TableDefinition.send(:prepend, Mv::Core::ActiveRecord::ConnectionAdapters::TableDefinitionDecorator) 16 | ::ActiveRecord::ConnectionAdapters::Table.send(:prepend, Mv::Core::ActiveRecord::ConnectionAdapters::TableDecorator) 17 | ::ActiveRecord::SchemaDumper.send(:prepend, Mv::Core::ActiveRecord::SchemaDumperDecorator) 18 | ::ActiveRecord::Schema.send(:prepend, Mv::Core::ActiveRecord::SchemaDecorator) 19 | ::ActiveRecord::Base.send(:extend, Mv::Core::ActiveRecord::BaseDecorator) 20 | ::ActiveRecord::Migration.send(:prepend, Mv::Core::ActiveRecord::MigrationDecorator) 21 | ::ActiveRecord::Migration::CommandRecorder.send(:prepend, Mv::Core::ActiveRecord::Migration::CommandRecorderDecorator) 22 | 23 | ActiveSupport.run_load_hooks(:mv_core) 24 | end 25 | 26 | -------------------------------------------------------------------------------- /lib/mv/core/constraint/builder/index.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Constraint 6 | module Builder 7 | class Index < Base 8 | def create 9 | super 10 | 11 | constraint.validations.group_by(&:table_name).each do |table_name, validations| 12 | remove_index(table_name) 13 | add_index(table_name, validations.collect(&:column_name)) 14 | end 15 | end 16 | 17 | def delete 18 | super 19 | 20 | constraint.validations.group_by(&:table_name).each do |table_name, validations| 21 | remove_index(table_name) 22 | end 23 | end 24 | 25 | def update new_constraint_builder 26 | super 27 | 28 | delete 29 | new_constraint_builder.create 30 | end 31 | 32 | private 33 | 34 | def index_exists?(table_name) 35 | db.data_source_exists?(table_name) && 36 | db.index_name_exists?(table_name, name, false) 37 | end 38 | 39 | def remove_index(table_name) 40 | db.remove_index(table_name, name: name) if index_exists?(table_name) 41 | end 42 | 43 | def add_index(table_name, columns) 44 | db.add_index(table_name, columns, name: name, unique: true) 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/mv/core/validation/builder/exclusion.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module Builder 7 | class Exclusion < Base 8 | delegate :in, to: :validation 9 | 10 | def conditions 11 | [{ 12 | statement: apply_allow_nil_and_blank(apply_in(column_reference)), 13 | message: message 14 | }] 15 | end 16 | 17 | protected 18 | 19 | def db_value value 20 | return value if value.is_a?(Integer) or value.is_a?(Float) 21 | return "'#{value.to_s}'" if value.is_a?(String) 22 | raise Mv::Core::Error.new(table_name: table_name, 23 | column_name: column_name, 24 | validation_type: :inclusion, 25 | options: { in: value }, 26 | error: "#{value.class} is not supported as :in value") 27 | end 28 | 29 | def apply_in stmt 30 | if self.in.is_a?(Range) 31 | "#{stmt} < #{db_value(self.in.min)} OR #{stmt} > #{db_value(self.in.max)}" 32 | else 33 | prepared_in = self.in.to_a.collect{ |v| db_value(v) } 34 | 35 | "#{stmt} NOT IN (#{prepared_in.join(', ')})" 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/mv/core/validation/active_model_presenter/absence_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/active_model_presenter/absence' 4 | 5 | describe Mv::Core::Validation::ActiveModelPresenter::Absence do 6 | subject(:validation) { 7 | Mv::Core::Validation::Absence.new(:table_name, 8 | :column_name, 9 | opts) 10 | } 11 | 12 | subject(:acive_model_presenter) { described_class.new(validation) } 13 | 14 | describe "#initialize" do 15 | let(:opts) { true } 16 | 17 | its(:validation) { is_expected.to eq(validation) } 18 | its(:column_name) { is_expected.to eq(:column_name) } 19 | end 20 | 21 | describe "#options" do 22 | subject { acive_model_presenter.options } 23 | 24 | describe "when empty properties has specified" do 25 | let(:opts) { {} } 26 | 27 | it { is_expected.to eq(absence: true) } 28 | end 29 | 30 | describe "when properties has specified" do 31 | let(:opts) { { 32 | on: :create, 33 | as: :trigger, 34 | allow_blank: true, 35 | allow_nil: true, 36 | message: 'some error message' 37 | } } 38 | 39 | it { is_expected.to eq( 40 | absence: { on: :create, 41 | allow_blank: true, 42 | allow_nil: true, 43 | message: 'some error message' } 44 | )} 45 | end 46 | 47 | end 48 | end -------------------------------------------------------------------------------- /spec/mv/core/validation/active_model_presenter/presence_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/active_model_presenter/presence' 4 | 5 | describe Mv::Core::Validation::ActiveModelPresenter::Presence do 6 | subject(:validation) { 7 | Mv::Core::Validation::Presence.new(:table_name, 8 | :column_name, 9 | opts) 10 | } 11 | 12 | subject(:acive_model_presenter) { described_class.new(validation) } 13 | 14 | describe "#initialize" do 15 | let(:opts) { true } 16 | 17 | its(:validation) { is_expected.to eq(validation) } 18 | its(:column_name) { is_expected.to eq(:column_name) } 19 | end 20 | 21 | describe "#options" do 22 | subject { acive_model_presenter.options } 23 | 24 | describe "when empty properties has specified" do 25 | let(:opts) { {} } 26 | 27 | it { is_expected.to eq(presence: true) } 28 | end 29 | 30 | describe "when properties has specified" do 31 | let(:opts) { { 32 | on: :create, 33 | as: :trigger, 34 | allow_blank: true, 35 | allow_nil: true, 36 | message: 'some error message' 37 | } } 38 | 39 | it { is_expected.to eq( 40 | presence: { on: :create, 41 | allow_blank: true, 42 | allow_nil: true, 43 | message: 'some error message' } 44 | )} 45 | end 46 | 47 | end 48 | end -------------------------------------------------------------------------------- /spec/mv/core/validation/active_model_presenter/uniqueness_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/active_model_presenter/uniqueness' 4 | 5 | describe Mv::Core::Validation::ActiveModelPresenter::Uniqueness do 6 | subject(:validation) { 7 | Mv::Core::Validation::Uniqueness.new(:table_name, 8 | :column_name, 9 | opts) 10 | } 11 | 12 | subject(:acive_model_presenter) { described_class.new(validation) } 13 | 14 | describe "#initialize" do 15 | let(:opts) { true } 16 | 17 | its(:validation) { is_expected.to eq(validation) } 18 | its(:column_name) { is_expected.to eq(:column_name) } 19 | end 20 | 21 | describe "#options" do 22 | subject { acive_model_presenter.options } 23 | 24 | describe "when empty properties has specified" do 25 | let(:opts) { {} } 26 | 27 | it { is_expected.to eq(uniqueness: true) } 28 | end 29 | 30 | describe "when properties has specified" do 31 | let(:opts) { { 32 | on: :create, 33 | as: :trigger, 34 | allow_blank: true, 35 | allow_nil: true, 36 | message: 'some error message' 37 | } } 38 | 39 | it { is_expected.to eq( 40 | uniqueness: { on: :create, 41 | allow_blank: true, 42 | allow_nil: true, 43 | message: 'some error message' } 44 | )} 45 | end 46 | 47 | end 48 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development, :test) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'rake' 11 | 12 | require 'jeweler' 13 | Jeweler::Tasks.new do |gem| 14 | gem.name = "mv-core" 15 | gem.homepage = "http://github.com/vprokopchuk256/mv-core" 16 | gem.license = "MIT" 17 | gem.summary = "DB constraints in migrations similiar to ActiveRecord validations. Core classes" 18 | gem.description = "DB constraints in migrations similiar to ActiveRecord validations. Core classes" 19 | gem.email = "vprokopchuk@gmail.com" 20 | gem.authors = ["Valeriy Prokopchuk"] 21 | gem.files = Dir.glob('lib/**/*.rb') 22 | gem.required_ruby_version = '>= 2.2.5' 23 | end 24 | Jeweler::RubygemsDotOrgTasks.new 25 | 26 | require 'rspec/core' 27 | require 'rspec/core/rake_task' 28 | RSpec::Core::RakeTask.new(:spec) do |spec| 29 | spec.pattern = FileList['spec/**/*_spec.rb'] 30 | end 31 | 32 | RSpec::Core::RakeTask.new(:rcov) do |spec| 33 | spec.pattern = 'spec/**/*_spec.rb' 34 | spec.rcov = true 35 | end 36 | 37 | task :default => :spec 38 | 39 | require 'rdoc/task' 40 | Rake::RDocTask.new do |rdoc| 41 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 42 | 43 | rdoc.rdoc_dir = 'rdoc' 44 | rdoc.title = "mv-core #{version}" 45 | rdoc.rdoc_files.include('README*') 46 | rdoc.rdoc_files.include('lib/**/*.rb') 47 | end 48 | -------------------------------------------------------------------------------- /spec/mv/core/migration/operations/add_column_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/migration/operations/add_column' 5 | 6 | describe Mv::Core::Migration::Operations::AddColumn do 7 | before do 8 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 9 | end 10 | 11 | describe '#initialize' do 12 | subject{ described_class.new(:table_name, :column_name, uniqueness: { as: :trigger }) } 13 | 14 | its(:table_name) { is_expected.to eq(:table_name) } 15 | its(:column_name) { is_expected.to eq(:column_name) } 16 | its(:opts) { is_expected.to eq(uniqueness: { as: :trigger }) } 17 | end 18 | 19 | describe '#execute' do 20 | describe "when opts are defined" do 21 | subject(:operation) do 22 | described_class.new(:table_name, :column_name, uniqueness: { as: :trigger}) 23 | end 24 | 25 | it "calls create_column_validator method" do 26 | expect(operation).to receive(:create_column_validator).with(:uniqueness, { as: :trigger }) 27 | .and_call_original 28 | operation.execute 29 | end 30 | end 31 | 32 | describe "when opts are not defined" do 33 | subject(:operation) do 34 | described_class.new(:table_name, :column_name) 35 | end 36 | 37 | it "does not raise an error" do 38 | expect{ operation.execute }.not_to raise_error 39 | end 40 | end 41 | end 42 | end -------------------------------------------------------------------------------- /spec/mv/core/validation/active_model_presenter/exclusion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/active_model_presenter/exclusion' 4 | 5 | describe Mv::Core::Validation::ActiveModelPresenter::Exclusion do 6 | subject(:validation) { 7 | Mv::Core::Validation::Exclusion.new(:table_name, 8 | :column_name, 9 | opts) 10 | } 11 | 12 | subject(:acive_model_presenter) { described_class.new(validation) } 13 | 14 | describe "#initialize" do 15 | let(:opts) { true } 16 | 17 | its(:validation) { is_expected.to eq(validation) } 18 | its(:column_name) { is_expected.to eq(:column_name) } 19 | end 20 | 21 | describe "#options" do 22 | subject { acive_model_presenter.options } 23 | 24 | describe "when empty properties has specified" do 25 | let(:opts) { { in: [1, 3] } } 26 | 27 | it { is_expected.to eq(exclusion: { in: [1, 3]}) } 28 | end 29 | 30 | describe "when properties has specified" do 31 | let(:opts) { { 32 | in: [1, 3], 33 | on: :create, 34 | as: :trigger, 35 | allow_blank: true, 36 | allow_nil: true, 37 | message: 'some error message' 38 | } } 39 | 40 | it { is_expected.to eq( 41 | exclusion: { in: [1, 3], 42 | on: :create, 43 | allow_blank: true, 44 | allow_nil: true, 45 | message: 'some error message' } 46 | )} 47 | end 48 | 49 | end 50 | end -------------------------------------------------------------------------------- /spec/mv/core/validation/active_model_presenter/inclusion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/active_model_presenter/inclusion' 4 | 5 | describe Mv::Core::Validation::ActiveModelPresenter::Inclusion do 6 | subject(:validation) { 7 | Mv::Core::Validation::Inclusion.new(:table_name, 8 | :column_name, 9 | opts) 10 | } 11 | 12 | subject(:acive_model_presenter) { described_class.new(validation) } 13 | 14 | describe "#initialize" do 15 | let(:opts) { true } 16 | 17 | its(:validation) { is_expected.to eq(validation) } 18 | its(:column_name) { is_expected.to eq(:column_name) } 19 | end 20 | 21 | describe "#options" do 22 | subject { acive_model_presenter.options } 23 | 24 | describe "when empty properties has specified" do 25 | let(:opts) { { in: [1, 3] } } 26 | 27 | it { is_expected.to eq(inclusion: { in: [1, 3]}) } 28 | end 29 | 30 | describe "when properties has specified" do 31 | let(:opts) { { 32 | in: [1, 3], 33 | on: :create, 34 | as: :trigger, 35 | allow_blank: true, 36 | allow_nil: true, 37 | message: 'some error message' 38 | } } 39 | 40 | it { is_expected.to eq( 41 | inclusion: { in: [1, 3], 42 | on: :create, 43 | allow_blank: true, 44 | allow_nil: true, 45 | message: 'some error message' } 46 | )} 47 | end 48 | 49 | end 50 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/builder/base.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Validation 4 | module Builder 5 | class Base 6 | attr_reader :validation 7 | 8 | delegate :table_name, 9 | :column_name, 10 | :allow_nil, 11 | :allow_blank, to: :validation 12 | 13 | def initialize(validation) 14 | @validation = validation 15 | end 16 | 17 | protected 18 | 19 | def message 20 | validation.full_message 21 | end 22 | 23 | def column_reference 24 | column_name 25 | end 26 | 27 | def apply_allow_nil_and_blank stmt 28 | res = stmt 29 | 30 | if allow_nil || allow_blank 31 | if allow_nil 32 | res = apply_allow_nil(res) 33 | end 34 | 35 | if allow_blank 36 | res = apply_allow_nil(res) unless allow_nil 37 | res = apply_allow_blank(res) 38 | end 39 | else 40 | res = apply_neither_nil_or_blank_allowed(stmt) 41 | end 42 | 43 | res 44 | end 45 | 46 | def apply_allow_nil stmt 47 | "#{stmt} OR #{column_reference} IS NULL" 48 | end 49 | 50 | def apply_allow_blank stmt 51 | "#{stmt} OR LENGTH(TRIM(#{column_reference})) = 0" 52 | end 53 | 54 | def apply_neither_nil_or_blank_allowed stmt 55 | "#{column_reference} IS NOT NULL AND #{stmt}" 56 | end 57 | end 58 | end 59 | end 60 | end 61 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/builder/factory.rb: -------------------------------------------------------------------------------- 1 | require_relative 'exclusion' 2 | require_relative 'inclusion' 3 | require_relative 'length' 4 | require_relative 'presence' 5 | require_relative 'absence' 6 | require_relative 'uniqueness' 7 | require_relative 'custom' 8 | 9 | module Mv 10 | module Core 11 | module Validation 12 | module Builder 13 | class Factory 14 | def create_builder validation 15 | factory_map[validation.class].new(validation) 16 | end 17 | 18 | def register_builder validation_class, builder_class 19 | factory_map[validation_class] = builder_class 20 | end 21 | 22 | def register_builders opts 23 | opts.each do |validation_class, builder_class| 24 | register_builder(validation_class, builder_class) 25 | end 26 | end 27 | 28 | private 29 | 30 | def factory_map 31 | @factory_map ||= { 32 | Mv::Core::Validation::Exclusion => Mv::Core::Validation::Builder::Exclusion, 33 | Mv::Core::Validation::Inclusion => Mv::Core::Validation::Builder::Inclusion, 34 | Mv::Core::Validation::Length => Mv::Core::Validation::Builder::Length, 35 | Mv::Core::Validation::Presence => Mv::Core::Validation::Builder::Presence, 36 | Mv::Core::Validation::Absence => Mv::Core::Validation::Builder::Absence, 37 | Mv::Core::Validation::Uniqueness => Mv::Core::Validation::Builder::Uniqueness, 38 | Mv::Core::Validation::Custom => Mv::Core::Validation::Builder::Custom 39 | } 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/mv/core/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/services/uninstall' 2 | require 'mv/core/services/delete_constraints' 3 | require 'mv/core/services/create_constraints' 4 | require 'mv/core/services/show_constraints' 5 | require 'mv/core/services/create_migration_validators_table' 6 | 7 | module Mv 8 | module Core 9 | class Railtie < ::Rails::Railtie 10 | rake_tasks do 11 | namespace :mv do 12 | task :uninstall => :environment do 13 | STDOUT.puts "Uninstall is irreversible operation. All constraints descriptions and declarations will be removed. 14 | You will have to either load schema or re - run all migrations to restore that info. Please enter YES if you want to continue anyway: ".squish 15 | 16 | if STDIN.gets.chomp == "YES" 17 | Mv::Core::Services::Uninstall.new.execute 18 | end 19 | end 20 | 21 | task :install => :environment do 22 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 23 | end 24 | 25 | task :show_constraints, [:tables] => :environment do |task, args| 26 | Mv::Core::Services::ShowConstraints.new((args[:tables] || '').split(/\s+/)).execute 27 | end 28 | 29 | task :delete_constraints, [:tables] => :environment do |task, args| 30 | Mv::Core::Services::DeleteConstraints.new((args[:tables] || '').split(/\s+/)).execute 31 | end 32 | 33 | task :create_constraints, [:tables] => :environment do |task, args| 34 | Mv::Core::Services::CreateConstraints.new((args[:tables] || '').split(/\s+/)).execute 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/mv/core/services/compare_constraint_arrays.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Services 4 | class CompareConstraintArrays 5 | attr_reader :old_constraints, :new_constraints 6 | 7 | def initialize(old_constraints, new_constraints) 8 | @old_constraints = old_constraints 9 | @new_constraints = new_constraints 10 | end 11 | 12 | def execute 13 | { 14 | deleted: deleted_constraints, 15 | updated: updated_constraints, 16 | added: added_constraints 17 | }.delete_if{ |key, value| value.blank? } 18 | end 19 | 20 | private 21 | 22 | def deleted_constraints 23 | old_constraints.select do |old_constraint| 24 | new_constraints.none? do |new_constraint| 25 | old_constraint.description == new_constraint.description 26 | end 27 | end 28 | end 29 | 30 | def updated_constraints 31 | old_constraints.inject([]) do |res, old_constraint| 32 | new_constraints.select do |new_constraint| 33 | old_constraint.description == new_constraint.description && 34 | old_constraint.validations.sort != new_constraint.validations.sort 35 | end.each{|new_constraint| res << [old_constraint, new_constraint]} 36 | res 37 | end 38 | end 39 | 40 | def added_constraints 41 | new_constraints.select do |new_constraint| 42 | old_constraints.none? do |old_constraint| 43 | old_constraint.description == new_constraint.description 44 | end 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /lib/mv/core/services/synchronize_constraints.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/constraint/builder/factory' 2 | require 'mv/core/services/say_constraints_diff' 3 | 4 | module Mv 5 | module Core 6 | module Services 7 | class SynchronizeConstraints 8 | attr_reader :additions, :updates, :deletions 9 | 10 | def initialize(additions, updates, deletions) 11 | @additions = additions 12 | @updates = updates 13 | @deletions = deletions 14 | end 15 | 16 | def execute 17 | delete 18 | update 19 | create 20 | end 21 | 22 | private 23 | 24 | def builder(constraint) 25 | Mv::Core::Constraint::Builder::Factory.create_builder(constraint) 26 | end 27 | 28 | def delete 29 | deletions.each do |constraint_to_delete| 30 | Mv::Core::Services::SayConstraintsDiff.new(constraint_to_delete, nil).execute do 31 | builder(constraint_to_delete).delete 32 | end 33 | end if deletions 34 | end 35 | 36 | def update 37 | updates.each do |old_constraint, new_constraint| 38 | Mv::Core::Services::SayConstraintsDiff.new(old_constraint, new_constraint).execute do 39 | builder(old_constraint).update(builder(new_constraint)) 40 | end 41 | end if updates 42 | end 43 | 44 | def create 45 | additions.each do |constraint_to_be_added| 46 | Mv::Core::Services::SayConstraintsDiff.new(nil, constraint_to_be_added).execute do 47 | builder(constraint_to_be_added).create 48 | end 49 | end if additions 50 | end 51 | end 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /spec/mv/core/router_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/router' 5 | 6 | Description = Mv::Core::Constraint::Description 7 | 8 | describe Mv::Core::Router do 9 | let(:migration_validator) { create(:migration_validator) } 10 | 11 | subject(:router) { described_class } 12 | 13 | before do 14 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 15 | end 16 | 17 | describe "#route" do 18 | let(:uniqueness) { 19 | Mv::Core::Validation::Uniqueness.new(:table_name, :column_name, options) 20 | } 21 | 22 | subject { described_class.route(uniqueness) } 23 | 24 | describe "when :as == :index" do 25 | let(:options) { { as: :index } } 26 | 27 | it { is_expected.to eq([Description.new(uniqueness.index_name, :index)]) } 28 | end 29 | 30 | describe "when :as == :trigger" do 31 | describe "when :on == :save" do 32 | let(:options) { { as: :trigger, on: :save } } 33 | 34 | it { is_expected.to match_array([ 35 | Description.new(uniqueness.update_trigger_name, :trigger, event: :update), 36 | Description.new(uniqueness.create_trigger_name, :trigger, event: :create) 37 | ]) } 38 | end 39 | 40 | describe "when :on == :create" do 41 | let(:options) { { as: :trigger, on: :create } } 42 | 43 | it { is_expected.to eq([Description.new(uniqueness.create_trigger_name, :trigger, event: :create)]) } 44 | end 45 | 46 | describe "when :on == :update" do 47 | let(:options) { { as: :trigger, on: :update } } 48 | 49 | it { is_expected.to eq([Description.new(uniqueness.update_trigger_name, :trigger, event: :update)]) } 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/mv/core/validation/builder/presence_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/presence' 4 | require 'mv/core/validation/builder/presence' 5 | 6 | describe Mv::Core::Validation::Builder::Presence do 7 | def presence(opts = {}) 8 | Mv::Core::Validation::Presence.new(:table_name, 9 | :column_name, 10 | { message: 'is absent' }.merge(opts)) 11 | end 12 | 13 | describe "#initialize" do 14 | subject { described_class.new(presence) } 15 | 16 | its(:validation) { is_expected.to eq(presence) } 17 | its(:allow_nil) { is_expected.to eq(presence.allow_nil) } 18 | its(:allow_blank) { is_expected.to eq(presence.allow_blank) } 19 | its(:column_name) { is_expected.to eq(presence.column_name) } 20 | end 21 | 22 | describe "#conditions" do 23 | subject { described_class.new(presence(opts)).conditions } 24 | 25 | describe "by default" do 26 | let(:opts) { {} } 27 | 28 | it { is_expected.to eq([{ 29 | statement: "column_name IS NOT NULL AND LENGTH(TRIM(column_name)) > 0", 30 | message: 'column_name is absent' 31 | }]) } 32 | end 33 | 34 | describe "when nil is allowed" do 35 | let(:opts) { { allow_nil: true } } 36 | 37 | it { is_expected.to eq([{ 38 | statement: "column_name IS NULL OR LENGTH(TRIM(column_name)) > 0", 39 | message: 'column_name is absent' 40 | }]) } 41 | end 42 | 43 | describe "when blank is allowed" do 44 | let(:opts) { { allow_blank: true } } 45 | 46 | it { is_expected.to eq([{ 47 | statement: "column_name IS NOT NULL OR LENGTH(TRIM(column_name)) = 0", 48 | message: 'column_name is absent' 49 | }]) } 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/mv/core/validation/builder/absence_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/absence' 4 | require 'mv/core/validation/builder/absence' 5 | 6 | describe Mv::Core::Validation::Builder::Absence do 7 | def absence(opts = {}) 8 | Mv::Core::Validation::Absence.new(:table_name, 9 | :column_name, 10 | { message: 'should be empty' }.merge(opts)) 11 | end 12 | 13 | describe "#initialize" do 14 | subject { described_class.new(absence) } 15 | 16 | its(:validation) { is_expected.to eq(absence) } 17 | its(:allow_nil) { is_expected.to eq(absence.allow_nil) } 18 | its(:allow_blank) { is_expected.to eq(absence.allow_blank) } 19 | its(:column_name) { is_expected.to eq(absence.column_name) } 20 | end 21 | 22 | describe "#conditions" do 23 | subject { described_class.new(absence(opts)).conditions } 24 | 25 | describe "by default" do 26 | let(:opts) { {} } 27 | 28 | it { is_expected.to eq([{ 29 | statement: "column_name IS NULL OR LENGTH(TRIM(column_name)) = 0", 30 | message: 'column_name should be empty' 31 | }]) } 32 | end 33 | 34 | describe "when nil is denied" do 35 | let(:opts) { { allow_nil: false } } 36 | 37 | it { is_expected.to eq([{ 38 | statement: "column_name IS NOT NULL AND LENGTH(TRIM(column_name)) = 0", 39 | message: 'column_name should be empty' 40 | }]) } 41 | end 42 | 43 | describe "when blank is denied" do 44 | let(:opts) { { allow_blank: false } } 45 | 46 | it { is_expected.to eq([{ 47 | statement: "column_name IS NULL AND LENGTH(TRIM(column_name)) > 0", 48 | message: 'column_name should be empty' 49 | }]) } 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/mv/core/services/create_constraints_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/services/create_constraints' 5 | 6 | describe Mv::Core::Services::CreateConstraints do 7 | let(:migration_validator) { 8 | create(:migration_validator, table_name: :table_name, 9 | validation_type: :uniqueness, 10 | options: { as: :index }) 11 | } 12 | 13 | before do 14 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 15 | migration_validator 16 | end 17 | 18 | subject(:service) { described_class.new(tables) } 19 | 20 | describe "#initialize" do 21 | describe "when tables list specified" do 22 | let(:tables) { [:table_name_1] } 23 | 24 | its(:tables) { is_expected.to eq([:table_name_1]) } 25 | end 26 | 27 | describe "when tables list not specified" do 28 | let(:tables) { [] } 29 | 30 | its(:tables) { is_expected.to eq(["table_name"]) } 31 | end 32 | end 33 | 34 | describe "#execute" do 35 | describe "when existing table name specified" do 36 | let(:tables) { [:table_name] } 37 | 38 | subject { service.execute } 39 | 40 | it "should remove constraints on the specified tables" do 41 | expect_any_instance_of(Mv::Core::Constraint::Builder::Index).to receive(:create) 42 | 43 | subject 44 | end 45 | end 46 | 47 | describe "when not existing table of table without constraints specified" do 48 | let(:tables) { [:table_name_1] } 49 | 50 | subject { service.execute } 51 | 52 | it "should remove constraints on the specified tables" do 53 | expect_any_instance_of(Mv::Core::Constraint::Builder::Index).not_to receive(:create) 54 | 55 | subject 56 | end 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /spec/mv/core/services/delete_constraints_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/services/delete_constraints' 5 | 6 | describe Mv::Core::Services::DeleteConstraints do 7 | let(:migration_validator) { 8 | create(:migration_validator, table_name: :table_name, 9 | validation_type: :uniqueness, 10 | options: { as: :index }) 11 | } 12 | 13 | before do 14 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 15 | migration_validator 16 | end 17 | 18 | subject(:service) { described_class.new(tables) } 19 | 20 | describe "#initialize" do 21 | describe "when tables list specified" do 22 | let(:tables) { [:table_name_1] } 23 | 24 | its(:tables) { is_expected.to eq([:table_name_1]) } 25 | end 26 | 27 | describe "when tables list not specified" do 28 | let(:tables) { [] } 29 | 30 | its(:tables) { is_expected.to eq(["table_name"]) } 31 | end 32 | end 33 | 34 | describe "#execute" do 35 | describe "when existing table name specified" do 36 | let(:tables) { [:table_name] } 37 | 38 | subject { service.execute } 39 | 40 | it "should remove constraints on the specified tables" do 41 | expect_any_instance_of(Mv::Core::Constraint::Builder::Index).to receive(:delete) 42 | 43 | subject 44 | end 45 | end 46 | 47 | describe "when not existing table of table without constraints specified" do 48 | let(:tables) { [:table_name_1] } 49 | 50 | subject { service.execute } 51 | 52 | it "should remove constraints on the specified tables" do 53 | expect_any_instance_of(Mv::Core::Constraint::Builder::Index).not_to receive(:delete) 54 | 55 | subject 56 | end 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /lib/mv/core/active_record/connection_adapters/abstract_adapter_decorator.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/migration/base' 2 | require 'mv/core/services/parse_validation_options' 3 | 4 | module Mv 5 | module Core 6 | module ActiveRecord 7 | module ConnectionAdapters 8 | module AbstractAdapterDecorator 9 | def add_column table_name, column_name, type, opts = {} 10 | Mv::Core::Migration::Base.add_column(table_name, column_name, params(opts)) 11 | 12 | super 13 | end 14 | 15 | def remove_column table_name, column_name, type = nil, options = {} 16 | Mv::Core::Migration::Base.remove_column table_name, column_name 17 | 18 | super 19 | end 20 | 21 | def rename_column table_name, old_column_name, new_column_name 22 | Mv::Core::Migration::Base.rename_column table_name, old_column_name, new_column_name 23 | 24 | super 25 | end 26 | 27 | def change_column table_name, column_name, type, opts = {} 28 | Mv::Core::Migration::Base.change_column(table_name, column_name, params(opts)) 29 | 30 | super 31 | end 32 | 33 | def validates table_name, column_name, opts 34 | Mv::Core::Migration::Base.change_column(table_name, column_name, opts) 35 | end 36 | 37 | def rename_table old_table_name, new_table_name 38 | Mv::Core::Migration::Base.rename_table(old_table_name, new_table_name) 39 | 40 | super 41 | end 42 | 43 | def drop_table table_name, opts = {} 44 | Mv::Core::Migration::Base.drop_table(table_name) 45 | 46 | super 47 | end 48 | 49 | private 50 | 51 | def params opts 52 | Mv::Core::Services::ParseValidationOptions.new(opts).execute 53 | end 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/mv/core/validation/active_model_presenter/factory.rb: -------------------------------------------------------------------------------- 1 | require_relative 'exclusion' 2 | require_relative 'inclusion' 3 | require_relative 'length' 4 | require_relative 'presence' 5 | require_relative 'absence' 6 | require_relative 'uniqueness' 7 | 8 | module Mv 9 | module Core 10 | module Validation 11 | module ActiveModelPresenter 12 | class Factory 13 | include Singleton 14 | 15 | def create_presenter validation 16 | klass = factory_map[validation.class] 17 | 18 | klass.new(validation) if klass 19 | end 20 | 21 | def register_presenter validation_class, presenter_class 22 | factory_map[validation_class] = presenter_class 23 | end 24 | 25 | def register_presenters opts 26 | opts.each do |validation_class, presenter_class| 27 | register_presenter(validation_class, presenter_class) 28 | end 29 | end 30 | 31 | class << self 32 | delegate :create_presenter, :register_presenter, :register_presenters, to: :instance 33 | end 34 | 35 | private 36 | 37 | def factory_map 38 | @factory_map ||= { 39 | Mv::Core::Validation::Exclusion => Mv::Core::Validation::ActiveModelPresenter::Exclusion, 40 | Mv::Core::Validation::Inclusion => Mv::Core::Validation::ActiveModelPresenter::Inclusion, 41 | Mv::Core::Validation::Length => Mv::Core::Validation::ActiveModelPresenter::Length, 42 | Mv::Core::Validation::Presence => Mv::Core::Validation::ActiveModelPresenter::Presence, 43 | Mv::Core::Validation::Absence => Mv::Core::Validation::ActiveModelPresenter::Absence, 44 | Mv::Core::Validation::Uniqueness => Mv::Core::Validation::ActiveModelPresenter::Uniqueness, 45 | } 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/mv/core/db/helpers/column_validators.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/validation/factory' 2 | require_relative 'table_validators' 3 | 4 | module Mv 5 | module Core 6 | module Db 7 | module Helpers 8 | module ColumnValidators 9 | include TableValidators 10 | 11 | attr_accessor :column_name 12 | 13 | def column_validators 14 | table_validators.where(column_name: column_name) 15 | end 16 | 17 | def create_column_validator validation_type, opts 18 | raise_column_validation_error(validation_type, opts) if opts == false 19 | 20 | update_column_validator(validation_type, opts) 21 | end 22 | 23 | def delete_column_validator 24 | delete_validators(column_validators) > 0 25 | end 26 | 27 | def update_column_validator validation_type, opts 28 | return delete_validators(column_validators.where(validation_type: validation_type)) if opts == false 29 | 30 | column_validators.where(validation_type: validation_type).first_or_initialize.tap do |validator| 31 | validator.options = normalize_opts(validation_type, opts) 32 | end.save! 33 | end 34 | 35 | def rename_column new_column_name 36 | column_validators.update_all(column_name: new_column_name) 37 | end 38 | 39 | private 40 | 41 | def normalize_opts validation_type, opts 42 | Mv::Core::Validation::Factory.create_validation(table_name, column_name, validation_type, opts).options 43 | end 44 | 45 | def raise_column_validation_error validation_type, opts 46 | raise Mv::Core::Error.new( 47 | table_name: table_name, 48 | column_name: column_name, 49 | validation_type: validation_type, 50 | options: opts, 51 | error: 'Validator can not be removed when new column is being added' 52 | ) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/mv/core/db/helpers/table_validators_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | 4 | require 'mv/core/services/create_migration_validators_table' 5 | require 'mv/core/db/migration_validator' 6 | require 'mv/core/db/helpers/column_validators' 7 | 8 | describe Mv::Core::Db::Helpers::TableValidators do 9 | let(:instance) do 10 | Class.new do 11 | include Mv::Core::Db::Helpers::TableValidators 12 | 13 | def initialize 14 | self.table_name = :table_name 15 | end 16 | end.new 17 | end 18 | 19 | before do 20 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 21 | end 22 | 23 | let!(:migration_validator) { create(:migration_validator) } 24 | 25 | describe "#delete_table_validators" do 26 | subject(:delete_table_validators) { instance.delete_table_validators } 27 | 28 | describe "when validator exists" do 29 | it "deletes validator" do 30 | expect{ subject }.to change(Mv::Core::Db::MigrationValidator, :count).by(-1) 31 | end 32 | end 33 | 34 | describe "when other table validator exist" do 35 | before do 36 | migration_validator.update_attribute(:table_name, :table_name_1) 37 | end 38 | 39 | it "does not delete validator" do 40 | expect{ subject }.not_to change(Mv::Core::Db::MigrationValidator, :count) 41 | end 42 | end 43 | end 44 | 45 | describe "update_table_validators" do 46 | subject(:update_table_validators) { instance.update_table_validators(:table_name_2) } 47 | 48 | describe "when validator exists" do 49 | it "updates validator" do 50 | expect{ subject }.to change{migration_validator.reload.table_name}.from("table_name").to("table_name_2") 51 | end 52 | end 53 | 54 | describe "when other table validator exist" do 55 | before do 56 | migration_validator.update_attribute(:table_name, :table_name_1) 57 | end 58 | 59 | it "does not update validator" do 60 | expect{ subject }.not_to change{migration_validator.reload.table_name} 61 | end 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /spec/mv/core/migration/operations/change_column_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/db/migration_validator' 5 | require 'mv/core/migration/operations/change_column' 6 | 7 | describe Mv::Core::Migration::Operations::ChangeColumn do 8 | before do 9 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 10 | end 11 | 12 | subject(:operation) { described_class.new(:table_name, :column_name, options ) } 13 | 14 | describe "#initialize" do 15 | let(:options) { { length: {is: 5} } } 16 | 17 | its(:table_name) { is_expected.to eq(:table_name) } 18 | its(:column_name) { is_expected.to eq(:column_name) } 19 | its(:opts) { is_expected.to eq(length: { is: 5 }) } 20 | end 21 | 22 | describe "#execute" do 23 | describe "when opts are defined" do 24 | let(:options) { { length: {is: 5} } } 25 | 26 | it "calls update_column_validator method with proper parameters" do 27 | expect(operation).to receive(:update_column_validator).with(:length, { is: 5 }) 28 | .and_call_original 29 | operation.execute 30 | end 31 | end 32 | 33 | describe "when opts are not defined" do 34 | let(:options) { {} } 35 | 36 | describe "when validations are defined for the column" do 37 | before { create(:migration_validator) } 38 | 39 | it "deletes validators" do 40 | expect(operation).to receive(:delete_column_validator).and_call_original 41 | operation.execute 42 | end 43 | end 44 | 45 | describe "when validations are NOT defined for the column" do 46 | it "does not update validators" do 47 | expect(operation).not_to receive(:update_column_validator) 48 | operation.execute 49 | end 50 | 51 | it "does not delete validators" do 52 | expect(operation).not_to receive(:delete_column_validator) 53 | operation.execute 54 | end 55 | end 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /spec/mv/core/db/migration_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/db/migration_validator' 5 | 6 | describe Mv::Core::Db::MigrationValidator do 7 | subject(:migration_validator) { 8 | described_class.create(table_name: :table_name, 9 | column_name: :column_name, 10 | validation_type: :uniqueness, 11 | options: { as: :trigger, update_trigger_name: :update_trigger_name}) 12 | } 13 | 14 | before do 15 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 16 | end 17 | 18 | describe "#initialize" do 19 | its(:table_name) { is_expected.to eq("table_name") } 20 | its(:column_name) { is_expected.to eq("column_name") } 21 | its(:validation_type) { is_expected.to eq("uniqueness") } 22 | its(:options) { is_expected.to eq({ as: :trigger, update_trigger_name: :update_trigger_name}) } 23 | end 24 | 25 | describe "db" do 26 | it { is_expected.to have_db_column(:table_name).with_options(null: false) } 27 | it { is_expected.to have_db_column(:column_name).with_options(null: false) } 28 | it { is_expected.to have_db_column(:validation_type).with_options(null: false) } 29 | it { is_expected.to have_db_column(:options) } 30 | end 31 | 32 | describe "options" do 33 | describe "when empty" do 34 | before do 35 | migration_validator.update_attributes(options: nil) 36 | end 37 | 38 | it "initialized by empty hash by default" do 39 | expect(migration_validator.options).to eq({}) 40 | end 41 | end 42 | end 43 | 44 | describe "#validation" do 45 | subject { migration_validator.validation } 46 | 47 | it { is_expected.not_to be_nil } 48 | 49 | describe "when not valid" do 50 | before do 51 | migration_validator.options[:as] = :check 52 | end 53 | 54 | it "invalidatates migration_validator" do 55 | migration_validator.valid? 56 | expect( migration_validator ).to be_invalid 57 | end 58 | end 59 | end 60 | end -------------------------------------------------------------------------------- /spec/mv/core/active_record/connection_adapters/table_definition_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mv::Core::ActiveRecord::ConnectionAdapters::TableDefinitionDecorator do 4 | let(:conn) { ::ActiveRecord::Base.connection } 5 | 6 | describe "#validates" do 7 | subject :create_table do 8 | conn.create_table :table_name, id: false do |t| 9 | t.string :column_name 10 | t.validates :column_name, length: { is: 5 } 11 | end 12 | end 13 | 14 | it "calls migration change_column method" do 15 | expect(Mv::Core::Migration::Base).to receive(:change_column).with( 16 | :table_name, :column_name, length: { is: 5 } 17 | ).at_least(:once) 18 | create_table 19 | end 20 | end 21 | 22 | describe "#column" do 23 | describe "by default" do 24 | subject :create_table do 25 | conn.create_table :table_name, id: false do |t| 26 | t.string :column_name, validates: { length: { is: 5 } } 27 | end 28 | end 29 | 30 | it "calls migration add_column method" do 31 | expect(Mv::Core::Migration::Base).to receive(:add_column).with( 32 | :table_name, :column_name, length: { is: 5 } 33 | ).at_least(:once) 34 | create_table 35 | end 36 | 37 | it "should call original methos" do 38 | create_table 39 | expect(conn.data_source_exists?(:table_name)).to be_truthy 40 | end 41 | end 42 | 43 | describe "when simplification provided" do 44 | subject :create_table do 45 | conn.create_table :table_name, id: false do |t| 46 | t.column :column_name, :string, length: { is: 5 } 47 | end 48 | end 49 | 50 | it "calls migration add_column method" do 51 | expect(Mv::Core::Migration::Base).to receive(:add_column).with( 52 | :table_name, :column_name, length: { is: 5 } 53 | ).at_least(:once) 54 | create_table 55 | end 56 | 57 | it "should call original method" do 58 | create_table 59 | expect(conn.data_source_exists?(:table_name)).to be_truthy 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/mv/core/validation/active_model_presenter/length_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/active_model_presenter/length' 4 | 5 | describe Mv::Core::Validation::ActiveModelPresenter::Length do 6 | subject(:validation) { 7 | Mv::Core::Validation::Length.new(:table_name, 8 | :column_name, 9 | opts) 10 | } 11 | 12 | subject(:acive_model_presenter) { described_class.new(validation) } 13 | 14 | describe "#initialize" do 15 | let(:opts) { { is: 1 } } 16 | 17 | its(:validation) { is_expected.to eq(validation) } 18 | its(:column_name) { is_expected.to eq(:column_name) } 19 | end 20 | 21 | describe "#options" do 22 | subject { acive_model_presenter.options } 23 | 24 | describe ":is" do 25 | let(:opts) { { is: 1 } } 26 | 27 | it { is_expected.to eq(length: { is: 1}) } 28 | end 29 | 30 | describe ":in" do 31 | let(:opts) { { in: [1, 3] } } 32 | 33 | it { is_expected.to eq(length: { in: [1, 3]}) } 34 | end 35 | 36 | describe ":within" do 37 | let(:opts) { { within: [1, 3] } } 38 | 39 | it { is_expected.to eq(length: { within: [1, 3]}) } 40 | end 41 | 42 | describe ":minimum & :maximum" do 43 | let(:opts) { { 44 | minimum: 1, 45 | maximum: 3, 46 | too_long: :too_long, 47 | too_short: :too_short 48 | } } 49 | 50 | it { is_expected.to eq( 51 | length: { minimum: 1, 52 | maximum: 3, 53 | too_long: :too_long, 54 | too_short: :too_short } 55 | ) } 56 | end 57 | 58 | describe "when properties has specified" do 59 | let(:opts) { { 60 | is: 1, 61 | on: :create, 62 | as: :trigger, 63 | allow_blank: true, 64 | allow_nil: true, 65 | message: 'some error message' 66 | } } 67 | 68 | it { is_expected.to eq( 69 | length: { is: 1, 70 | on: :create, 71 | allow_blank: true, 72 | allow_nil: true, 73 | message: 'some error message' } 74 | )} 75 | end 76 | 77 | end 78 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/factory.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/error' 2 | require_relative 'uniqueness' 3 | require_relative 'exclusion' 4 | require_relative 'inclusion' 5 | require_relative 'length' 6 | require_relative 'presence' 7 | require_relative 'absence' 8 | require_relative 'custom' 9 | 10 | module Mv 11 | module Core 12 | module Validation 13 | class Factory 14 | include Singleton 15 | 16 | def create_validation table_name, column_name, validation_type, opts 17 | validation_class = factory_map[validation_type.to_sym] 18 | 19 | raise Mv::Core::Error.new(table_name: table_name, 20 | column_name: column_name, 21 | validation_type: validation_type, 22 | opts: opts, 23 | error: "Validation '#{validation_type}' is not supported") unless validation_class 24 | 25 | validation_class.new(table_name, column_name, opts) 26 | end 27 | 28 | def register_validation validation_type, klass 29 | factory_map[validation_type.to_sym] = klass 30 | end 31 | 32 | def register_validations opts 33 | opts.each do |validation_type, klass| 34 | register_validation(validation_type, klass) 35 | end 36 | end 37 | 38 | def registered_validations 39 | factory_map.keys 40 | end 41 | 42 | class << self 43 | delegate :create_validation, 44 | :registered_validations, 45 | :register_validation, 46 | :register_validations, to: :instance 47 | end 48 | 49 | private 50 | 51 | def factory_map 52 | @factory_map ||= { 53 | uniqueness: Mv::Core::Validation::Uniqueness, 54 | exclusion: Mv::Core::Validation::Exclusion, 55 | inclusion: Mv::Core::Validation::Inclusion, 56 | length: Mv::Core::Validation::Length, 57 | presence: Mv::Core::Validation::Presence, 58 | absence: Mv::Core::Validation::Absence, 59 | custom: Mv::Core::Validation::Custom 60 | } 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/mv/core/constraint/builder/factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/constraint/builder/factory' 4 | 5 | describe Mv::Core::Constraint::Builder::Factory do 6 | describe "#create_builder" do 7 | subject { described_class.create_builder(constraint) } 8 | 9 | describe "for index constraint" do 10 | let(:index_description) { Mv::Core::Constraint::Description.new(:idx_table_name, :index) } 11 | let(:constraint) { Mv::Core::Constraint::Index.new(index_description)} 12 | 13 | it { is_expected.to be_a_kind_of(Mv::Core::Constraint::Builder::Index) } 14 | end 15 | 16 | describe "for trigger constraint" do 17 | let(:trigger_description) { Mv::Core::Constraint::Description.new(:trg_table_name_upd, :trigger, event: :update) } 18 | let(:constraint) { Mv::Core::Constraint::Trigger.new(trigger_description)} 19 | 20 | it { is_expected.to be_a_kind_of(Mv::Core::Constraint::Builder::Trigger) } 21 | end 22 | 23 | describe "when custom constraint builder defined" do 24 | let(:trigger_description) { Mv::Core::Constraint::Description.new(:trg_table_name_upd, :trigger, event: :update) } 25 | let(:klass) { Class.new(Mv::Core::Constraint::Builder::Trigger) } 26 | let(:constraint) { Mv::Core::Constraint::Trigger.new(trigger_description)} 27 | 28 | before { described_class.register_builder(Mv::Core::Constraint::Trigger, klass) } 29 | 30 | it { is_expected.to be_a_kind_of(klass) } 31 | 32 | after { described_class.register_builder(Mv::Core::Constraint::Trigger, Mv::Core::Constraint::Builder::Trigger) } 33 | end 34 | 35 | describe "when custom constraint builders defined as a hash" do 36 | let(:trigger_description) { Mv::Core::Constraint::Description.new(:trg_table_name_upd, :trigger, event: :update) } 37 | let(:klass) { Class.new(Mv::Core::Constraint::Builder::Trigger) } 38 | let(:constraint) { Mv::Core::Constraint::Trigger.new(trigger_description)} 39 | 40 | before { described_class.register_builders(Mv::Core::Constraint::Trigger => klass) } 41 | 42 | it { is_expected.to be_a_kind_of(klass) } 43 | 44 | after { described_class.register_builders(Mv::Core::Constraint::Trigger => Mv::Core::Constraint::Builder::Trigger) } 45 | end 46 | end 47 | end -------------------------------------------------------------------------------- /lib/mv/core/services/say_constraints_diff.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/services/compare_constraints' 2 | require 'mv/core/presenter/validation/base' 3 | require 'mv/core/presenter/constraint/description' 4 | 5 | module Mv 6 | module Core 7 | module Services 8 | class SayConstraintsDiff 9 | attr_reader :old_constraint, :new_constraint 10 | 11 | def initialize(old_constraint, new_constraint) 12 | @old_constraint = old_constraint 13 | @new_constraint = new_constraint 14 | end 15 | 16 | def execute 17 | ::ActiveRecord::Migration.say_with_time("#{constraint_operation} #{constraint_presenter} on table \"#{table_name}\"") do 18 | comparison[:deleted].each do |validation| 19 | say("delete #{validation_presenter(validation)}") 20 | end if comparison[:deleted] 21 | 22 | comparison[:added].each do |validation| 23 | say("create #{validation_presenter(validation)}") 24 | end if comparison[:added] 25 | 26 | yield 27 | end unless comparison.blank? 28 | end 29 | 30 | private 31 | 32 | def constraint_operation 33 | return 'create' unless old_constraint 34 | return 'delete' unless new_constraint 35 | 'update' 36 | end 37 | 38 | def table_name 39 | old_constraint.try(:validations).try(:first).try(:table_name) || 40 | new_constraint.try(:validations).try(:first).try(:table_name) 41 | end 42 | 43 | def constraint_presenter 44 | Mv::Core::Presenter::Constraint::Description.new((old_constraint || new_constraint).description) 45 | end 46 | 47 | def validation_presenter validation 48 | Mv::Core::Presenter::Validation::Base.new(validation) 49 | end 50 | 51 | def comparison 52 | @comparison ||= Mv::Core::Services::CompareConstraints.new(old_constraint, new_constraint) 53 | .execute 54 | end 55 | 56 | def say_with_time msg, &block 57 | ::ActiveRecord::Migration.say_with_time(msg, &block) 58 | end 59 | 60 | def say msg 61 | ::ActiveRecord::Migration.say(msg, true) 62 | end 63 | end 64 | end 65 | end 66 | end -------------------------------------------------------------------------------- /spec/mv/core/services/compare_constraint_arrays_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/compare_constraint_arrays' 4 | require 'mv/core/constraint/description' 5 | require 'mv/core/validation/uniqueness' 6 | require 'mv/core/validation/presence' 7 | 8 | describe Mv::Core::Services::CompareConstraintArrays do 9 | let(:description) { 10 | Mv::Core::Constraint::Description.new(:trg_mv_table_name_ins, :trigger, event: :create) 11 | } 12 | 13 | let(:uniqueness) { Mv::Core::Validation::Uniqueness.new(:table_name, :column_name, as: :trigger, on: :create)} 14 | let(:presence) { Mv::Core::Validation::Presence.new(:table_name, :column_name, as: :trigger, on: :create)} 15 | 16 | let(:old_constraint) { Mv::Core::Constraint::Trigger.new(description)} 17 | let(:new_constraint) { Mv::Core::Constraint::Trigger.new(description)} 18 | 19 | before do 20 | old_constraint.validations << uniqueness 21 | old_constraint.validations << presence 22 | 23 | new_constraint.validations << uniqueness 24 | end 25 | 26 | subject(:compare) { described_class.new(old_constraints, new_constraints) } 27 | 28 | describe "#initialize" do 29 | let(:old_constraints) { [old_constraint] } 30 | let(:new_constraints) { [new_constraint] } 31 | 32 | its(:old_constraints) { is_expected.to eq(old_constraints)} 33 | its(:new_constraints) { is_expected.to eq(new_constraints)} 34 | end 35 | 36 | describe "#execute" do 37 | subject { compare.execute } 38 | 39 | describe "when constraint was deleted" do 40 | let(:old_constraints) { [old_constraint] } 41 | let(:new_constraints) { [] } 42 | 43 | it { is_expected.to eq({ deleted: [old_constraint] }) } 44 | end 45 | 46 | describe "when constraint was added" do 47 | let(:old_constraints) { [] } 48 | let(:new_constraints) { [new_constraint] } 49 | 50 | it { is_expected.to eq({ added: [new_constraint] }) } 51 | end 52 | 53 | describe "when constraint was updated" do 54 | let(:old_constraints) { [old_constraint] } 55 | let(:new_constraints) { [new_constraint] } 56 | 57 | it { is_expected.to eq({ updated: [[old_constraint, new_constraint]] }) } 58 | end 59 | 60 | describe "when nothing was changed" do 61 | let(:old_constraints) { [old_constraint] } 62 | let(:new_constraints) { [old_constraint] } 63 | 64 | it { is_expected.to be_blank } 65 | end 66 | end 67 | end -------------------------------------------------------------------------------- /spec/mv/core/validation/builder/custom_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/custom' 4 | require 'mv/core/validation/builder/custom' 5 | 6 | describe Mv::Core::Validation::Builder::Custom do 7 | def custom(opts = {}) 8 | Mv::Core::Validation::Custom.new(:table_name, 9 | :column_name, 10 | { message: 'is not valid' }.merge(opts)) 11 | end 12 | 13 | let(:builder_class) { 14 | Class.new(described_class) do 15 | protected 16 | 17 | def column_reference 18 | "NEW.#{column_name}" 19 | end 20 | end 21 | } 22 | 23 | describe "#initialize" do 24 | subject { builder_class.new(custom) } 25 | 26 | its(:validation) { is_expected.to eq(custom) } 27 | its(:allow_nil) { is_expected.to eq(custom.allow_nil) } 28 | its(:allow_blank) { is_expected.to eq(custom.allow_blank) } 29 | its(:column_name) { is_expected.to eq(custom.column_name) } 30 | end 31 | 32 | describe "#conditions" do 33 | subject { builder_class.new(custom(opts)).conditions } 34 | 35 | describe "by default" do 36 | let(:opts) { { statement: "{column_name} > 0"} } 37 | 38 | it { is_expected.to eq([{ 39 | statement: "NEW.column_name IS NOT NULL AND (NEW.column_name > 0)", 40 | message: 'column_name is not valid' 41 | }]) } 42 | end 43 | 44 | describe "when nil is allowed" do 45 | let(:opts) { { statement: "{column_name} > 0", allow_nil: true } } 46 | 47 | it { is_expected.to eq([{ 48 | statement: "(NEW.column_name > 0) OR NEW.column_name IS NULL", 49 | message: 'column_name is not valid' 50 | }]) } 51 | end 52 | 53 | describe "when blank is allowed" do 54 | let(:opts) { { statement: "{column_name} > 0", allow_blank: true } } 55 | 56 | it { is_expected.to eq([{ 57 | statement: "(NEW.column_name > 0) OR NEW.column_name IS NULL OR LENGTH(TRIM(NEW.column_name)) = 0", 58 | message: 'column_name is not valid' 59 | }]) } 60 | end 61 | 62 | describe "when both blank and nil are allowed" do 63 | let(:opts) { { statement: "{column_name} > 0", allow_blank: true, allow_nil: true } } 64 | 65 | it { is_expected.to eq([{ 66 | statement: "(NEW.column_name > 0) OR NEW.column_name IS NULL OR LENGTH(TRIM(NEW.column_name)) = 0", 67 | message: 'column_name is not valid' 68 | }]) } 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/mv/core/presenter/validation/base.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Presenter 4 | module Validation 5 | class Base 6 | attr_reader :validation 7 | 8 | delegate :table_name, :column_name, :options, to: :validation 9 | 10 | def initialize(validation) 11 | @validation = validation 12 | end 13 | 14 | def to_s 15 | options_as_str = options.blank? ? 'true' : "{ #{options_str} }" 16 | "validates(\"#{table_name}\", \"#{column_name}\", #{validation_type}: #{options_as_str})".squish 17 | end 18 | 19 | private 20 | 21 | def validation_type 22 | validation.class.name.demodulize.underscore 23 | end 24 | 25 | def options_str 26 | options.collect do |key, value| 27 | "#{key}: #{value_to_str(value)}" 28 | end.join(', ') 29 | end 30 | 31 | def value_to_str value 32 | return ":#{value}" if value.is_a?(Symbol) 33 | return time_to_str(value) if value.is_a?(Time) 34 | return datetime_to_str(value) if value.is_a?(DateTime) 35 | return date_to_str(value) if value.is_a?(Date) 36 | return range_to_str(value) if value.is_a?(Range) 37 | return array_to_str(value) if value.is_a?(Array) 38 | return "'#{escaped_string(value)}'" if value.is_a?(String) 39 | return "/#{value.source}/" if value.is_a?(Regexp) 40 | value.to_s 41 | end 42 | 43 | def array_to_str value 44 | "[#{value.collect{ |value| value_to_str(value) }.join(', ')}]" 45 | end 46 | 47 | def range_to_str value 48 | [ 49 | value_to_str(value.first), 50 | value.exclude_end? ? '...' : '..', 51 | value_to_str(value.last) 52 | ].join 53 | end 54 | 55 | def escaped_string value 56 | value.gsub(/'/, %q(\\\')) 57 | end 58 | 59 | def time_to_str value 60 | "Time.new(#{value.strftime("%Y, %-m, %-d, %-H, %-M, %-S")})" 61 | end 62 | 63 | def datetime_to_str value 64 | "DateTime.new(#{value.utc.strftime("%Y, %-m, %-d, %-H, %-M, %-S")})" 65 | end 66 | 67 | def date_to_str value 68 | "Date.new(#{value.strftime("%Y, %-m, %-d")})" 69 | end 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/mv/core/services/show_constraints_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/services/show_constraints' 5 | 6 | describe Mv::Core::Services::ShowConstraints do 7 | let(:migration_validator) { 8 | create(:migration_validator, table_name: :table_name, 9 | validation_type: :uniqueness, 10 | options: { as: :trigger, 11 | update_trigger_name: :trg_table_name_upd, 12 | on: :update }) 13 | } 14 | 15 | let(:trigger_description) { Mv::Core::Constraint::Description.new(:trg_table_name_upd, 16 | :trigger, 17 | event: :update)} 18 | 19 | before do 20 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 21 | migration_validator 22 | end 23 | 24 | subject(:service) { described_class.new(tables) } 25 | 26 | describe "#initialize" do 27 | describe "when tables list specified" do 28 | let(:tables) { [:table_name_1] } 29 | 30 | its(:tables) { is_expected.to eq([:table_name_1]) } 31 | end 32 | 33 | describe "when tables list not specified" do 34 | let(:tables) { [] } 35 | 36 | its(:tables) { is_expected.to eq(["table_name"]) } 37 | end 38 | end 39 | 40 | describe "#execute" do 41 | describe "when existing table name specified" do 42 | let(:tables) { [:table_name] } 43 | 44 | subject { service.execute } 45 | 46 | it 'displays constraint' do 47 | expect(::ActiveRecord::Migration).to receive(:say_with_time).with("#{Mv::Core::Presenter::Constraint::Description.new(trigger_description)}") 48 | 49 | subject 50 | end 51 | 52 | it 'displays vlaidation' do 53 | expect(::ActiveRecord::Migration).to receive(:say).with("#{Mv::Core::Presenter::Validation::Base.new(migration_validator.validation)}", true) 54 | 55 | subject 56 | end 57 | end 58 | 59 | describe "when not existing table of table without constraints specified" do 60 | let(:tables) { [:table_name_1] } 61 | 62 | subject { service.execute } 63 | 64 | it "does not say anything" do 65 | expect(::ActiveRecord::Migration).not_to receive(:say) 66 | expect(::ActiveRecord::Migration).not_to receive(:say_with_time) 67 | 68 | subject 69 | end 70 | end 71 | end 72 | end -------------------------------------------------------------------------------- /spec/mv/core/services/compare_constraints_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/compare_constraints' 4 | 5 | describe Mv::Core::Services::CompareConstraints do 6 | let(:constraint_description) { 7 | Mv::Core::Constraint::Description.new(:trg_table_name_upd, :trigger, event: :update) 8 | } 9 | 10 | let(:old_constraint) { Mv::Core::Constraint::Trigger.new(constraint_description) } 11 | let(:new_constraint) { Mv::Core::Constraint::Trigger.new(constraint_description) } 12 | 13 | let(:uniqueness) { 14 | Mv::Core::Validation::Uniqueness.new(:table_name, 15 | :column_name, 16 | as: :trigger) 17 | } 18 | 19 | describe "#initialize" do 20 | subject { described_class.new(old_constraint, new_constraint) } 21 | 22 | its(:old_constraint) { is_expected.to eq(old_constraint) } 23 | its(:new_constraint) { is_expected.to eq(new_constraint) } 24 | end 25 | 26 | describe "#execute" do 27 | describe "when new constraint is not provided" do 28 | before { old_constraint.validations << uniqueness } 29 | 30 | subject { described_class.new(old_constraint, nil).execute } 31 | 32 | it { is_expected.to eq( 33 | deleted: [uniqueness] 34 | )} 35 | end 36 | 37 | describe "when old constraint is not provided" do 38 | before { new_constraint.validations << uniqueness } 39 | 40 | subject { described_class.new(nil, new_constraint).execute } 41 | 42 | it { is_expected.to eq( 43 | added: [uniqueness] 44 | )} 45 | end 46 | 47 | describe "when both new & old constraints are provided" do 48 | subject { described_class.new(old_constraint, new_constraint).execute } 49 | 50 | describe "when old constrains contains validation that is not included to the new one" do 51 | before { old_constraint.validations << uniqueness } 52 | 53 | it { is_expected.to eq( 54 | deleted: [uniqueness] 55 | )} 56 | end 57 | 58 | describe "when new constraint contains validation that is not included to the old one" do 59 | before { new_constraint.validations << uniqueness } 60 | 61 | it { is_expected.to eq( 62 | added: [uniqueness] 63 | )} 64 | end 65 | 66 | describe "when both new & old constraint has the same validations list" do 67 | before { 68 | new_constraint.validations << uniqueness 69 | old_constraint.validations << uniqueness 70 | } 71 | 72 | it { is_expected.to be_empty } 73 | end 74 | end 75 | end 76 | end -------------------------------------------------------------------------------- /spec/mv/core/constraint/index_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/constraint/index' 5 | require 'mv/core/error' 6 | 7 | describe Mv::Core::Constraint::Index do 8 | before do 9 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 10 | Mv::Core::Db::MigrationValidator.delete_all 11 | end 12 | 13 | let(:index_description) { Mv::Core::Constraint::Description.new(:idx_mv_table_name, :index) } 14 | 15 | subject(:index) { described_class.new(index_description) } 16 | 17 | describe "#initialize" do 18 | its(:options) { is_expected.to eq({}) } 19 | its(:name) { is_expected.to eq(:idx_mv_table_name) } 20 | its(:validations) { is_expected.to eq([]) } 21 | end 22 | 23 | describe "#<=>" do 24 | let(:inclusion) { Mv::Core::Validation::Inclusion.new(:table_name, :column_name, in: [1, 2], as: :index) } 25 | let(:exclusion) { Mv::Core::Validation::Exclusion.new(:table_name, :column_name, in: [0, 3], as: :index) } 26 | 27 | before do 28 | index.validations << inclusion 29 | index.validations << exclusion 30 | end 31 | 32 | it { is_expected.to eq(index) } 33 | 34 | describe "when description is different" do 35 | let(:other_index_description) { Mv::Core::Constraint::Description.new(:idx_mv_table_name_1, :index) } 36 | let(:other_index) { described_class.new(other_index_description) } 37 | 38 | before do 39 | other_index.validations << inclusion 40 | other_index.validations << exclusion 41 | end 42 | 43 | it { is_expected.not_to eq(other_index) } 44 | end 45 | 46 | describe "when validations list contains different elements" do 47 | let(:other_index) { described_class.new(index_description) } 48 | 49 | before do 50 | other_index.validations << inclusion 51 | end 52 | 53 | it { is_expected.not_to eq(other_index) } 54 | end 55 | 56 | describe "when validations list sorted differently" do 57 | let(:other_index) { described_class.new(index_description) } 58 | 59 | before do 60 | other_index.validations << exclusion 61 | other_index.validations << inclusion 62 | end 63 | 64 | it { is_expected.to eq(other_index) } 65 | end 66 | 67 | describe "when description and validations list are the same" do 68 | let(:other_index) { described_class.new(index_description) } 69 | 70 | before do 71 | other_index.validations << inclusion 72 | other_index.validations << exclusion 73 | end 74 | 75 | it { is_expected.to eq(other_index) } 76 | end 77 | end 78 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/builder/length.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Mv 4 | module Core 5 | module Validation 6 | module Builder 7 | class Length < Base 8 | delegate :in, 9 | :within, 10 | :is, 11 | :maximum, 12 | :minimum, 13 | to: :validation 14 | 15 | def conditions 16 | res = apply_in(column_reference) if self.in 17 | res = apply_within(column_reference) if within 18 | res = apply_is(column_reference) if self.is 19 | res = apply_maximum(column_reference) if maximum && !minimum 20 | res = apply_minimum(column_reference) if minimum && !maximum 21 | res = apply_minimum_and_maximum(column_reference) if minimum && maximum 22 | 23 | res.collect do |condition| 24 | { statement: apply_allow_nil_and_blank(condition[:statement]), message: condition[:message] } 25 | end 26 | end 27 | 28 | protected 29 | 30 | def too_short 31 | validation.full_too_short 32 | end 33 | 34 | def too_long 35 | validation.full_too_long 36 | end 37 | 38 | def apply_in stmt 39 | [{ statement: self.in.is_a?(Range) ? "LENGTH(#{stmt}) BETWEEN #{self.in.min} AND #{self.in.max}" : 40 | "LENGTH(#{stmt}) IN (#{self.in.join(', ')})", 41 | message: message }] 42 | end 43 | 44 | def apply_within stmt 45 | [{ statement: within.is_a?(Range) ? "LENGTH(#{stmt}) BETWEEN #{within.min} AND #{within.max}" : 46 | "LENGTH(#{stmt}) IN (#{within.join(', ')})", 47 | message: message }] 48 | end 49 | 50 | def apply_is stmt 51 | [{ statement: "LENGTH(#{stmt}) = #{self.is}", message: message }] 52 | end 53 | 54 | def apply_maximum stmt 55 | [{ statement: "LENGTH(#{stmt}) <= #{maximum}", message: too_long || message }] 56 | end 57 | 58 | def apply_minimum stmt 59 | [{ statement: "LENGTH(#{stmt}) >= #{minimum}", message: too_short || message }] 60 | end 61 | 62 | def apply_minimum_and_maximum stmt 63 | if too_long == too_short 64 | [{ statement: "LENGTH(#{stmt}) BETWEEN #{minimum} AND #{maximum}", 65 | message: message }] 66 | else 67 | [apply_minimum(stmt), apply_maximum(stmt)].flatten 68 | end 69 | end 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/mv/core/services/synchronize_constraints_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/synchronize_constraints' 4 | require 'mv/core/constraint/trigger' 5 | require 'mv/core/constraint/builder/trigger' 6 | 7 | describe Mv::Core::Services::SynchronizeConstraints do 8 | let(:description) { 9 | Mv::Core::Constraint::Description.new(:trg_mv_table_name_ins, 10 | :trigger, 11 | event: :create) 12 | } 13 | 14 | let(:uniqueness) { Mv::Core::Validation::Uniqueness.new(:table_name, :column_name, as: :trigger, on: :create)} 15 | let(:presence) { Mv::Core::Validation::Presence.new(:table_name, :column_name, as: :trigger, on: :create)} 16 | 17 | let(:old_constraint) { Mv::Core::Constraint::Trigger.new(description)} 18 | let(:new_constraint) { Mv::Core::Constraint::Trigger.new(description)} 19 | 20 | before do 21 | old_constraint.validations << uniqueness 22 | new_constraint.validations << presence 23 | end 24 | 25 | describe "#execute" do 26 | subject(:execute) { sync.execute } 27 | 28 | describe "deletions" do 29 | let(:sync) { described_class.new(nil, nil, [old_constraint])} 30 | 31 | it "calls #delete on old constraint" do 32 | builder = double 33 | 34 | expect(builder).to receive(:delete) 35 | expect(Mv::Core::Constraint::Builder::Trigger).to receive(:new).with(old_constraint).and_return(builder) 36 | expect(Mv::Core::Services::SayConstraintsDiff).to receive(:new).with(*[old_constraint, nil]).and_call_original 37 | 38 | execute 39 | end 40 | end 41 | 42 | describe "updates" do 43 | let(:sync) { described_class.new(nil, [[old_constraint, new_constraint]], nil)} 44 | 45 | it "calls #update on old_constraint" do 46 | builder = double 47 | 48 | expect(builder).to receive(:update) 49 | expect(Mv::Core::Constraint::Builder::Trigger).to receive(:new).twice.and_return(builder) 50 | expect(Mv::Core::Services::SayConstraintsDiff).to receive(:new).with(*[old_constraint, new_constraint]).and_call_original 51 | 52 | execute 53 | end 54 | end 55 | 56 | describe "additions" do 57 | let(:sync) { described_class.new([new_constraint], nil, nil)} 58 | 59 | it "calls #create on old constraint" do 60 | builder = double 61 | 62 | expect(builder).to receive(:create) 63 | expect(Mv::Core::Constraint::Builder::Trigger).to receive(:new).with(new_constraint).and_return(builder) 64 | expect(Mv::Core::Services::SayConstraintsDiff).to receive(:new).with(*[nil, new_constraint]).and_call_original 65 | 66 | execute 67 | end 68 | end 69 | end 70 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/length.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/validators/integers_array_validator' 2 | require_relative 'base' 3 | 4 | module Mv 5 | module Core 6 | module Validation 7 | class Length < Base 8 | include ActiveModel::Validations 9 | 10 | attr_reader :in, :within, :is, :maximum, :minimum, 11 | :too_long, :too_short 12 | 13 | validates :in, :within, presence: true, allow_nil: true, integers_array: true 14 | validates :is, :minimum, :maximum, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true 15 | 16 | validate :in_within_is_maximum_minimum_allowance 17 | 18 | def initialize(table_name, column_name, opts) 19 | unless opts.is_a?(Hash) 20 | opts = { in: opts } if opts.respond_to?(:to_a) 21 | opts = { is: opts } if opts.is_a?(Integer) 22 | end 23 | 24 | super(table_name, column_name, opts) 25 | 26 | opts.with_indifferent_access.tap do |opts| 27 | @in = opts[:in] 28 | @within = opts[:within] 29 | @is = opts[:is] 30 | @maximum = opts[:maximum] 31 | @minimum = opts[:minimum] 32 | @too_long = opts[:too_long] || default_too_long_message 33 | @too_short = opts[:too_short] || default_too_short_message 34 | end 35 | end 36 | 37 | def full_too_short 38 | too_short ? compose_full_message(too_short) : nil 39 | end 40 | 41 | def full_too_long 42 | too_long ? compose_full_message(too_long) : nil 43 | end 44 | 45 | protected 46 | 47 | def to_a 48 | super + [self.in.try(:sort), within.try(:sort), is.to_s, maximum.to_s, minimum.to_s, too_short.to_s, too_long.to_s] 49 | end 50 | 51 | def default_message 52 | 'is the wrong length' 53 | end 54 | 55 | def default_too_short_message 56 | minimum ? 'is too short' : nil 57 | end 58 | 59 | def default_too_long_message 60 | maximum ? 'is too long' : nil 61 | end 62 | 63 | private 64 | 65 | def in_within_is_maximum_minimum_allowance 66 | not_null_attrs = [[is, :is], 67 | [within, :within], 68 | [self.in, :in], 69 | [maximum || minimum, :minimum_or_maximum]] 70 | .select(&:first).collect(&:second) 71 | 72 | if not_null_attrs.length != 1 73 | not_null_attrs << :is if not_null_attrs.blank? 74 | 75 | not_null_attrs.each do |attr| 76 | errors.add( 77 | attr, 78 | 'One and only one attribute from the list [:is, :within, :in, [:minimum, :maximum] can be defined' 79 | ) 80 | end 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/mv/core/services/parse_validation_options_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/parse_validation_options' 4 | 5 | describe Mv::Core::Services::ParseValidationOptions do 6 | subject(:service) { described_class.new(opts) } 7 | 8 | describe "#initialize" do 9 | let(:opts) { { validates: { presence: true } } } 10 | 11 | its(:opts) { is_expected.to eq(opts) } 12 | end 13 | 14 | describe "#execute" do 15 | subject { service.execute } 16 | 17 | describe "validates" do 18 | let(:opts) { { validates: { presence: true } } } 19 | 20 | it { is_expected.to eq(presence: true) } 21 | 22 | it "deletes handled parameters" do 23 | subject 24 | expect(opts).to be_empty 25 | end 26 | end 27 | 28 | describe "validates as string" do 29 | let(:opts) { { "validates" => { presence: true } } } 30 | 31 | it { is_expected.to eq(presence: true) } 32 | 33 | it "deletes handled parameters" do 34 | subject 35 | expect(opts).to be_empty 36 | end 37 | end 38 | 39 | describe "validates with simplification" do 40 | let(:opts) { { validates: '{column_name} > 1' } } 41 | 42 | it { is_expected.to eq(custom: '{column_name} > 1') } 43 | end 44 | 45 | describe "simplifications are defined" do 46 | let(:opts) do 47 | { absence: true, 48 | custom: '{column_name} > 1', 49 | exclusion: 1..2, 50 | inclusion: 1..2, 51 | length: 1..3, 52 | presence: true, 53 | uniqueness: true, 54 | not_known_validation: true } 55 | end 56 | 57 | it { is_expected.to eq({uniqueness: true, 58 | exclusion: 1..2, 59 | inclusion: 1..2, 60 | length: 1..3, 61 | presence: true, 62 | absence: true, 63 | custom: "{column_name} > 1"}) } 64 | 65 | it "deletes handled parameters" do 66 | subject 67 | expect(opts).to eq(not_known_validation: true) 68 | end 69 | end 70 | 71 | describe "string simplifications are defined" do 72 | let(:opts) do 73 | { 'absence' => true, 74 | 'custom' => '{column_name} > 1', 75 | 'exclusion' => 1..2, 76 | 'inclusion' => 1..2, 77 | 'length' => 1..3, 78 | 'presence' => true, 79 | 'uniqueness' => true, 80 | 'not_known_validation' => true } 81 | end 82 | 83 | it { is_expected.to eq({uniqueness: true, 84 | exclusion: 1..2, 85 | inclusion: 1..2, 86 | length: 1..3, 87 | presence: true, 88 | absence: true, 89 | custom: "{column_name} > 1"}) } 90 | 91 | it "deletes handled parameters" do 92 | subject 93 | expect(opts).to eq('not_known_validation' => true) 94 | end 95 | end 96 | end 97 | end -------------------------------------------------------------------------------- /spec/mv/core/validation/builder/uniqueness_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/uniqueness' 4 | require 'mv/core/validation/builder/uniqueness' 5 | 6 | describe Mv::Core::Validation::Builder::Uniqueness do 7 | def uniqueness(opts = {}) 8 | Mv::Core::Validation::Uniqueness.new(:table_name, 9 | :column_name, 10 | { message: 'is not unique as expected' }.merge(opts)) 11 | end 12 | 13 | describe "#initialize" do 14 | subject { described_class.new(uniqueness) } 15 | 16 | its(:validation) { is_expected.to eq(uniqueness) } 17 | its(:allow_nil) { is_expected.to eq(uniqueness.allow_nil) } 18 | its(:allow_blank) { is_expected.to eq(uniqueness.allow_blank) } 19 | its(:column_name) { is_expected.to eq(uniqueness.column_name) } 20 | its(:table_name) { is_expected.to eq(uniqueness.table_name) } 21 | its(:message) { is_expected.to eq(uniqueness.full_message) } 22 | end 23 | 24 | describe "#conditions" do 25 | let(:uniqueness_builder) do 26 | Class.new(described_class) do 27 | def column_reference 28 | :db_name 29 | end 30 | end 31 | end 32 | subject { uniqueness_builder.new(uniqueness(opts)).conditions } 33 | 34 | describe "by default" do 35 | let(:opts) { {} } 36 | 37 | it { is_expected.to eq([{ 38 | statement: "db_name IS NOT NULL AND NOT EXISTS(SELECT column_name 39 | FROM table_name 40 | WHERE db_name = column_name)".squish, 41 | message: 'column_name is not unique as expected' 42 | }])} 43 | end 44 | 45 | describe "when nil is allowed" do 46 | let(:opts) { {allow_nil: true} } 47 | 48 | it { is_expected.to eq([{ 49 | statement: "NOT EXISTS(SELECT column_name 50 | FROM table_name 51 | WHERE db_name = column_name) 52 | OR db_name IS NULL".squish, 53 | message: 'column_name is not unique as expected' 54 | }])} 55 | end 56 | 57 | describe "when blank is allowed" do 58 | let(:opts) { {allow_blank: true} } 59 | 60 | it { is_expected.to eq([{ 61 | statement: "NOT EXISTS(SELECT column_name 62 | FROM table_name 63 | WHERE db_name = column_name) 64 | OR db_name IS NULL 65 | OR LENGTH(TRIM(db_name)) = 0".squish, 66 | message: 'column_name is not unique as expected' 67 | }])} 68 | 69 | end 70 | 71 | describe "when both blank & nil are allowed" do 72 | let(:opts) { {allow_blank: true, allow_nil: true} } 73 | 74 | it { is_expected.to eq([{ 75 | statement: "NOT EXISTS(SELECT column_name 76 | FROM table_name 77 | WHERE db_name = column_name) 78 | OR db_name IS NULL 79 | OR LENGTH(TRIM(db_name)) = 0".squish, 80 | message: 'column_name is not unique as expected' 81 | }])} 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/mv/core/validation/builder/factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/builder/factory' 4 | 5 | describe Mv::Core::Validation::Builder::Factory do 6 | subject(:factory) { described_class.new } 7 | 8 | describe "#create_builder" do 9 | subject { factory.create_builder(validation) } 10 | 11 | describe "exclusion" do 12 | let(:validation) { Mv::Core::Validation::Exclusion.new(:table_name, :column_name, in: [1, 2]) } 13 | 14 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::Builder::Exclusion) } 15 | end 16 | 17 | describe "inclusion" do 18 | let(:validation) { Mv::Core::Validation::Inclusion.new(:table_name, :column_name, in: [1, 2]) } 19 | 20 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::Builder::Inclusion) } 21 | end 22 | 23 | describe "length" do 24 | let(:validation) { Mv::Core::Validation::Length.new(:table_name, :column_name, in: [1, 2]) } 25 | 26 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::Builder::Length) } 27 | end 28 | 29 | describe "presence" do 30 | let(:validation) { Mv::Core::Validation::Presence.new(:table_name, :column_name, {}) } 31 | 32 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::Builder::Presence) } 33 | end 34 | 35 | describe "absence" do 36 | let(:validation) { Mv::Core::Validation::Absence.new(:table_name, :column_name, {}) } 37 | 38 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::Builder::Absence) } 39 | end 40 | 41 | describe "uniqueness" do 42 | let(:validation) { Mv::Core::Validation::Uniqueness.new(:table_name, :column_name, {}) } 43 | 44 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::Builder::Uniqueness) } 45 | end 46 | 47 | describe "custom" do 48 | let(:validation) { Mv::Core::Validation::Custom.new(:table_name, :column_name, { statement: 'column_name > 0'}) } 49 | 50 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::Builder::Custom) } 51 | end 52 | end 53 | 54 | describe "#register_builder" do 55 | let(:klass) { CustomValidationBuilderClass = Class.new(Mv::Core::Validation::Builder::Exclusion) } 56 | let(:validation) { Mv::Core::Validation::Exclusion.new(:table_name, :column_name, in: [1, 2]) } 57 | 58 | before { factory.register_builder(Mv::Core::Validation::Exclusion, klass) } 59 | 60 | subject { factory.create_builder(validation) } 61 | 62 | it { is_expected.to be_an_instance_of(klass) } 63 | 64 | after { factory.register_builder(Mv::Core::Validation::Exclusion, Mv::Core::Validation::Builder::Exclusion) } 65 | end 66 | 67 | describe "#register_builders" do 68 | let(:klass) { CustomValidationBuilderClass1 = Class.new(Mv::Core::Validation::Builder::Exclusion) } 69 | let(:validation) { Mv::Core::Validation::Exclusion.new(:table_name, :column_name, in: [1, 2]) } 70 | 71 | before { factory.register_builders(Mv::Core::Validation::Exclusion => klass) } 72 | 73 | subject { factory.create_builder(validation) } 74 | 75 | it { is_expected.to be_an_instance_of(klass) } 76 | 77 | after { factory.register_builders(Mv::Core::Validation::Exclusion => Mv::Core::Validation::Builder::Exclusion) } 78 | end 79 | end -------------------------------------------------------------------------------- /spec/mv/core/validation/active_model_presenter/factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/active_model_presenter/factory' 4 | 5 | describe Mv::Core::Validation::ActiveModelPresenter::Factory do 6 | subject(:factory) { described_class } 7 | 8 | describe "#create_presenter" do 9 | subject { factory.create_presenter(validation) } 10 | 11 | describe "exclusion" do 12 | let(:validation) { Mv::Core::Validation::Exclusion.new(:table_name, :column_name, in: [1, 2]) } 13 | 14 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::ActiveModelPresenter::Exclusion) } 15 | end 16 | 17 | describe "inclusion" do 18 | let(:validation) { Mv::Core::Validation::Inclusion.new(:table_name, :column_name, in: [1, 2]) } 19 | 20 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::ActiveModelPresenter::Inclusion) } 21 | end 22 | 23 | describe "length" do 24 | let(:validation) { Mv::Core::Validation::Length.new(:table_name, :column_name, in: [1, 2]) } 25 | 26 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::ActiveModelPresenter::Length) } 27 | end 28 | 29 | describe "presence" do 30 | let(:validation) { Mv::Core::Validation::Presence.new(:table_name, :column_name, {}) } 31 | 32 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::ActiveModelPresenter::Presence) } 33 | end 34 | 35 | describe "absence" do 36 | let(:validation) { Mv::Core::Validation::Absence.new(:table_name, :column_name, {}) } 37 | 38 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::ActiveModelPresenter::Absence) } 39 | end 40 | 41 | describe "uniqueness" do 42 | let(:validation) { Mv::Core::Validation::Uniqueness.new(:table_name, :column_name, {}) } 43 | 44 | it { is_expected.to be_an_instance_of(Mv::Core::Validation::ActiveModelPresenter::Uniqueness) } 45 | end 46 | 47 | describe "custom" do 48 | let(:validation) { Mv::Core::Validation::Custom.new(:table_name, :column_name, { statement: 'column_name > 0'}) } 49 | 50 | it { is_expected.to be_nil } 51 | end 52 | end 53 | 54 | describe "#register_presenter" do 55 | let(:klass) { CustomValidationPresenterClass = Class.new(Mv::Core::Validation::ActiveModelPresenter::Exclusion) } 56 | let(:validation) { Mv::Core::Validation::Exclusion.new(:table_name, :column_name, in: [1, 2]) } 57 | 58 | before { factory.register_presenter(Mv::Core::Validation::Exclusion, klass) } 59 | 60 | subject { factory.create_presenter(validation) } 61 | 62 | it { is_expected.to be_an_instance_of(klass) } 63 | 64 | after { factory.register_presenter(Mv::Core::Validation::Exclusion, Mv::Core::Validation::ActiveModelPresenter::Exclusion) } 65 | end 66 | 67 | describe "#register_presenters" do 68 | let(:klass) { CustomValidationPresenterClass1 = Class.new(Mv::Core::Validation::ActiveModelPresenter::Exclusion) } 69 | let(:validation) { Mv::Core::Validation::Exclusion.new(:table_name, :column_name, in: [1, 2]) } 70 | 71 | before { factory.register_presenters(Mv::Core::Validation::Exclusion => klass) } 72 | 73 | subject { factory.create_presenter(validation) } 74 | 75 | it { is_expected.to be_an_instance_of(klass) } 76 | 77 | after { factory.register_presenters(Mv::Core::Validation::Exclusion => Mv::Core::Validation::ActiveModelPresenter::Exclusion) } 78 | end 79 | end -------------------------------------------------------------------------------- /lib/mv/core/migration/base.rb: -------------------------------------------------------------------------------- 1 | require 'mv/core/db/migration_validator' 2 | require 'mv/core/migration/operations/list' 3 | require 'mv/core/migration/operations/factory' 4 | require 'mv/core/services/load_constraints' 5 | require 'mv/core/services/compare_constraint_arrays' 6 | require 'mv/core/services/synchronize_constraints' 7 | require 'mv/core/services/create_migration_validators_table.rb' 8 | 9 | module Mv 10 | module Core 11 | module Migration 12 | class Base 13 | include Singleton 14 | 15 | SUPPORTED_METHODS = %i{ add_column remove_column rename_column 16 | change_column rename_table drop_table} 17 | 18 | attr_reader :operations_list, :operations_factory 19 | 20 | def initialize() 21 | @operations_list = Mv::Core::Migration::Operations::List.new 22 | @operations_factory = Mv::Core::Migration::Operations::Factory.new() 23 | end 24 | 25 | SUPPORTED_METHODS.each do |operation_name| 26 | define_method operation_name do |*args| 27 | 28 | unless disabled_validations? 29 | operation = operations_factory.create_operation(operation_name, *args) 30 | operations_list.add_operation(operation) 31 | end 32 | end 33 | end 34 | 35 | def execute 36 | if operations_list.present? 37 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 38 | 39 | constraints_loader = Mv::Core::Services::LoadConstraints.new(operations_list.tables) 40 | 41 | old_constraints = constraints_loader.execute 42 | 43 | operations_list.execute() 44 | 45 | new_constraints = constraints_loader.execute 46 | 47 | constraints_comparizon = Mv::Core::Services::CompareConstraintArrays.new(old_constraints, new_constraints) 48 | .execute 49 | 50 | Mv::Core::Services::SynchronizeConstraints.new(constraints_comparizon[:added], 51 | constraints_comparizon[:updated], 52 | constraints_comparizon[:deleted]) 53 | .execute 54 | end 55 | end 56 | 57 | def add_column table_name, column_name, opts 58 | return unless opts.present? 59 | 60 | unless disabled_validations? 61 | operation = operations_factory.create_operation(:add_column, table_name, column_name, opts) 62 | operations_list.add_operation(operation) 63 | end 64 | end 65 | 66 | def with_suppressed_validations 67 | disable_validations! 68 | yield 69 | enable_validations! 70 | end 71 | 72 | class << self 73 | alias_method :current, :instance 74 | 75 | delegate *[SUPPORTED_METHODS, :execute, :with_suppressed_validations].flatten, to: :current, allow_nil: true 76 | end 77 | 78 | private 79 | 80 | def disabled_validations? 81 | !!@disabled_validations 82 | end 83 | 84 | def disable_validations! 85 | @disabled_validations = true 86 | end 87 | 88 | def enable_validations! 89 | @disabled_validations = false 90 | end 91 | end 92 | end 93 | end 94 | end -------------------------------------------------------------------------------- /spec/mv/core/constraint/trigger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/constraint/trigger' 4 | require 'mv/core/error' 5 | 6 | describe Mv::Core::Constraint::Trigger do 7 | let(:trigger_description) { Mv::Core::Constraint::Description.new(:update_trigger_name, 8 | :trigger, 9 | { event: :update })} 10 | 11 | subject(:trigger) { described_class.new(trigger_description) } 12 | 13 | describe "#initialize" do 14 | its(:description) { is_expected.to eq(trigger_description) } 15 | its(:validations) { is_expected.to eq([]) } 16 | end 17 | 18 | describe "#constraint interface" do 19 | it { is_expected.to respond_to(:create) } 20 | it { is_expected.to respond_to(:delete) } 21 | end 22 | 23 | describe "#update?" do 24 | let(:trigger_description) { Mv::Core::Constraint::Description.new(:update_trigger_name, 25 | :trigger, 26 | opts)} 27 | subject { trigger.update? } 28 | 29 | describe "when :event == :create" do 30 | let(:opts) { { event: :create } } 31 | 32 | it { is_expected.to be_falsey } 33 | end 34 | 35 | describe "when :event == :update" do 36 | let(:opts) { { event: :update } } 37 | 38 | it { is_expected.to be_truthy } 39 | end 40 | 41 | describe "when :event is not defined" do 42 | let(:opts) { { } } 43 | 44 | it { is_expected.to be_falsey } 45 | end 46 | end 47 | 48 | describe "#<=>" do 49 | let(:inclusion) { Mv::Core::Validation::Inclusion.new(:table_name, :column_name, in: [1, 2], as: :trigger) } 50 | let(:exclusion) { Mv::Core::Validation::Exclusion.new(:table_name, :column_name, in: [0, 3], as: :trigger) } 51 | 52 | before do 53 | trigger.validations << inclusion 54 | trigger.validations << exclusion 55 | end 56 | 57 | it { is_expected.to eq(trigger) } 58 | 59 | describe "when description is different" do 60 | let(:other_trigger_description) { Mv::Core::Constraint::Description.new(:trg_mv_table_name_1, :trigger) } 61 | let(:other_trigger) { described_class.new(other_trigger_description) } 62 | 63 | before do 64 | other_trigger.validations << inclusion 65 | other_trigger.validations << exclusion 66 | end 67 | 68 | it { is_expected.not_to eq(other_trigger) } 69 | end 70 | 71 | describe "when validations list contains different elements" do 72 | let(:other_trigger) { described_class.new(trigger_description) } 73 | 74 | before do 75 | other_trigger.validations << inclusion 76 | end 77 | 78 | it { is_expected.not_to eq(other_trigger) } 79 | end 80 | 81 | describe "when validations list sorted differently" do 82 | let(:other_trigger) { described_class.new(trigger_description) } 83 | 84 | before do 85 | other_trigger.validations << exclusion 86 | other_trigger.validations << inclusion 87 | end 88 | 89 | it { is_expected.to eq(other_trigger) } 90 | end 91 | 92 | describe "when description and validations list are the same" do 93 | let(:other_trigger) { described_class.new(trigger_description) } 94 | 95 | before do 96 | other_trigger.validations << inclusion 97 | other_trigger.validations << exclusion 98 | end 99 | 100 | it { is_expected.to eq(other_trigger) } 101 | end 102 | end 103 | end -------------------------------------------------------------------------------- /lib/mv/core/validation/base.rb: -------------------------------------------------------------------------------- 1 | module Mv 2 | module Core 3 | module Validation 4 | class Base 5 | include Comparable 6 | include ActiveModel::Validations 7 | 8 | attr_reader :table_name, :column_name, :message, :on, :create_trigger_name, 9 | :update_trigger_name, :allow_nil, :allow_blank, :as, :options 10 | 11 | validates :on, inclusion: { in: :available_on }, allow_nil: true 12 | validates :allow_nil, :allow_blank, inclusion: { in: [true, false] } 13 | validates :as, inclusion: { in: :available_as } 14 | 15 | validates :on, absence: { message: 'allowed when :as == :trigger' }, unless: :trigger? 16 | validates :create_trigger_name, absence: { message: 'allowed when :on in [:save, :create] and :as == :trigger'}, unless: "create? && trigger?" 17 | validates :update_trigger_name, absence: { message: 'allowed when :on in [:save, :update] and :as == :trigger'}, unless: "update? && trigger?" 18 | 19 | def initialize table_name, column_name, options 20 | @table_name = table_name 21 | @column_name = column_name 22 | @options = options 23 | 24 | options.with_indifferent_access.tap do |options| 25 | @message = options[:message] || default_message 26 | @as = options[:as] || default_as 27 | @on = options[:on] || default_on 28 | @create_trigger_name = options[:create_trigger_name] || default_create_trigger_name 29 | @update_trigger_name = options[:update_trigger_name] || default_update_trigger_name 30 | @allow_nil = options[:allow_nil].nil? ? default_allow_nil : options[:allow_nil] 31 | @allow_blank = options[:allow_blank].nil? ? default_allow_blank : options[:allow_blank] 32 | end 33 | end 34 | 35 | def to_a 36 | [table_name.to_s, column_name.to_s, message.to_s, on.to_s, create_trigger_name.to_s, 37 | update_trigger_name.to_s, allow_nil, allow_blank, as.to_s] 38 | end 39 | 40 | def <=> other_validation 41 | [self.class.name, to_a] <=> [other_validation.class.name, other_validation.to_a] 42 | end 43 | 44 | def update? 45 | [:save, :update].include?(on.try(:to_sym)) 46 | end 47 | 48 | def create? 49 | [:save, :create].include?(on.try(:to_sym)) 50 | end 51 | 52 | def full_message 53 | compose_full_message(message) 54 | end 55 | 56 | protected 57 | 58 | def available_as 59 | [:trigger] 60 | end 61 | 62 | def available_on 63 | [:save, :update, :create] 64 | end 65 | 66 | def default_message 67 | "#{self.class.name.split('::').last} violated on the table #{table_name} column #{column_name}" 68 | end 69 | 70 | def default_on 71 | :save if trigger? 72 | end 73 | 74 | def default_as 75 | :trigger 76 | end 77 | 78 | def default_create_trigger_name 79 | "trg_mv_#{table_name}_ins" if create? && trigger? 80 | end 81 | 82 | def default_update_trigger_name 83 | "trg_mv_#{table_name}_upd" if update? && trigger? 84 | end 85 | 86 | def default_allow_nil 87 | false 88 | end 89 | 90 | def default_allow_blank 91 | false 92 | end 93 | 94 | def compose_full_message message 95 | "#{column_name.to_s} #{message}" 96 | end 97 | 98 | private 99 | 100 | def trigger? 101 | as.try(:to_sym) == :trigger 102 | end 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/mv/core/validation/builder/inclusion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/inclusion' 4 | require 'mv/core/validation/builder/inclusion' 5 | 6 | describe Mv::Core::Validation::Builder::Inclusion do 7 | def inclusion(opts = {}) 8 | Mv::Core::Validation::Inclusion.new(:table_name, 9 | :column_name, 10 | { in: [1, 5], message: 'is included'}.merge(opts)) 11 | end 12 | 13 | describe "#initalize" do 14 | subject { described_class.new(inclusion) } 15 | 16 | its(:validation) { is_expected.to eq(inclusion) } 17 | its(:in) { is_expected.to eq(inclusion.in) } 18 | its(:allow_nil) { is_expected.to eq(inclusion.allow_nil) } 19 | its(:allow_blank) { is_expected.to eq(inclusion.allow_blank) } 20 | its(:column_name) { is_expected.to eq(inclusion.column_name) } 21 | its(:message) { is_expected.to eq(inclusion.full_message) } 22 | end 23 | 24 | describe "#conditions" do 25 | subject { described_class.new(inclusion(opts)).conditions } 26 | 27 | describe "when integers array passed" do 28 | let(:opts) { { in: [1, 5], message: 'is included' } } 29 | 30 | it { is_expected.to eq([{ 31 | statement: 'column_name IS NOT NULL AND column_name IN (1, 5)', 32 | message: 'column_name is included' 33 | }]) } 34 | end 35 | 36 | describe "when range passed" do 37 | let(:opts) { { in: 1..3 } } 38 | 39 | it { is_expected.to eq([{ 40 | statement: 'column_name IS NOT NULL AND column_name BETWEEN 1 AND 3', 41 | message: 'column_name is included' 42 | }]) } 43 | end 44 | 45 | describe "when string range passed" do 46 | let(:opts) { { in: 'a'..'c' } } 47 | 48 | it { is_expected.to eq([{ 49 | statement: "column_name IS NOT NULL AND column_name BETWEEN 'a' AND 'c'", 50 | message: 'column_name is included' 51 | }]) } 52 | end 53 | 54 | describe "when strings array passed" do 55 | let(:opts) { {in: ['a', 'c']} } 56 | 57 | it { is_expected.to eq([{ 58 | statement: "column_name IS NOT NULL AND column_name IN ('a', 'c')", 59 | message: 'column_name is included' 60 | }]) } 61 | end 62 | 63 | describe "when floats array passed" do 64 | let(:opts) { {in: [1.5, 1.8]} } 65 | 66 | it { is_expected.to eq([{ 67 | statement: "column_name IS NOT NULL AND column_name IN (1.5, 1.8)", 68 | message: 'column_name is included' 69 | }]) } 70 | end 71 | 72 | describe "when not supported types array passed" do 73 | let(:opts) { {in: [{}, {}]} } 74 | 75 | it 'raises an error' do 76 | expect{ subject }.to raise_error(Mv::Core::Error) 77 | end 78 | end 79 | 80 | describe "when nil is allowed" do 81 | let(:opts) { { in: [1, 5], allow_nil: true } } 82 | 83 | it { is_expected.to eq([{ 84 | statement: 'column_name IN (1, 5) OR column_name IS NULL', 85 | message: 'column_name is included' 86 | }]) } 87 | end 88 | 89 | describe "when blank is allowed" do 90 | let(:opts) { { in: [1, 5], allow_blank: true } } 91 | 92 | it { is_expected.to eq([{ 93 | statement: 'column_name IN (1, 5) OR column_name IS NULL OR LENGTH(TRIM(column_name)) = 0', 94 | message: 'column_name is included' 95 | }]) } 96 | end 97 | 98 | describe "when blank and nill are both allowed" do 99 | let(:opts) { { in: [1, 5], allow_blank: true, allow_nil: true } } 100 | 101 | it { is_expected.to eq([{ 102 | statement: 'column_name IN (1, 5) OR column_name IS NULL OR LENGTH(TRIM(column_name)) = 0', 103 | message: 'column_name is included' 104 | }]) } 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/mv/core/validation/builder/exclusion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/exclusion' 4 | require 'mv/core/validation/builder/exclusion' 5 | 6 | describe Mv::Core::Validation::Builder::Exclusion do 7 | def exclusion(opts = {}) 8 | Mv::Core::Validation::Exclusion.new(:table_name, 9 | :column_name, 10 | { in: [1, 5], message: 'is excluded' }.merge(opts)) 11 | end 12 | 13 | describe "#initalize" do 14 | subject { described_class.new(exclusion) } 15 | 16 | its(:validation) { is_expected.to eq(exclusion) } 17 | its(:in) { is_expected.to eq(exclusion.in) } 18 | its(:allow_nil) { is_expected.to eq(exclusion.allow_nil) } 19 | its(:allow_blank) { is_expected.to eq(exclusion.allow_blank) } 20 | its(:column_name) { is_expected.to eq(exclusion.column_name) } 21 | its(:message) { is_expected.to eq(exclusion.full_message) } 22 | end 23 | 24 | describe "#conditions" do 25 | subject { described_class.new(exclusion(opts)).conditions } 26 | 27 | describe "when integers array passed" do 28 | let(:opts) { { in: [1, 5], message: 'is excluded' } } 29 | 30 | it { is_expected.to eq([{ 31 | statement: 'column_name IS NOT NULL AND column_name NOT IN (1, 5)', 32 | message: 'column_name is excluded' 33 | }]) } 34 | end 35 | 36 | describe "when range passed" do 37 | let(:opts) { { in: 1..3 } } 38 | 39 | it { is_expected.to eq([{ 40 | statement: 'column_name IS NOT NULL AND column_name < 1 OR column_name > 3', 41 | message: 'column_name is excluded' 42 | }]) } 43 | end 44 | 45 | describe "when string range passed" do 46 | let(:opts) { { in: 'a'..'c' } } 47 | 48 | it { is_expected.to eq([{ 49 | statement: "column_name IS NOT NULL AND column_name < 'a' OR column_name > 'c'", 50 | message: 'column_name is excluded' 51 | }]) } 52 | end 53 | 54 | describe "when strings array passed" do 55 | let(:opts) { {in: ['a', 'c']} } 56 | 57 | it { is_expected.to eq([{ 58 | statement: "column_name IS NOT NULL AND column_name NOT IN ('a', 'c')", 59 | message: 'column_name is excluded' 60 | }]) } 61 | end 62 | 63 | describe "when floats array passed" do 64 | let(:opts) { {in: [1.5, 1.8]} } 65 | 66 | it { is_expected.to eq([{ 67 | statement: "column_name IS NOT NULL AND column_name NOT IN (1.5, 1.8)", 68 | message: 'column_name is excluded' 69 | }]) } 70 | end 71 | 72 | describe "when not supported types array passed" do 73 | let(:opts) { {in: [{}, {}]} } 74 | 75 | it 'raises an error' do 76 | expect{ subject }.to raise_error(Mv::Core::Error) 77 | end 78 | end 79 | 80 | describe "when nil is allowed" do 81 | let(:opts) { { in: [1, 5], allow_nil: true } } 82 | 83 | it { is_expected.to eq([{ 84 | statement: 'column_name NOT IN (1, 5) OR column_name IS NULL', 85 | message: 'column_name is excluded' 86 | }]) } 87 | end 88 | 89 | describe "when blank is allowed" do 90 | let(:opts) { { in: [1, 5], allow_blank: true } } 91 | 92 | it { is_expected.to eq([{ 93 | statement: 'column_name NOT IN (1, 5) OR column_name IS NULL OR LENGTH(TRIM(column_name)) = 0', 94 | message: 'column_name is excluded' 95 | }]) } 96 | end 97 | 98 | describe "when blank and nill are both allowed" do 99 | let(:opts) { { in: [1, 5], allow_blank: true, allow_nil: true } } 100 | 101 | it { is_expected.to eq([{ 102 | statement: 'column_name NOT IN (1, 5) OR column_name IS NULL OR LENGTH(TRIM(column_name)) = 0', 103 | message: 'column_name is excluded' 104 | }]) } 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/mv/core/active_record/base_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | 5 | describe Mv::Core::ActiveRecord::BaseDecorator do 6 | def db 7 | ::ActiveRecord::Base.connection 8 | end 9 | 10 | before do 11 | db.drop_table(:table_name) if db.data_source_exists?(:table_name) 12 | db.create_table :table_name do |t| 13 | t.column :column_name, column_type 14 | end 15 | 16 | end 17 | 18 | let(:klass) { 19 | Object.send(:remove_const, :TestBaseDecoratorClass) if defined?(TestBaseDecoratorClass) 20 | 21 | TestBaseDecoratorClass = Class.new(::ActiveRecord::Base) do 22 | self.table_name = 'table_name' 23 | 24 | enforce_migration_validations 25 | end 26 | } 27 | 28 | subject { klass.new(column_name: :column_value) } 29 | 30 | let(:column_type) { :string } 31 | 32 | context 'when migration validators table is not initialized' do 33 | it 'does not raise an error' do 34 | expect { subject.valid? }.not_to raise_error 35 | end 36 | end 37 | 38 | context 'when migration validators table properly initialized' do 39 | before do 40 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 41 | 42 | create(:migration_validator, 43 | table_name: :table_name, 44 | column_name: :column_name, 45 | validation_type: validation_type, 46 | options: opts) 47 | end 48 | 49 | describe "when custom validation defined" do 50 | let(:validation_type) { :custom } 51 | let(:opts) { { statement: '{column_name} IS NOT NULL'} } 52 | 53 | it { is_expected.not_to validate_presence_of(:column_name) } 54 | end 55 | 56 | describe "when presence validation defined" do 57 | let(:validation_type) { :presence } 58 | let(:opts) { {} } 59 | 60 | it { is_expected.to validate_presence_of(:column_name) } 61 | end 62 | 63 | describe "when uniqueness validation defined" do 64 | let(:validation_type) { :uniqueness } 65 | let(:opts) { {} } 66 | 67 | it { is_expected.to validate_uniqueness_of(:column_name) } 68 | end 69 | 70 | describe "when absence validation defined" do 71 | let(:validation_type) { :absence } 72 | let(:opts) { {} } 73 | 74 | it { is_expected.to validate_absence_of(:column_name) } 75 | end 76 | 77 | describe "when inclusion validation defined" do 78 | let(:validation_type) { :inclusion } 79 | 80 | describe "array" do 81 | let(:opts) { { in: ['str1', 'str2'] } } 82 | 83 | it { is_expected.to validate_inclusion_of(:column_name).in_array(['str1', 'str2']) } 84 | end 85 | 86 | describe "range" do 87 | let(:column_type) { :integer } 88 | let(:opts) { { in: 1..3 } } 89 | 90 | it { is_expected.to validate_inclusion_of(:column_name).in_range(1..3) } 91 | end 92 | end 93 | 94 | describe "when exclusion validation defined" do 95 | let(:validation_type) { :exclusion } 96 | 97 | describe "array" do 98 | let(:opts) { { in: ['str1', 'str2'] } } 99 | 100 | it { is_expected.to validate_exclusion_of(:column_name).in_array(['str1', 'str2']) } 101 | end 102 | 103 | describe "range" do 104 | let(:column_type) { :integer } 105 | let(:opts) { { in: 1..3 } } 106 | 107 | it { is_expected.to validate_exclusion_of(:column_name).in_range(1..3) } 108 | end 109 | end 110 | 111 | describe "when length validation defined" do 112 | let(:validation_type) { :length } 113 | 114 | describe ":in" do 115 | let(:opts) { { in: 1..3 } } 116 | 117 | it { is_expected.to validate_length_of(:column_name) } 118 | end 119 | 120 | describe ":is" do 121 | let(:opts) { { is: 1 } } 122 | 123 | it { is_expected.to validate_length_of(:column_name).is_equal_to(1) } 124 | end 125 | 126 | describe ":within" do 127 | let(:opts) { { within: 1..3 } } 128 | 129 | it { is_expected.to validate_length_of(:column_name) } 130 | end 131 | 132 | describe ":minimum" do 133 | let(:opts) { { minimum: 1 } } 134 | 135 | it { is_expected.to validate_length_of(:column_name).is_at_least(1) } 136 | end 137 | 138 | describe ":maximum" do 139 | let(:opts) { { maximum: 1 } } 140 | 141 | it { is_expected.to validate_length_of(:column_name).is_at_most(1) } 142 | end 143 | end 144 | end 145 | 146 | end 147 | -------------------------------------------------------------------------------- /spec/mv/core/validation/factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/factory' 4 | 5 | describe Mv::Core::Validation::Factory do 6 | subject(:factory) { described_class } 7 | 8 | describe "exclusion" do 9 | subject { factory.create_validation(:table_name, 10 | :column_name, 11 | :exclusion, 12 | { as: :check })} 13 | 14 | it { is_expected.to be_kind_of(Mv::Core::Validation::Exclusion) } 15 | its(:as) { is_expected.to eq(:check) } 16 | end 17 | 18 | describe "uniqueness" do 19 | subject { factory.create_validation(:table_name, 20 | :column_name, 21 | :uniqueness, 22 | { as: :check })} 23 | 24 | it { is_expected.to be_kind_of(Mv::Core::Validation::Uniqueness) } 25 | its(:as) { is_expected.to eq(:check) } 26 | end 27 | 28 | describe "inclusion" do 29 | subject { factory.create_validation(:table_name, 30 | :column_name, 31 | :inclusion, 32 | { as: :check })} 33 | 34 | it { is_expected.to be_kind_of(Mv::Core::Validation::Inclusion) } 35 | its(:as) { is_expected.to eq(:check) } 36 | end 37 | 38 | describe "length" do 39 | subject { factory.create_validation(:table_name, 40 | :column_name, 41 | :length, 42 | { as: :check })} 43 | 44 | it { is_expected.to be_kind_of(Mv::Core::Validation::Length) } 45 | its(:as) { is_expected.to eq(:check) } 46 | end 47 | 48 | describe "presence" do 49 | subject { factory.create_validation(:table_name, 50 | :column_name, 51 | :presence, 52 | { as: :check })} 53 | 54 | it { is_expected.to be_kind_of(Mv::Core::Validation::Presence) } 55 | its(:as) { is_expected.to eq(:check) } 56 | end 57 | 58 | describe "absence" do 59 | subject { factory.create_validation(:table_name, 60 | :column_name, 61 | :absence, 62 | { as: :check })} 63 | 64 | it { is_expected.to be_kind_of(Mv::Core::Validation::Absence) } 65 | its(:as) { is_expected.to eq(:check) } 66 | end 67 | 68 | describe "custom" do 69 | subject { factory.create_validation(:table_name, 70 | :column_name, 71 | :custom, 72 | { as: :check })} 73 | 74 | it { is_expected.to be_kind_of(Mv::Core::Validation::Custom) } 75 | its(:as) { is_expected.to eq(:check) } 76 | end 77 | 78 | 79 | describe "when other validation provided" do 80 | let(:klass) { TestClass = Class.new(Mv::Core::Validation::Uniqueness) } 81 | 82 | before { described_class.register_validation(:uniqueness, klass) } 83 | 84 | subject { factory.create_validation(:table_name, 85 | :column_name, 86 | :uniqueness, 87 | { as: :check })} 88 | 89 | it { is_expected.to be_instance_of(klass) } 90 | 91 | after { described_class.register_validation(:uniqueness, Mv::Core::Validation::Uniqueness) } 92 | end 93 | 94 | describe "when custom validations provided" do 95 | let(:klass) { TestClass1 = Class.new(Mv::Core::Validation::Uniqueness) } 96 | 97 | before { described_class.register_validations(:uniqueness => klass) } 98 | 99 | subject { factory.create_validation(:table_name, 100 | :column_name, 101 | :uniqueness, 102 | { as: :check })} 103 | 104 | it { is_expected.to be_instance_of(klass) } 105 | 106 | after { described_class.register_validations(:uniqueness => Mv::Core::Validation::Uniqueness) } 107 | end 108 | 109 | describe "when requested validation is not defined" do 110 | subject { factory.create_validation(:table_name, 111 | :column_name, 112 | :unknown, 113 | { as: :check })} 114 | 115 | it "raises an error" do 116 | expect{ subject }.to raise_error(Mv::Core::Error) 117 | end 118 | end 119 | end -------------------------------------------------------------------------------- /spec/mv/core/constraint/builder/index_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/constraint/index' 5 | require 'mv/core/error' 6 | require 'mv/core/constraint/builder/index' 7 | 8 | describe Mv::Core::Constraint::Builder::Index do 9 | before do 10 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 11 | Mv::Core::Db::MigrationValidator.delete_all 12 | end 13 | 14 | let(:index_description) { Mv::Core::Constraint::Description.new(:idx_mv_table_name, :index) } 15 | let(:index_constraint) { Mv::Core::Constraint::Index.new(index_description) } 16 | let(:uniqueness) { 17 | Mv::Core::Validation::Uniqueness.new(:table_name, 18 | :column_name, 19 | as: :index, 20 | index_name: :idx_mv_table_name) 21 | } 22 | 23 | before do 24 | index_constraint.validations << uniqueness 25 | end 26 | 27 | subject(:index_builder) { described_class.new(index_constraint) } 28 | 29 | describe "#initialization" do 30 | its(:constraint) { is_expected.to eq(index_constraint) } 31 | end 32 | 33 | describe "SQL methods" do 34 | before do 35 | if ActiveRecord::Base.connection.index_name_exists?(:table_name, :idx_mv_table_name, false) 36 | ActiveRecord::Base.connection.remove_index!(:table_name, :idx_mv_table_name) 37 | end 38 | 39 | Mv::Core::Migration::Base.with_suppressed_validations do 40 | ActiveRecord::Base.connection.drop_table(:table_name) if ActiveRecord::Base.connection.data_source_exists?(:table_name) 41 | 42 | ActiveRecord::Base.connection.create_table(:table_name) do |t| 43 | t.string :column_name 44 | t.string :column_name_1 45 | end 46 | end 47 | end 48 | 49 | describe "#create" do 50 | before { index_builder.create } 51 | 52 | subject { ActiveRecord::Base.connection.indexes(:table_name).find{|idx| idx.name == "idx_mv_table_name"} } 53 | 54 | it { is_expected.to be_present } 55 | its(:name) { is_expected.to eq('idx_mv_table_name')} 56 | its(:columns) { is_expected.to eq(['column_name'])} 57 | 58 | describe 'when called second time' do 59 | before do 60 | index_constraint.validations << Mv::Core::Validation::Uniqueness.new(:table_name, 61 | :column_name_1, 62 | as: :index, 63 | index_name: :idx_mv_table_name) 64 | index_builder.create 65 | end 66 | 67 | its(:columns) { is_expected.to eq(['column_name', 'column_name_1'])} 68 | end 69 | end 70 | 71 | describe "#update" do 72 | subject { ActiveRecord::Base.connection.indexes(:table_name).find{|idx| idx.name == "idx_mv_table_name"} } 73 | 74 | describe "when index exists" do 75 | before do 76 | index_builder.create 77 | index_constraint.validations << Mv::Core::Validation::Uniqueness.new(:table_name, 78 | :column_name_1, 79 | as: :index, 80 | index_name: :idx_mv_table_name) 81 | index_builder.update(index_builder) 82 | end 83 | 84 | its(:columns) { is_expected.to eq(['column_name', 'column_name_1'])} 85 | end 86 | 87 | describe "when index does not exist" do 88 | before do 89 | index_builder.update(index_builder) 90 | end 91 | 92 | it { is_expected.to be_present } 93 | end 94 | end 95 | 96 | describe "#delete" do 97 | before { 98 | ActiveRecord::Base.connection.add_index(:table_name, :column_name, name: :idx_mv_table_name) 99 | index_builder.delete 100 | } 101 | 102 | subject { ActiveRecord::Base.connection.indexes(:table_name).find{|idx| idx.name == "idx_mv_table_name"} } 103 | 104 | it { is_expected.to be_nil } 105 | 106 | describe "when called second time" do 107 | it "should not raise error" do 108 | expect{ index_builder.delete }.not_to raise_error 109 | end 110 | end 111 | 112 | describe "when table does not exist" do 113 | before { ActiveRecord::Base.connection.drop_table(:table_name) } 114 | 115 | it "should not raise error" do 116 | expect{ index_builder.delete }.not_to raise_error 117 | end 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/mv/core/services/load_constraints_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/create_migration_validators_table' 4 | require 'mv/core/services/load_constraints' 5 | require 'mv/core/validation/uniqueness' 6 | 7 | describe Mv::Core::Services::LoadConstraints do 8 | before do 9 | Mv::Core::Services::CreateMigrationValidatorsTable.new.execute 10 | end 11 | 12 | describe "#initialize" do 13 | subject { described_class.new([:table_1, :table_2]) } 14 | 15 | its(:tables) { is_expected.to match_array([:table_1, :table_2]) } 16 | end 17 | 18 | describe "#execute" do 19 | 20 | describe "1 migration validator, 1 validation => 1 constraint" do 21 | let!(:migration_validator_trigger) { 22 | create(:migration_validator, table_name: :table_name, 23 | column_name: :column_name, 24 | validation_type: :uniqueness, 25 | options: { as: :trigger, on: :create }) 26 | } 27 | 28 | subject(:constraints) { described_class.new([:table_name]).execute.sort } 29 | 30 | its(:length) { is_expected.to eq(1) } 31 | 32 | describe "first constraint" do 33 | subject { constraints.first } 34 | 35 | its(:description) { is_expected.to eq(Mv::Core::Constraint::Description.new(:trg_mv_table_name_ins, :trigger, event: :create)) } 36 | 37 | its(:validations) { is_expected.to match_array([ 38 | Mv::Core::Validation::Uniqueness.new(:table_name, :column_name, as: :trigger, on: :create) 39 | ])} 40 | 41 | end 42 | end 43 | 44 | describe "1 migration validator, 2 validations => 2 constraints" do 45 | let!(:migration_validator_trigger) { 46 | create(:migration_validator, table_name: :table_name, 47 | column_name: :column_name, 48 | options: { as: :trigger, on: :save }) 49 | } 50 | 51 | subject(:constraints) { described_class.new([:table_name]).execute.sort } 52 | 53 | its(:length) { is_expected.to eq(2) } 54 | 55 | describe "first constraint" do 56 | subject { constraints.first } 57 | 58 | its(:description) { is_expected.to eq(Mv::Core::Constraint::Description.new(:trg_mv_table_name_ins, :trigger, event: :create)) } 59 | 60 | its(:validations) { is_expected.to match_array([ 61 | Mv::Core::Validation::Uniqueness.new(:table_name, :column_name, as: :trigger, on: :save) 62 | ])} 63 | end 64 | 65 | describe "second constraint" do 66 | subject { constraints.second } 67 | 68 | its(:description) { is_expected.to eq(Mv::Core::Constraint::Description.new(:trg_mv_table_name_upd, :trigger, event: :update)) } 69 | 70 | its(:validations) { is_expected.to match_array([ 71 | Mv::Core::Validation::Uniqueness.new(:table_name, :column_name, as: :trigger, on: :save) 72 | ])} 73 | end 74 | end 75 | 76 | describe "2 migration validators, 2 validations => 2 constraints" do 77 | let!(:migration_validator_trigger) { 78 | create(:migration_validator, table_name: :table_name, 79 | column_name: :column_name, 80 | options: { as: :trigger, on: :create }) 81 | } 82 | 83 | let!(:migration_validator_index) { 84 | create(:migration_validator, table_name: :table_name_1, 85 | column_name: :column_name, 86 | options: { as: :index }) 87 | } 88 | 89 | subject(:constraints) { described_class.new([:table_name, :table_name_1]).execute.sort } 90 | 91 | its(:length) { is_expected.to eq(2) } 92 | 93 | describe "first constraint" do 94 | subject { constraints.first } 95 | 96 | its(:description) { is_expected.to eq(Mv::Core::Constraint::Description.new(:idx_mv_table_name_1_column_name_uniq, :index)) } 97 | 98 | its(:validations) { is_expected.to match_array([ 99 | Mv::Core::Validation::Uniqueness.new(:table_name_1, :column_name, as: :index) 100 | ])} 101 | end 102 | 103 | describe "second constraint" do 104 | subject { constraints.second } 105 | 106 | its(:description) { is_expected.to eq(Mv::Core::Constraint::Description.new(:trg_mv_table_name_ins, :trigger, event: :create)) } 107 | 108 | its(:validations) { is_expected.to match_array([ 109 | Mv::Core::Validation::Uniqueness.new(:table_name, :column_name, as: :trigger, on: :create) 110 | ])} 111 | end 112 | 113 | describe "when one table is filtered out" do 114 | subject { described_class.new([:table_name]).execute } 115 | 116 | its(:length) { is_expected.to eq(1) } 117 | end 118 | end 119 | end 120 | end -------------------------------------------------------------------------------- /spec/mv/core/services/say_constraints_diff_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/services/say_constraints_diff' 4 | 5 | describe Mv::Core::Services::SayConstraintsDiff do 6 | let(:constraint_description) { 7 | Mv::Core::Constraint::Description.new(:trg_table_name_upd, :trigger, event: :update) 8 | } 9 | 10 | let(:constraint_presenter) { 11 | Mv::Core::Presenter::Constraint::Description.new(constraint_description) 12 | } 13 | 14 | let(:old_constraint) { Mv::Core::Constraint::Trigger.new(constraint_description) } 15 | let(:new_constraint) { Mv::Core::Constraint::Trigger.new(constraint_description) } 16 | 17 | let(:uniqueness) { 18 | Mv::Core::Validation::Uniqueness.new(:table_name, 19 | :column_name, 20 | as: :trigger) 21 | } 22 | 23 | let(:validation_presenter) { 24 | Mv::Core::Presenter::Validation::Base.new(uniqueness) 25 | } 26 | 27 | describe "#initialize" do 28 | subject { described_class.new(old_constraint, new_constraint) } 29 | 30 | its(:old_constraint) { is_expected.to eq(old_constraint) } 31 | its(:new_constraint) { is_expected.to eq(new_constraint) } 32 | end 33 | 34 | describe "#execute" do 35 | describe "when new constraint is not provided" do 36 | before { old_constraint.validations << uniqueness } 37 | 38 | subject do 39 | described_class.new(old_constraint, nil).execute do 40 | ::ActiveRecord::Migration.say('internal code') 41 | end 42 | end 43 | 44 | it "outputs constraint description as deleted and it's validations as deleted" do 45 | expect(::ActiveRecord::Migration).to receive(:say_with_time).with("delete #{constraint_presenter} on table \"table_name\"").and_call_original 46 | expect(::ActiveRecord::Migration).to receive(:say).once.ordered.with("delete #{validation_presenter}", true) 47 | expect(::ActiveRecord::Migration).to receive(:say).once.ordered.with("internal code") 48 | subject 49 | end 50 | end 51 | 52 | describe "when old constraint is not provided" do 53 | before { new_constraint.validations << uniqueness } 54 | 55 | subject do 56 | described_class.new(nil, new_constraint).execute do 57 | ::ActiveRecord::Migration.say('internal code') 58 | end 59 | end 60 | 61 | it "outputs constraint description as added and it's validations as added" do 62 | expect(::ActiveRecord::Migration).to receive(:say_with_time).with("create #{constraint_presenter} on table \"table_name\"").and_call_original 63 | expect(::ActiveRecord::Migration).to receive(:say).once.ordered.with("create #{validation_presenter}", true) 64 | expect(::ActiveRecord::Migration).to receive(:say).once.ordered.with("internal code") 65 | subject 66 | end 67 | end 68 | 69 | describe "when both new & old constraints are provided" do 70 | subject do 71 | described_class.new(old_constraint, new_constraint).execute do 72 | ::ActiveRecord::Migration.say('internal code') 73 | end 74 | end 75 | 76 | describe "when old constrains contains validation that is not included to the new one" do 77 | before { old_constraint.validations << uniqueness } 78 | 79 | it "outputs constraint description as deleted and it's validations as deleted" do 80 | expect(::ActiveRecord::Migration).to receive(:say_with_time).with("update #{constraint_presenter} on table \"table_name\"").and_call_original 81 | expect(::ActiveRecord::Migration).to receive(:say).once.ordered.with("delete #{validation_presenter}", true) 82 | expect(::ActiveRecord::Migration).to receive(:say).once.ordered.with("internal code") 83 | subject 84 | end 85 | end 86 | 87 | describe "when new constraint contains validation that is not included to the old one" do 88 | before { new_constraint.validations << uniqueness } 89 | 90 | it "outputs constraint description as added and it's validations as added" do 91 | expect(::ActiveRecord::Migration).to receive(:say_with_time).with("update #{constraint_presenter} on table \"table_name\"").and_call_original 92 | expect(::ActiveRecord::Migration).to receive(:say).once.ordered.with("create #{validation_presenter}", true) 93 | expect(::ActiveRecord::Migration).to receive(:say).once.ordered.with("internal code") 94 | subject 95 | end 96 | end 97 | 98 | describe "when both new & old constraint has the same validations list" do 99 | before { 100 | new_constraint.validations << uniqueness 101 | old_constraint.validations << uniqueness 102 | } 103 | 104 | it "outputs nothing" do 105 | expect(::ActiveRecord::Migration).not_to receive(:say) 106 | subject 107 | end 108 | end 109 | end 110 | end 111 | end -------------------------------------------------------------------------------- /spec/mv/core/validation/builder/length_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'mv/core/validation/length' 4 | require 'mv/core/validation/builder/length' 5 | 6 | describe Mv::Core::Validation::Builder::Length do 7 | def length(opts = {}) 8 | Mv::Core::Validation::Length.new(:table_name, 9 | :column_name, 10 | { message: 'has invalid length' }.merge(opts)) 11 | end 12 | 13 | describe "#initalize" do 14 | let(:validation) { length(is: 5) } 15 | subject { described_class.new(validation) } 16 | 17 | its(:validation) { is_expected.to eq(validation) } 18 | its(:in) { is_expected.to eq(validation.in) } 19 | its(:within) { is_expected.to eq(validation.within) } 20 | its(:is) { is_expected.to eq(validation.is) } 21 | its(:maximum) { is_expected.to eq(validation.maximum) } 22 | its(:minimum) { is_expected.to eq(validation.minimum) } 23 | its(:allow_nil) { is_expected.to eq(validation.allow_nil) } 24 | its(:allow_blank) { is_expected.to eq(validation.allow_blank) } 25 | its(:column_name) { is_expected.to eq(validation.column_name) } 26 | its(:message) { is_expected.to eq(validation.full_message) } 27 | its(:too_short) { is_expected.to eq(validation.full_too_short) } 28 | its(:too_long) { is_expected.to eq(validation.full_too_long) } 29 | end 30 | 31 | describe "conditions" do 32 | subject { described_class.new(length(opts)).conditions } 33 | 34 | 35 | describe "when :in is defined" do 36 | describe "as array" do 37 | let(:opts) { { in: [1, 3] } } 38 | 39 | it { is_expected.to eq([{ 40 | statement: 'column_name IS NOT NULL AND LENGTH(column_name) IN (1, 3)', 41 | message: 'column_name has invalid length' 42 | }]) } 43 | end 44 | 45 | describe "as range" do 46 | let(:opts) { { in: 1..3 } } 47 | 48 | it { is_expected.to eq([{ 49 | statement: 'column_name IS NOT NULL AND LENGTH(column_name) BETWEEN 1 AND 3', 50 | message: 'column_name has invalid length' 51 | }]) } 52 | end 53 | end 54 | 55 | describe "when :within is defined" do 56 | describe "as array" do 57 | let(:opts) { { within: [1, 3] } } 58 | 59 | it { is_expected.to eq([{ 60 | statement: 'column_name IS NOT NULL AND LENGTH(column_name) IN (1, 3)', 61 | message: 'column_name has invalid length' 62 | }]) } 63 | end 64 | 65 | describe "as range" do 66 | let(:opts) { { within: 1..3 } } 67 | 68 | it { is_expected.to eq([{ 69 | statement: 'column_name IS NOT NULL AND LENGTH(column_name) BETWEEN 1 AND 3', 70 | message: 'column_name has invalid length' 71 | }]) } 72 | end 73 | end 74 | 75 | describe "when :is is defined" do 76 | let(:opts) { { is: 1 } } 77 | 78 | it { is_expected.to eq([{ 79 | statement: 'column_name IS NOT NULL AND LENGTH(column_name) = 1', 80 | message: 'column_name has invalid length' 81 | }]) } 82 | end 83 | 84 | describe "when :maximum is defined" do 85 | let(:opts) { { maximum: 3, too_long: 'is longer than expected' } } 86 | 87 | it { is_expected.to eq([{ 88 | statement: 'column_name IS NOT NULL AND LENGTH(column_name) <= 3', 89 | message: 'column_name is longer than expected' 90 | }]) } 91 | end 92 | 93 | describe "when :minimum is defined" do 94 | let(:opts) { { minimum: 1, too_short: 'is shorter than expected' } } 95 | 96 | it { is_expected.to eq([{ 97 | statement: 'column_name IS NOT NULL AND LENGTH(column_name) >= 1', 98 | message: 'column_name is shorter than expected' 99 | }]) } 100 | end 101 | 102 | describe "when both :maximum & :minimum are defined" do 103 | # it { is_expected.to eq('LENGTH(column_name) BETWEEN 1 AND 3') } 104 | describe "and too long and too short messages are the same" do 105 | let(:opts) { { minimum: 1, maximum: 3, too_long: 'is longer than expected', too_short: 'is shorter than expected' } } 106 | 107 | it { is_expected.to match_array([ 108 | { statement: 'column_name IS NOT NULL AND LENGTH(column_name) >= 1', 109 | message: 'column_name is shorter than expected' }, 110 | { statement: 'column_name IS NOT NULL AND LENGTH(column_name) <= 3', 111 | message: 'column_name is longer than expected' } 112 | ]) } 113 | 114 | end 115 | 116 | describe "and too long and too short messages are different" do 117 | let(:opts) { { minimum: 1, maximum: 3, too_long: 'has invalid length', too_short: 'has invalid length' } } 118 | 119 | it { is_expected.to eq([{ 120 | statement: 'column_name IS NOT NULL AND LENGTH(column_name) BETWEEN 1 AND 3', 121 | message: 'column_name has invalid length' 122 | }]) } 123 | end 124 | end 125 | 126 | describe "when nil is allowed" do 127 | let(:opts) { { is: 1, allow_nil: true } } 128 | 129 | 130 | it { is_expected.to eq([{ 131 | statement: 'LENGTH(column_name) = 1 OR column_name IS NULL', 132 | message: 'column_name has invalid length' 133 | }]) } 134 | end 135 | 136 | describe "when blank is allowed" do 137 | let(:opts) { { is: 1, allow_blank: true } } 138 | 139 | it { is_expected.to eq([{ 140 | statement: 'LENGTH(column_name) = 1 OR column_name IS NULL OR LENGTH(TRIM(column_name)) = 0', 141 | message: 'column_name has invalid length' 142 | }]) } 143 | end 144 | 145 | describe "when both nil & blank are allowed" do 146 | let(:opts) { { is: 1, allow_nil: true, allow_blank: true } } 147 | 148 | it { is_expected.to eq([{ 149 | statement: 'LENGTH(column_name) = 1 OR column_name IS NULL OR LENGTH(TRIM(column_name)) = 0', 150 | message: 'column_name has invalid length' 151 | }]) } 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/svenfuchs/i18n.git 3 | revision: d5fb5dd1fa204048f4c4a0123c961fa457640a69 4 | specs: 5 | i18n (0.8.0.beta1) 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | actionpack (5.0.1) 11 | actionview (= 5.0.1) 12 | activesupport (= 5.0.1) 13 | rack (~> 2.0) 14 | rack-test (~> 0.6.3) 15 | rails-dom-testing (~> 2.0) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 17 | actionview (5.0.1) 18 | activesupport (= 5.0.1) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 2.0) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 23 | activemodel (5.0.1) 24 | activesupport (= 5.0.1) 25 | activerecord (5.0.1) 26 | activemodel (= 5.0.1) 27 | activesupport (= 5.0.1) 28 | arel (~> 7.0) 29 | activesupport (5.0.1) 30 | concurrent-ruby (~> 1.0, >= 1.0.2) 31 | i18n (~> 0.7) 32 | minitest (~> 5.1) 33 | tzinfo (~> 1.1) 34 | addressable (2.5.0) 35 | public_suffix (~> 2.0, >= 2.0.2) 36 | arel (7.1.4) 37 | builder (3.2.2) 38 | byebug (9.0.6) 39 | coderay (1.1.1) 40 | concurrent-ruby (1.0.4) 41 | coveralls (0.8.17) 42 | json (>= 1.8, < 3) 43 | simplecov (~> 0.12.0) 44 | term-ansicolor (~> 1.3) 45 | thor (~> 0.19.1) 46 | tins (~> 1.6) 47 | descendants_tracker (0.0.4) 48 | thread_safe (~> 0.3, >= 0.3.1) 49 | diff-lcs (1.2.5) 50 | docile (1.1.5) 51 | erubis (2.7.0) 52 | factory_girl (4.8.0) 53 | activesupport (>= 3.0.0) 54 | faraday (0.9.2) 55 | multipart-post (>= 1.2, < 3) 56 | ffi (1.9.14) 57 | formatador (0.2.5) 58 | git (1.3.0) 59 | github_api (0.11.3) 60 | addressable (~> 2.3) 61 | descendants_tracker (~> 0.0.1) 62 | faraday (~> 0.8, < 0.10) 63 | hashie (>= 1.2) 64 | multi_json (>= 1.7.5, < 2.0) 65 | nokogiri (~> 1.6.0) 66 | oauth2 67 | guard (2.14.0) 68 | formatador (>= 0.2.4) 69 | listen (>= 2.7, < 4.0) 70 | lumberjack (~> 1.0) 71 | nenv (~> 0.1) 72 | notiffany (~> 0.0) 73 | pry (>= 0.9.12) 74 | shellany (~> 0.0) 75 | thor (>= 0.18.1) 76 | guard-compat (1.2.1) 77 | guard-rspec (4.7.3) 78 | guard (~> 2.1) 79 | guard-compat (~> 1.1) 80 | rspec (>= 2.99.0, < 4.0) 81 | hashie (3.4.6) 82 | highline (1.7.8) 83 | jeweler (2.3.2) 84 | builder 85 | bundler (>= 1.0) 86 | git (>= 1.2.5) 87 | github_api (~> 0.11.0) 88 | highline (>= 1.6.15) 89 | nokogiri (>= 1.5.10) 90 | psych (~> 2.2) 91 | rake 92 | rdoc 93 | semver2 94 | json (2.0.2) 95 | jwt (1.5.6) 96 | listen (3.1.5) 97 | rb-fsevent (~> 0.9, >= 0.9.4) 98 | rb-inotify (~> 0.9, >= 0.9.7) 99 | ruby_dep (~> 1.2) 100 | loofah (2.0.3) 101 | nokogiri (>= 1.5.9) 102 | lumberjack (1.0.10) 103 | method_source (0.8.2) 104 | mini_portile2 (2.1.0) 105 | minitest (5.10.1) 106 | multi_json (1.12.1) 107 | multi_xml (0.6.0) 108 | multipart-post (2.0.0) 109 | nenv (0.3.0) 110 | nokogiri (1.6.8.1) 111 | mini_portile2 (~> 2.1.0) 112 | notiffany (0.1.1) 113 | nenv (~> 0.1) 114 | shellany (~> 0.0) 115 | oauth2 (1.3.0) 116 | faraday (>= 0.8, < 0.11) 117 | jwt (~> 1.0) 118 | multi_json (~> 1.3) 119 | multi_xml (~> 0.5) 120 | rack (>= 1.2, < 3) 121 | pry (0.10.4) 122 | coderay (~> 1.1.0) 123 | method_source (~> 0.8.1) 124 | slop (~> 3.4) 125 | pry-byebug (3.4.2) 126 | byebug (~> 9.0) 127 | pry (~> 0.10) 128 | psych (2.2.2) 129 | public_suffix (2.0.4) 130 | rack (2.0.1) 131 | rack-test (0.6.3) 132 | rack (>= 1.0) 133 | rails-dom-testing (2.0.2) 134 | activesupport (>= 4.2.0, < 6.0) 135 | nokogiri (~> 1.6) 136 | rails-html-sanitizer (1.0.3) 137 | loofah (~> 2.0) 138 | railties (5.0.1) 139 | actionpack (= 5.0.1) 140 | activesupport (= 5.0.1) 141 | method_source 142 | rake (>= 0.8.7) 143 | thor (>= 0.18.1, < 2.0) 144 | rake (12.0.0) 145 | rb-fsevent (0.9.8) 146 | rb-inotify (0.9.7) 147 | ffi (>= 0.5.0) 148 | rdoc (5.0.0) 149 | rspec (3.5.0) 150 | rspec-core (~> 3.5.0) 151 | rspec-expectations (~> 3.5.0) 152 | rspec-mocks (~> 3.5.0) 153 | rspec-core (3.5.4) 154 | rspec-support (~> 3.5.0) 155 | rspec-expectations (3.5.0) 156 | diff-lcs (>= 1.2.0, < 2.0) 157 | rspec-support (~> 3.5.0) 158 | rspec-its (1.2.0) 159 | rspec-core (>= 3.0.0) 160 | rspec-expectations (>= 3.0.0) 161 | rspec-mocks (3.5.0) 162 | diff-lcs (>= 1.2.0, < 2.0) 163 | rspec-support (~> 3.5.0) 164 | rspec-support (3.5.0) 165 | ruby_dep (1.5.0) 166 | semver2 (3.4.2) 167 | shellany (0.0.1) 168 | shoulda (3.5.0) 169 | shoulda-context (~> 1.0, >= 1.0.1) 170 | shoulda-matchers (>= 1.4.1, < 3.0) 171 | shoulda-context (1.2.2) 172 | shoulda-matchers (2.8.0) 173 | activesupport (>= 3.0.0) 174 | simplecov (0.12.0) 175 | docile (~> 1.1.0) 176 | json (>= 1.8, < 3) 177 | simplecov-html (~> 0.10.0) 178 | simplecov-html (0.10.0) 179 | slop (3.6.0) 180 | sqlite3 (1.3.12) 181 | term-ansicolor (1.4.0) 182 | tins (~> 1.0) 183 | terminal-notifier-guard (1.7.0) 184 | thor (0.19.4) 185 | thread_safe (0.3.5) 186 | tins (1.13.0) 187 | tzinfo (1.2.2) 188 | thread_safe (~> 0.1) 189 | 190 | PLATFORMS 191 | ruby 192 | 193 | DEPENDENCIES 194 | activerecord (~> 5.0) 195 | coveralls (~> 0.7) 196 | factory_girl (~> 4.5) 197 | guard-rspec (~> 4.5) 198 | i18n (~> 0.7)! 199 | jeweler (~> 2.0) 200 | pry-byebug 201 | railties (~> 5.0) 202 | rb-fsevent 203 | rspec (~> 3.1) 204 | rspec-its (~> 1.1) 205 | shoulda (~> 3.5) 206 | sqlite3 (~> 1.3) 207 | terminal-notifier-guard 208 | 209 | BUNDLED WITH 210 | 1.13.7 211 | --------------------------------------------------------------------------------