OSDN Git Service

initialize repository master
authorokimoto <okimoto@good-day.co.jp>
Wed, 9 Dec 2009 06:01:36 +0000 (15:01 +0900)
committerokimoto <okimoto@good-day.co.jp>
Wed, 9 Dec 2009 06:01:36 +0000 (15:01 +0900)
12 files changed:
.gitignore [new file with mode: 0644]
README [new file with mode: 0644]
README.ja [new file with mode: 0644]
Rakefile [new file with mode: 0644]
lib/force_columns.rb [new file with mode: 0644]
rails/init.rb [new file with mode: 0644]
spec/db/database.yml [new file with mode: 0644]
spec/db/schema.rb [new file with mode: 0644]
spec/force_columns_spec.rb [new file with mode: 0644]
spec/spec_helper.rb [new file with mode: 0644]
tasks/force_columns_tasks.rake [new file with mode: 0644]
test/force_columns_test.rb [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..3b53699
--- /dev/null
@@ -0,0 +1,4 @@
+*~
+*.log
+*.db
+
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..6db4533
--- /dev/null
+++ b/README
@@ -0,0 +1,4 @@
+ForceColumns
+============
+
+Description goes here
\ No newline at end of file
diff --git a/README.ja b/README.ja
new file mode 100644 (file)
index 0000000..02fdf98
--- /dev/null
+++ b/README.ja
@@ -0,0 +1,46 @@
+ForceColumns
+============
+
+すべてのモデルに強制的にあるカラムを作成する。
+
+現在のところ以下のものを自動的に追加する。
+
+* domain_id
+* created_at
+* updated_at
+* created_by_id
+* updated_by_id
+* created_in_id
+* updated_in_id
+* lock_version
+
+他には domain_id の更新を自動的に行ったり、filter_by_current_domain という named_scope を自動的に
+追加する。
+
+
+Example
+=======
+
+特に何もしなくても上記のカラムは追加される。
+
+追加したくないカラムがある場合:
+
+  class CreateSamples < ActiveRecord::Migration
+    def self.up
+      create_table :samples, :domain_id => false do
+        t.string :name
+      end
+    end
+    def self.down
+      drop_table :samples
+    end
+  end
+
+set_table_name を使用する場合:
+
+  class DummySample < ActiveRecord::Base
+    set_table_name 'samples'
+    force_domain
+  end
+
+
diff --git a/Rakefile b/Rakefile
new file mode 100644 (file)
index 0000000..d13fa26
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,21 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'spec/rake/spectask'
+
+desc 'Default: run specs.'
+task :default => :spec
+
+Spec::Rake::SpecTask.new do |t|
+  t.spec_files = FileList['spec/*_spec.rb']
+  t.spec_opts = ['-c']
+end
+
+desc 'Generate documentation for the force_columns plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+  rdoc.rdoc_dir = 'rdoc'
+  rdoc.title    = 'ForceColumns'
+  rdoc.options << '--line-numbers' << '--inline-source'
+  rdoc.rdoc_files.include('README')
+  rdoc.rdoc_files.include('lib/**/*.rb')
+end
diff --git a/lib/force_columns.rb b/lib/force_columns.rb
new file mode 100644 (file)
index 0000000..a195764
--- /dev/null
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+
+module AssertSQLIdentifier
+  # この値で識別子の長さの制限を行う。
+  LENGTH_MAX = 30
+
+  ANSI_SQL_RESERVED = %w[ADD ALL ALTER AND ANY AS ASC BETWEEN BY CHAR CHECK CONNECT CREATE CURRENT DATE DECIMAL DEFAULT DELETE DESC DISTINCT DROP ELSE FLOAT FOR FROM GRANT GROUP HAVING IMMEDIATE IN INSERT INTEGER INTERSECT INTO IS LEVEL LIKE NOT NULL OF ON OPTION OR ORDER PRIOR PRIVILEGES PUBLIC REVOKE ROWS SELECT SESSION SET SIZE SMALLINT TABLE THEN TO UNION UNIQUE UPDATE USER VALUES VARCHAR VIEW WHENEVER WITH]
+  ORACLE_SQL_RESERVED = %w[ACCESS AUDIT A ABORT ACCESSED ACCOUNT ACTIVATE ADMINISTER ADMINISTRATOR ADVISE ADVISOR AFTER ALGORITHM ALIAS ALLOCATE ALLOW ANALYZE CLUSTER COLUMN COMMENT COMPRESS EXCLUSIVE EXISTS FILE IDENTIFIED INCREMENT INDEX INITIAL LOCK LONG MAXEXTENTS MINUS MLSLABEL MODE MODIFY NOAUDIT NOCOMPRESS NOWAIT NUMBER OFFLINE ONLINE PCTFREE RAW RENAME RESOURCE ROW ROWID ROWNUM SHARE START SUCCESSFUL SYNONYM SYSDATE TRIGGER UID VALIDATE VARCHAR2 WHERE] # 'ADMIN' is also reserved in fact.
+  LOCAL_RESERVED = %w[APPLICATION RECORD RECORD_ID RECORD_NAME RECORD_CODE] # for record picker
+
+  def assert_length(*names)
+    names.each do |name|
+      raise ArgumentError, "exceed maximum length of name: #{name}" if name.to_s.length > LENGTH_MAX
+    end
+  end
+
+  def assert_unreserved(*names)
+    names.each do |name|
+      raise ArgumentError, "reserved identifier: #{name}" if (ANSI_SQL_RESERVED | ORACLE_SQL_RESERVED).include? name.to_s.upcase
+    end
+  end
+end
+
+# フレームワークに導入するテーブルに付随する識別子を検証する。
+module ForceColumns
+  include AssertSQLIdentifier
+
+  def create_table(name, options = {})
+    # ActiveRecordStore
+    # schema_migrations since Rails 2.1.0
+    if %w|sessions schema_migrations|.include?(name.to_s)
+      return super
+    end
+
+    # through only while loading db/schema.rb
+    if options[:force] && !options[:force_columns]
+      return super
+    end
+
+    assert_length name
+    assert_unreserved name
+    super do |t|
+      class << t
+        def column(name, type, options = {})
+          self.extend(AssertSQLIdentifier)
+          self.assert_length name
+          self.assert_unreserved name
+          super(name.to_s, type, options)
+        end
+      end
+      [
+        :domain_id,
+      ].each do |column_name|
+        unless options[column_name] == false
+          t.column column_name, :integer, :null => false
+        end
+      end
+      yield t
+      [
+        :created_at,
+        :updated_at,
+      ].each do |column_name|
+        unless options[column_name] == false
+          t.column column_name, :string, :limit => 14
+        end
+      end
+      [
+        :created_by_id,
+        :updated_by_id,
+        :created_in_id,
+        :updated_in_id,
+      ].each do |column_name|
+        unless options[column_name] == false
+          t.column column_name, :integer
+        end
+      end
+      [
+        :lock_version,
+      ].each do |column_name|
+        unless options[column_name] == false
+          t.column column_name, :integer, :null => false, :default => 0
+        end
+      end
+    end
+    self.add_index name, :domain_id unless options[:domain_id] == false
+  end
+
+  def add_column(table_name, column_name, type, options = {})
+    assert_length table_name, column_name
+    assert_unreserved table_name, column_name
+    super
+  end
+
+  def drop_table(name, options = {})
+    self.remove_index name, :domain_id rescue nil
+    super(name, options = {})
+  end
+
+  # 同じ構成の待機用テーブルも併せて作成する。
+  def create_table_with_standby(name, options = {}, &block)
+    create_table(name, options, &block)
+    if /\Awf_(.+)\z/ =~ name.to_s
+      create_table("wfb_#{$1}", options, &block)
+    else
+      create_table("b_#{name}", options, &block)
+    end
+  end
+
+  # 待機用テーブルも併せて削除する。
+  def drop_table_with_standby(name, options = {})
+    if /\Awf_(.+)\z/ =~ name.to_s
+      drop_table("wfb_#{$1}", options)
+    else
+      drop_table("b_#{name}", options)
+    end
+    drop_table(name, options)
+  end
+
+  module ForceDomain
+    def self.set_domain_model_name(name)
+      module_eval <<-"end_eval"
+        def self.current_id
+          #{name}.current_id
+        end
+      end_eval
+    end
+
+    def self.included(base) #:nodoc:
+      super
+      base.extend(ClassMethods)
+    end
+
+    module ClassMethods
+      def force_domain
+        return unless table_exists?
+        return unless column_names.include?('domain_id')
+        class_eval do
+          belongs_to :domain
+          named_scope :filter_by_current_domain, lambda{
+            current_id = ::ForceColumns::ForceDomain.current_id
+            if column_names.include?("domain_id") && current_id
+              { :conditions => ["#{table_name}.domain_id = ?", current_id] }
+            end
+          }
+          callbacks = Callbacks.new
+          before_create callbacks
+          before_update callbacks
+        end
+      end
+    end
+
+    class Callbacks # :nodoc: all
+      def initialize
+      end
+
+      def before_create(record)
+        set_current_domain(record)
+      end
+
+      def before_update(record)
+        set_current_domain(record)
+      end
+
+      private
+      def set_current_domain(record)
+        if record.class.column_names.include?("domain_id") && record.read_attribute("domain_id").nil?
+          record.write_attribute("domain_id", ::ForceColumns::ForceDomain.current_id)
+        end
+      end
+    end
+  end
+end
diff --git a/rails/init.rb b/rails/init.rb
new file mode 100644 (file)
index 0000000..9727f9c
--- /dev/null
@@ -0,0 +1,17 @@
+# Include hook code here
+require "active_record"
+require "force_columns"
+ActiveRecord::ConnectionAdapters::AbstractAdapter.__send__(:include, ForceColumns)
+ActiveRecord::Base.__send__(:include, ForceColumns::ForceDomain)
+# NOTE monkey patch. Check this code when update Rails.
+class ActiveRecord::Base
+  class << self
+    def inherited_with_force_domain(subclass)
+      inherited_without_force_domain(subclass)
+      changed
+      notify_observers :observed_class_inherited, subclass
+      subclass.__send__ :force_domain
+    end
+    alias_method_chain :inherited, :force_domain
+  end
+end
diff --git a/spec/db/database.yml b/spec/db/database.yml
new file mode 100644 (file)
index 0000000..a5574ba
--- /dev/null
@@ -0,0 +1,19 @@
+
+sqlite3:
+  :adapter: sqlite3
+  :database: spec/db/force_columns.sqlite3.db
+
+# postgresql:
+#   :adapter: postgresql
+#   :username: rails
+#   :password: rails
+#   :database: force_columns_test
+#   :min_messages: ERROR
+# 
+# mysql:
+#   :adapter: mysql
+#   :host: localhost
+#   :username: rails
+#   :password: rails
+#   :database: force_columns_test
+
diff --git a/spec/db/schema.rb b/spec/db/schema.rb
new file mode 100644 (file)
index 0000000..90d4f82
--- /dev/null
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+ActiveRecord::Schema.define(:version => 0) do
+  create_table :domains, :force => true do |t|
+    t.string  :name
+  end
+end
+
diff --git a/spec/force_columns_spec.rb b/spec/force_columns_spec.rb
new file mode 100644 (file)
index 0000000..0870770
--- /dev/null
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+
+require File.join(File.dirname(__FILE__), 'spec_helper')
+
+ForceColumns::ForceDomain.set_domain_model_name('Domain')
+
+class CreateSamples < ActiveRecord::Migration
+  def self.up
+    create_table :samples do |t|
+      t.string  :name
+    end
+  end
+  def self.down
+    drop_table :samples
+  end
+end
+
+class CreateNotes < ActiveRecord::Migration
+  def self.up
+    create_table :notes, {
+      :domain_id     => false,
+      :created_at    => false,
+      :updated_at    => false,
+      :created_by_id => false,
+      :updated_by_id => false,
+      :created_in_id => false,
+      :updated_in_id => false,
+      :lock_version  => false,
+    } do |t|
+      t.string :name
+    end
+  end
+  def self.down
+    drop_table :notes
+  end
+end
+
+describe ForceColumns do
+  describe 'force_columns' do
+    before do
+      swap_stdout{ CreateSamples.up }
+      class Sample < ActiveRecord::Base
+      end
+    end
+    after do
+      swap_stdout{ CreateSamples.down }
+    end
+    [
+     'domain_id',
+     'created_at',
+     'updated_at',
+     'created_by_id',
+     'updated_by_id',
+     'created_in_id',
+     'updated_in_id',
+     'lock_version',
+    ].each do |column_name|
+      it "have a column named #{column_name}" do
+        Sample.column_names.should be_include(column_name)
+      end
+    end
+  end
+  describe 'no force_columns' do
+    before do
+      swap_stdout{ CreateNotes.up }
+      class Note < ActiveRecord::Base
+      end
+    end
+    after do
+      swap_stdout{ CreateNotes.down }
+    end
+    [
+     'domain_id',
+     'created_at',
+     'updated_at',
+     'created_by_id',
+     'updated_by_id',
+     'created_in_id',
+     'updated_in_id',
+     'lock_version',
+    ].each do |column_name|
+      it "do not have a column named #{column_name}" do
+        Note.column_names.should_not be_include(column_name)
+      end
+    end
+  end
+end
+
+describe ForceColumns::ForceDomain do
+  describe 'Sample' do
+    before :all do
+      swap_stdout{ CreateSamples.up }
+      class Domain < ActiveRecord::Base
+        def self.current_id
+          @current_id
+        end
+        def self.current_id=(id)
+          @current_id = id
+        end
+      end
+      class Sample < ActiveRecord::Base
+      end
+    end
+    before do
+      %w[a b c].each do |name|
+        Domain.create(:name => name)
+      end
+      ('a'..'z').each_with_index do |c, index|
+        Sample.create(:name => c, :domain_id => index % 3 + 1)
+      end
+    end
+    after :all do
+      swap_stdout{ CreateSamples.down }
+    end
+    after do
+      Sample.delete_all
+      Domain.delete_all
+    end
+    describe "Sample" do
+      it "belongs to :domain" do
+        Sample.instance_methods.should be_include('domain')
+      end
+    end
+    describe "current domain is a" do
+      before do
+        Domain.current_id = 1
+      end
+      it "find 9 records" do
+        Sample.filter_by_current_domain.should have(9).founds
+      end
+    end
+    describe "create Sample" do
+      before do
+        Domain.current_id = 1
+        @sample = Sample.create(:name => 'aa')
+      end
+      it "belongs_to domain 1" do
+        @sample.reload
+        @sample.domain_id.should == 1
+      end
+    end
+    describe "update Sample" do
+      before do
+        Domain.current_id = 1
+        @sample = Sample.create(:name => 'aa')
+        @sample.reload
+        @sample.name = 'bb'
+        @sample.save
+      end
+      it "name == 'bb'" do
+        @sample.reload
+        @sample.name.should == 'bb'
+      end
+      it "belongs_to domain 1" do
+        @sample.reload
+        @sample.domain_id.should == 1
+      end
+    end
+    describe "update Sample, change domain" do
+      before do
+        Domain.current_id = 1
+        @sample = Sample.create(:name => 'aa')
+        @sample.reload
+        @sample.domain_id = 2
+        @sample.name = 'bb'
+        @sample.save
+      end
+      it "name == 'bb'" do
+        @sample.reload
+        @sample.name.should == 'bb'
+      end
+      it "belongs_to domain 2" do
+        @sample.reload
+        @sample.domain_id.should == 2
+      end
+    end
+    describe "when call set_table_name" do
+      it "do not raise Error" do
+        class DummySample < ActiveRecord::Base
+          set_table_name 'samples'
+        end
+      end
+    end
+    describe "when call set_table_name with force_domain" do
+      before :all do
+        class DummySample < ActiveRecord::Base
+          set_table_name 'samples'
+          force_domain
+        end
+      end
+      before do
+        Domain.current_id = 1
+        @sample = DummySample.create(:name => 'aa')
+      end
+      after do
+        DummySample.delete_all
+      end
+      it "belongs_to domain 1" do
+        @sample.reload
+        @sample.domain_id.should == 1
+      end
+      describe "current domain is a" do
+        before do
+          Domain.current_id = 2
+        end
+        it "find 9 records" do
+          DummySample.filter_by_current_domain.should have(9).founds
+        end
+      end
+    end
+  end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644 (file)
index 0000000..ca71ef7
--- /dev/null
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+plugin_spec_dir = File.dirname(__FILE__)
+
+$:.unshift File.join(plugin_spec_dir, '..', 'lib')
+
+require 'rubygems'
+require 'activerecord'
+require 'spec'
+
+ActiveRecord::Base.logger = Logger.new(plugin_spec_dir + "/debug.log")
+
+database = YAML.load(File.read(plugin_spec_dir + '/db/database.yml'))
+ActiveRecord::Base.establish_connection(database[ENV['DB'] || 'sqlite3'])
+load File.join(plugin_spec_dir, 'db', 'schema.rb')
+
+require File.join(plugin_spec_dir, '..', 'rails', 'init')
+
+def swap_stdout(verbose = false)
+  $stdout = StringIO.new
+  yield
+  $stdout = STDOUT
+end
diff --git a/tasks/force_columns_tasks.rake b/tasks/force_columns_tasks.rake
new file mode 100644 (file)
index 0000000..350a360
--- /dev/null
@@ -0,0 +1,4 @@
+# desc "Explaining what the task does"
+# task :force_columns do
+#   # Task goes here
+# end
\ No newline at end of file
diff --git a/test/force_columns_test.rb b/test/force_columns_test.rb
new file mode 100644 (file)
index 0000000..303222d
--- /dev/null
@@ -0,0 +1,8 @@
+require 'test/unit'
+
+class ForceColumnsTest < Test::Unit::TestCase
+  # Replace this with your real tests.
+  def test_this_plugin
+    flunk
+  end
+end