declare_schema 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.dependabot/config.yml +10 -0
- data/.github/workflows/gem_release.yml +38 -0
- data/.gitignore +14 -0
- data/.jenkins/Jenkinsfile +72 -0
- data/.jenkins/ruby_build_pod.yml +19 -0
- data/.rspec +2 -0
- data/.rubocop.yml +189 -0
- data/.ruby-version +1 -0
- data/Appraisals +14 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +203 -0
- data/LICENSE.txt +22 -0
- data/README.md +11 -0
- data/Rakefile +56 -0
- data/bin/declare_schema +11 -0
- data/declare_schema.gemspec +25 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_4.gemfile +25 -0
- data/gemfiles/rails_5.gemfile +25 -0
- data/gemfiles/rails_6.gemfile +25 -0
- data/lib/declare_schema.rb +44 -0
- data/lib/declare_schema/command.rb +65 -0
- data/lib/declare_schema/extensions/active_record/fields_declaration.rb +28 -0
- data/lib/declare_schema/extensions/module.rb +36 -0
- data/lib/declare_schema/field_declaration_dsl.rb +40 -0
- data/lib/declare_schema/model.rb +242 -0
- data/lib/declare_schema/model/field_spec.rb +162 -0
- data/lib/declare_schema/model/index_spec.rb +175 -0
- data/lib/declare_schema/railtie.rb +12 -0
- data/lib/declare_schema/version.rb +5 -0
- data/lib/generators/declare_schema/migration/USAGE +47 -0
- data/lib/generators/declare_schema/migration/migration_generator.rb +184 -0
- data/lib/generators/declare_schema/migration/migrator.rb +567 -0
- data/lib/generators/declare_schema/migration/templates/migration.rb.erb +9 -0
- data/lib/generators/declare_schema/model/USAGE +19 -0
- data/lib/generators/declare_schema/model/model_generator.rb +12 -0
- data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +25 -0
- data/lib/generators/declare_schema/support/eval_template.rb +21 -0
- data/lib/generators/declare_schema/support/model.rb +64 -0
- data/lib/generators/declare_schema/support/thor_shell.rb +39 -0
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +28 -0
- data/spec/spec_helper.rb +28 -0
- data/test/api.rdoctest +136 -0
- data/test/doc-only.rdoctest +76 -0
- data/test/generators.rdoctest +60 -0
- data/test/interactive_primary_key.rdoctest +56 -0
- data/test/migration_generator.rdoctest +846 -0
- data/test/migration_generator_comments.rdoctestDISABLED +74 -0
- data/test/prepare_testapp.rb +15 -0
- data/test_responses.txt +2 -0
- metadata +109 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2008 Tom Locke
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# DeclareSchema
|
2
|
+
|
3
|
+
Declare your active_record model schemas and have database migrations generated for you!
|
4
|
+
|
5
|
+
## Testing
|
6
|
+
To run tests:
|
7
|
+
```
|
8
|
+
rake test:prepare_testapp[force]
|
9
|
+
rake test:all < test_responses.txt
|
10
|
+
```
|
11
|
+
(Note: there currently are no unit tests. The above will run the `rdoctests`.)
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "bundler/gem_tasks"
|
5
|
+
require "rspec/core/rake_task"
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'tmpdir'
|
11
|
+
require 'pry'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift File.expand_path('lib', __dir__)
|
14
|
+
require 'declare_schema'
|
15
|
+
|
16
|
+
RUBY = 'ruby'
|
17
|
+
RUBYDOCTEST = ENV['RUBYDOCTEST'] || "#{RUBY} -S rubydoctest"
|
18
|
+
GEM_ROOT = __dir__
|
19
|
+
TESTAPP_PATH = ENV['TESTAPP_PATH'] || File.join(Dir.tmpdir, 'declare_schema_testapp')
|
20
|
+
BIN = File.expand_path('bin/declare_schema', __dir__)
|
21
|
+
|
22
|
+
task default: 'test:all'
|
23
|
+
|
24
|
+
include Rake::DSL
|
25
|
+
|
26
|
+
namespace "test" do
|
27
|
+
task all: [:doctest, :spec]
|
28
|
+
|
29
|
+
desc "Run the doctests"
|
30
|
+
task :doctest do |_t|
|
31
|
+
files = Dir['test/*.rdoctest'].sort.map { |f| File.expand_path(f) }.join(' ')
|
32
|
+
system("#{RUBYDOCTEST} #{files}") or exit(1)
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Prepare a rails application for testing"
|
36
|
+
task :prepare_testapp, :force do |_t, args|
|
37
|
+
if args.force || !File.directory?(TESTAPP_PATH)
|
38
|
+
FileUtils.remove_entry_secure(TESTAPP_PATH, true)
|
39
|
+
sh %(#{BIN} new #{TESTAPP_PATH} --skip-wizard --skip-bundle)
|
40
|
+
FileUtils.chdir TESTAPP_PATH
|
41
|
+
sh %(bundle install)
|
42
|
+
sh %(echo "" >> Gemfile)
|
43
|
+
sh %(echo "gem 'irt', :group => :development" >> Gemfile) # to make the bundler happy
|
44
|
+
sh %(echo "gem 'therubyracer'" >> Gemfile)
|
45
|
+
sh %(echo "gem 'kramdown'" >> Gemfile)
|
46
|
+
sh %(echo "" > app/models/.gitignore) # because git reset --hard would rm the dir
|
47
|
+
rm %(.gitignore) # we need to reset everything in a testapp
|
48
|
+
sh %(git init && git add . && git commit -m "initial commit")
|
49
|
+
puts %(The testapp has been created in '#{TESTAPP_PATH}')
|
50
|
+
else
|
51
|
+
FileUtils.chdir TESTAPP_PATH
|
52
|
+
sh %(git add .)
|
53
|
+
sh %(git reset --hard -q HEAD)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/bin/declare_schema
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
require 'rubygems'
|
6
|
+
require_relative '../lib/declare_schema/command'
|
7
|
+
require_relative '../lib/declare_schema/version'
|
8
|
+
|
9
|
+
ARGV.freeze
|
10
|
+
|
11
|
+
DeclareSchema::Command.run(:DeclareSchema, ARGV.dup, DeclareSchema::VERSION)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/declare_schema/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.authors = ['Invoca Development adapted from hobo_fields by Tom Locke']
|
7
|
+
s.email = 'development@invoca.com'
|
8
|
+
s.homepage = 'https://github.com/Invoca/declare_schema'
|
9
|
+
s.summary = 'Database migration generator for Rails'
|
10
|
+
s.description = 'Declare your Rails/active_record model schemas and have database migrations generated for you!'
|
11
|
+
s.name = "declare_schema"
|
12
|
+
s.version = DeclareSchema::VERSION
|
13
|
+
|
14
|
+
s.metadata = {
|
15
|
+
"allowed_push_host" => "https://rubygems.org"
|
16
|
+
}
|
17
|
+
|
18
|
+
s.executables = ["declare_schema"]
|
19
|
+
s.files = `git ls-files -x declare_schema/* -z`.split("\0")
|
20
|
+
|
21
|
+
s.required_rubygems_version = ">= 1.3.6"
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
|
24
|
+
s.add_dependency 'rails', '>= 4.2'
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file was generated by Appraisal
|
4
|
+
|
5
|
+
source "https://rubygems.org"
|
6
|
+
source "https://gem.fury.io/invoca"
|
7
|
+
|
8
|
+
gem "appraisal"
|
9
|
+
gem "pry"
|
10
|
+
gem "pry-byebug"
|
11
|
+
gem "rails", "~> 4.2"
|
12
|
+
gem "responders"
|
13
|
+
gem "rubydoctest"
|
14
|
+
gem "sqlite3", "~> 1.3.0"
|
15
|
+
gem "test_overrides"
|
16
|
+
gem "yard"
|
17
|
+
|
18
|
+
group :testapp do
|
19
|
+
gem "bootsnap", ">= 1.1.0", require: false
|
20
|
+
gem "kramdown"
|
21
|
+
gem "listen"
|
22
|
+
gem "RedCloth"
|
23
|
+
end
|
24
|
+
|
25
|
+
gemspec path: "../"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file was generated by Appraisal
|
4
|
+
|
5
|
+
source "https://rubygems.org"
|
6
|
+
source "https://gem.fury.io/invoca"
|
7
|
+
|
8
|
+
gem "appraisal"
|
9
|
+
gem "pry"
|
10
|
+
gem "pry-byebug"
|
11
|
+
gem "rails", "~> 5.2"
|
12
|
+
gem "responders"
|
13
|
+
gem "rubydoctest"
|
14
|
+
gem "sqlite3"
|
15
|
+
gem "test_overrides"
|
16
|
+
gem "yard"
|
17
|
+
|
18
|
+
group :testapp do
|
19
|
+
gem "bootsnap", ">= 1.1.0", require: false
|
20
|
+
gem "kramdown"
|
21
|
+
gem "listen"
|
22
|
+
gem "RedCloth"
|
23
|
+
end
|
24
|
+
|
25
|
+
gemspec path: "../"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file was generated by Appraisal
|
4
|
+
|
5
|
+
source "https://rubygems.org"
|
6
|
+
source "https://gem.fury.io/invoca"
|
7
|
+
|
8
|
+
gem "appraisal"
|
9
|
+
gem "pry"
|
10
|
+
gem "pry-byebug"
|
11
|
+
gem "rails", "~> 6.0"
|
12
|
+
gem "responders"
|
13
|
+
gem "rubydoctest"
|
14
|
+
gem "sqlite3"
|
15
|
+
gem "test_overrides"
|
16
|
+
gem "yard"
|
17
|
+
|
18
|
+
group :testapp do
|
19
|
+
gem "bootsnap", ">= 1.1.0", require: false
|
20
|
+
gem "kramdown"
|
21
|
+
gem "listen"
|
22
|
+
gem "RedCloth"
|
23
|
+
end
|
24
|
+
|
25
|
+
gemspec path: "../"
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/all'
|
5
|
+
require_relative 'declare_schema/version'
|
6
|
+
|
7
|
+
ActiveSupport::Dependencies.autoload_paths |= [__dir__]
|
8
|
+
|
9
|
+
module DeclareSchema
|
10
|
+
class Boolean; end
|
11
|
+
|
12
|
+
PLAIN_TYPES = {
|
13
|
+
boolean: Boolean,
|
14
|
+
date: Date,
|
15
|
+
datetime: ActiveSupport::TimeWithZone,
|
16
|
+
time: Time,
|
17
|
+
integer: Integer,
|
18
|
+
decimal: BigDecimal,
|
19
|
+
float: Float,
|
20
|
+
string: String,
|
21
|
+
text: String
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def to_class(type)
|
26
|
+
case type
|
27
|
+
when Class
|
28
|
+
type
|
29
|
+
when Symbol, String
|
30
|
+
PLAIN_TYPES[type.to_sym]
|
31
|
+
else
|
32
|
+
raise ArgumentError, "expected Class or Symbol or String: got #{type.inspect}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
require 'declare_schema/extensions/active_record/fields_declaration'
|
39
|
+
require 'declare_schema/field_declaration_dsl'
|
40
|
+
require 'declare_schema/model'
|
41
|
+
require 'declare_schema/model/field_spec'
|
42
|
+
require 'declare_schema/model/index_spec'
|
43
|
+
|
44
|
+
require 'declare_schema/railtie' if defined?(Rails)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'tmpdir'
|
5
|
+
require 'rubygems'
|
6
|
+
|
7
|
+
module DeclareSchema
|
8
|
+
module Command
|
9
|
+
BANNER = <<~EOS
|
10
|
+
Usage:
|
11
|
+
declare_schema new <app_name> [rails_opt] Creates a new declare_schema Application
|
12
|
+
declare_schema generate|g <generator> [ARGS] [options] Fires the declare_schema:<generator>
|
13
|
+
declare_schema destroy <generator> [ARGS] [options] Tries to undo generated code
|
14
|
+
declare_schema --help|-h This help screen
|
15
|
+
|
16
|
+
EOS
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def run(gem, args, version)
|
20
|
+
command = args.shift
|
21
|
+
|
22
|
+
case command
|
23
|
+
|
24
|
+
when nil
|
25
|
+
puts "\nThe command is missing!\n\n"
|
26
|
+
puts BANNER
|
27
|
+
exit(1)
|
28
|
+
|
29
|
+
when /^--help|-h$/
|
30
|
+
puts BANNER
|
31
|
+
exit
|
32
|
+
|
33
|
+
when 'new'
|
34
|
+
app_name = args.shift or begin
|
35
|
+
puts "\nThe application name is missing!\n\n"
|
36
|
+
puts BANNER
|
37
|
+
exit(1)
|
38
|
+
end
|
39
|
+
template_path = File.join(Dir.tmpdir, "declare_schema_app_template")
|
40
|
+
File.open(template_path, 'w') do |file|
|
41
|
+
file.puts "gem '#{gem}', '>= #{version}'"
|
42
|
+
end
|
43
|
+
puts "Generating Rails infrastructure..."
|
44
|
+
system("rails new #{app_name} #{args * ' '} -m #{template_path}")
|
45
|
+
File.delete(template_path)
|
46
|
+
|
47
|
+
when /^(g|generate|destroy)$/
|
48
|
+
cmd = Regexp.last_match(1)
|
49
|
+
if args.empty?
|
50
|
+
puts "\nThe generator name is missing!\n\n"
|
51
|
+
puts BANNER
|
52
|
+
exit(1)
|
53
|
+
else
|
54
|
+
system("bundle exec rails #{cmd} declare_schema:#{args * ' '}")
|
55
|
+
end
|
56
|
+
|
57
|
+
else
|
58
|
+
puts "\n => '#{command}' is an unknown command!\n\n"
|
59
|
+
puts BANNER
|
60
|
+
exit(1)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'declare_schema/model'
|
5
|
+
require 'declare_schema/field_declaration_dsl'
|
6
|
+
|
7
|
+
module DeclareSchema
|
8
|
+
module FieldsDsl
|
9
|
+
def fields(&block)
|
10
|
+
# Any model that calls 'fields' gets DeclareSchema::Model behavior
|
11
|
+
DeclareSchema::Model.mix_in(self)
|
12
|
+
|
13
|
+
# @include_in_migration = false #||= options.fetch(:include_in_migration, true); options.delete(:include_in_migration)
|
14
|
+
@include_in_migration = true
|
15
|
+
|
16
|
+
if block
|
17
|
+
dsl = DeclareSchema::FieldDeclarationDsl.new(self, null: false)
|
18
|
+
if block.arity == 1
|
19
|
+
yield dsl
|
20
|
+
else
|
21
|
+
dsl.instance_eval(&block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
ActiveRecord::Base.singleton_class.prepend DeclareSchema::FieldsDsl
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Module
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
# Creates a class attribute reader that will delegate to the superclass
|
8
|
+
# if not defined on self. Default values can be a Proc object that takes the class as a parameter.
|
9
|
+
def inheriting_cattr_reader(*names)
|
10
|
+
receiver =
|
11
|
+
if self.class == Module
|
12
|
+
self
|
13
|
+
else
|
14
|
+
singleton_class
|
15
|
+
end
|
16
|
+
|
17
|
+
names_with_defaults = (names.pop if names.last.is_a?(Hash)) || {}
|
18
|
+
|
19
|
+
(names + names_with_defaults.keys).each do |name|
|
20
|
+
ivar_name = "@#{name}"
|
21
|
+
block = names_with_defaults[name]
|
22
|
+
|
23
|
+
receiver.send(:define_method, name) do
|
24
|
+
if instance_variable_defined? ivar_name
|
25
|
+
instance_variable_get(ivar_name)
|
26
|
+
else
|
27
|
+
superclass.respond_to?(name) && superclass.send(name) ||
|
28
|
+
block && begin
|
29
|
+
result = block.is_a?(Proc) ? block.call(self) : block
|
30
|
+
instance_variable_set(ivar_name, result) if result
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/proxy_object'
|
4
|
+
|
5
|
+
module DeclareSchema
|
6
|
+
class FieldDeclarationDsl < BasicObject # avoid Object because that gets extended by lots of gems
|
7
|
+
include ::Kernel # but we need the basic class methods
|
8
|
+
|
9
|
+
instance_methods.each do |m|
|
10
|
+
unless m.to_s.starts_with?('__') || m.in?([:object_id, :instance_eval])
|
11
|
+
undef_method(m)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(model, options = {})
|
16
|
+
@model = model
|
17
|
+
@options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :model
|
21
|
+
|
22
|
+
def timestamps
|
23
|
+
field(:created_at, :datetime, null: true)
|
24
|
+
field(:updated_at, :datetime, null: true)
|
25
|
+
end
|
26
|
+
|
27
|
+
def optimistic_lock
|
28
|
+
field(:lock_version, :integer, default: 1, null: false)
|
29
|
+
end
|
30
|
+
|
31
|
+
def field(name, type, *args)
|
32
|
+
options = args.extract_options!
|
33
|
+
@model.declare_field(name, type, *(args + [@options.merge(options)]))
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(name, *args)
|
37
|
+
field(name, args.first, *args[1..-1])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'declare_schema/extensions/module'
|
4
|
+
|
5
|
+
module DeclareSchema
|
6
|
+
module Model
|
7
|
+
class << self
|
8
|
+
def mix_in(base)
|
9
|
+
base.singleton_class.prepend ClassMethods unless base.singleton_class < ClassMethods # don't mix in if a base class already did it
|
10
|
+
|
11
|
+
base.class_eval do
|
12
|
+
# ignore the model in the migration until somebody sets
|
13
|
+
# @include_in_migration via the fields declaration
|
14
|
+
inheriting_cattr_reader include_in_migration: false
|
15
|
+
|
16
|
+
# attr_types holds the type class for any attribute reader (i.e. getter
|
17
|
+
# method) that returns rich-types
|
18
|
+
inheriting_cattr_reader attr_types: HashWithIndifferentAccess.new
|
19
|
+
inheriting_cattr_reader attr_order: []
|
20
|
+
|
21
|
+
# field_specs holds FieldSpec objects for every declared
|
22
|
+
# field. Note that attribute readers are created (by ActiveRecord)
|
23
|
+
# for all fields, so there is also an entry for the field in
|
24
|
+
# attr_types. This is redundant but simplifies the implementation
|
25
|
+
# and speeds things up a little.
|
26
|
+
inheriting_cattr_reader field_specs: HashWithIndifferentAccess.new
|
27
|
+
|
28
|
+
# index_specs holds IndexSpec objects for all the declared indexes.
|
29
|
+
inheriting_cattr_reader index_specs: []
|
30
|
+
inheriting_cattr_reader ignore_indexes: []
|
31
|
+
inheriting_cattr_reader constraint_specs: []
|
32
|
+
|
33
|
+
# eval avoids the ruby 1.9.2 "super from singleton method ..." error
|
34
|
+
|
35
|
+
eval %(
|
36
|
+
def self.inherited(klass)
|
37
|
+
unless klass.field_specs.has_key?(inheritance_column)
|
38
|
+
fields do |f|
|
39
|
+
f.field(inheritance_column, :string, limit: 255, null: true)
|
40
|
+
end
|
41
|
+
index(inheritance_column)
|
42
|
+
end
|
43
|
+
super
|
44
|
+
end
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
module ClassMethods
|
51
|
+
def index(fields, options = {})
|
52
|
+
# don't double-index fields
|
53
|
+
index_fields_s = Array.wrap(fields).map(&:to_s)
|
54
|
+
unless index_specs.any? { |index_spec| index_spec.fields == index_fields_s }
|
55
|
+
index_specs << ::DeclareSchema::Model::IndexSpec.new(self, fields, options)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def primary_key_index(*fields)
|
60
|
+
index(fields.flatten, unique: true, name: "PRIMARY_KEY")
|
61
|
+
end
|
62
|
+
|
63
|
+
def constraint(fkey, options = {})
|
64
|
+
fkey_s = fkey.to_s
|
65
|
+
unless constraint_specs.any? { |constraint_spec| constraint_spec.foreign_key == fkey_s }
|
66
|
+
constraint_specs << DeclareSchema::Model::ForeignKeySpec.new(self, fkey, options)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# tell the migration generator to ignore the named index. Useful for existing indexes, or for indexes
|
71
|
+
# that can't be automatically generated (for example: an prefix index in MySQL)
|
72
|
+
def ignore_index(index_name)
|
73
|
+
ignore_indexes << index_name.to_s
|
74
|
+
end
|
75
|
+
|
76
|
+
# Declare named field with a type and an arbitrary set of
|
77
|
+
# arguments. The arguments are forwarded to the #field_added
|
78
|
+
# callback, allowing custom metadata to be added to field
|
79
|
+
# declarations.
|
80
|
+
def declare_field(name, type, *args)
|
81
|
+
options = args.extract_options!
|
82
|
+
field_added(name, type, args, options) if respond_to?(:field_added)
|
83
|
+
add_formatting_for_field(name, type, args)
|
84
|
+
add_validations_for_field(name, type, args, options)
|
85
|
+
add_index_for_field(name, args, options)
|
86
|
+
field_specs[name] = ::DeclareSchema::Model::FieldSpec.new(self, name, type, options)
|
87
|
+
attr_order << name unless name.in?(attr_order)
|
88
|
+
end
|
89
|
+
|
90
|
+
def index_specs_with_primary_key
|
91
|
+
if index_specs.any?(&:primary_key?)
|
92
|
+
index_specs
|
93
|
+
else
|
94
|
+
index_specs + [rails_default_primary_key]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def primary_key
|
99
|
+
super || 'id'
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def rails_default_primary_key
|
105
|
+
::DeclareSchema::Model::IndexSpec.new(self, [primary_key.to_sym], unique: true, name: DeclareSchema::Model::IndexSpec::PRIMARY_KEY_NAME)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Extend belongs_to so that it creates a FieldSpec for the foreign key
|
109
|
+
def belongs_to(name, *args, &block)
|
110
|
+
if args.size == 0 || (args.size == 1 && args[0].is_a?(Proc))
|
111
|
+
options = {}
|
112
|
+
args.push(options)
|
113
|
+
elsif args.size == 1
|
114
|
+
options = args[0]
|
115
|
+
else
|
116
|
+
options = args[1]
|
117
|
+
end
|
118
|
+
column_options = {}
|
119
|
+
column_options[:null] = options.delete(:null) || false
|
120
|
+
column_options[:comment] = options.delete(:comment) if options.has_key?(:comment)
|
121
|
+
column_options[:default] = options.delete(:default) if options.has_key?(:default)
|
122
|
+
column_options[:limit] = options.delete(:limit) if options.has_key?(:limit)
|
123
|
+
|
124
|
+
index_options = {}
|
125
|
+
index_options[:name] = options.delete(:index) if options.has_key?(:index)
|
126
|
+
index_options[:unique] = options.delete(:unique) if options.has_key?(:unique)
|
127
|
+
index_options[:allow_equivalent] = options.delete(:allow_equivalent) if options.has_key?(:allow_equivalent)
|
128
|
+
|
129
|
+
fk_options = options.dup
|
130
|
+
fk_options[:constraint_name] = options.delete(:constraint) if options.has_key?(:constraint)
|
131
|
+
fk_options[:index_name] = index_options[:name]
|
132
|
+
|
133
|
+
fk_options[:dependent] = options.delete(:far_end_dependent) if options.has_key?(:far_end_dependent)
|
134
|
+
super(name, *args, &block).tap do |_bt|
|
135
|
+
refl = reflections[name.to_s] or raise "Couldn't find reflection #{name} in #{reflections.keys}"
|
136
|
+
fkey = refl.foreign_key
|
137
|
+
declare_field(fkey.to_sym, :integer, column_options)
|
138
|
+
if refl.options[:polymorphic]
|
139
|
+
foreign_type = options[:foreign_type] || "#{name}_type"
|
140
|
+
declare_polymorphic_type_field(foreign_type, column_options)
|
141
|
+
index([foreign_type, fkey], index_options) if index_options[:name] != false
|
142
|
+
else
|
143
|
+
index(fkey, index_options) if index_options[:name] != false
|
144
|
+
options[:constraint_name] = options
|
145
|
+
constraint(fkey, fk_options) if fk_options[:constraint_name] != false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Declares the "foo_type" field that accompanies the "foo_id"
|
151
|
+
# field for a polymorphic belongs_to
|
152
|
+
def declare_polymorphic_type_field(foreign_type, column_options)
|
153
|
+
declare_field(foreign_type, :string, column_options.merge(limit: 255))
|
154
|
+
# FIXME: Before declare_schema was extracted, this used to now do:
|
155
|
+
# never_show(type_col)
|
156
|
+
# That needs doing somewhere
|
157
|
+
end
|
158
|
+
|
159
|
+
# Declare a rich-type for any attribute (i.e. getter method). This
|
160
|
+
# does not effect the attribute in any way - it just records the
|
161
|
+
# metadata.
|
162
|
+
def declare_attr_type(name, type, options = {})
|
163
|
+
klass = DeclareSchema.to_class(type)
|
164
|
+
attr_types[name] = DeclareSchema.to_class(type)
|
165
|
+
klass.declared(self, name, options) if klass.respond_to?(:declared)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Add field validations according to arguments in the
|
169
|
+
# field declaration
|
170
|
+
def add_validations_for_field(name, type, args, options)
|
171
|
+
validates_presence_of name if :required.in?(args)
|
172
|
+
validates_uniqueness_of name, allow_nil: !:required.in?(args) if :unique.in?(args)
|
173
|
+
|
174
|
+
if (validates_options = options[:validates])
|
175
|
+
validates name, validates_options
|
176
|
+
end
|
177
|
+
|
178
|
+
# Support for custom validations
|
179
|
+
if (type_class = DeclareSchema.to_class(type))
|
180
|
+
if type_class.public_method_defined?("validate")
|
181
|
+
validate do |record|
|
182
|
+
v = record.send(name)&.validate
|
183
|
+
record.errors.add(name, v) if v.is_a?(String)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_formatting_for_field(name, type, _args)
|
190
|
+
if (type_class = DeclareSchema.to_class(type))
|
191
|
+
if "format".in?(type_class.instance_methods)
|
192
|
+
before_validation do |record|
|
193
|
+
record.send("#{name}=", record.send(name)&.format)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def add_index_for_field(name, args, options)
|
200
|
+
if (to_name = options.delete(:index))
|
201
|
+
index_opts =
|
202
|
+
{
|
203
|
+
unique: args.include?(:unique) || options.delete(:unique)
|
204
|
+
}
|
205
|
+
# support index: true declaration
|
206
|
+
index_opts[:name] = to_name unless to_name == true
|
207
|
+
index(name, index_opts)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Returns the type (a class) for a given field or association. If
|
212
|
+
# the association is a collection (has_many or habtm) return the
|
213
|
+
# AssociationReflection instead
|
214
|
+
public \
|
215
|
+
def attr_type(name)
|
216
|
+
if attr_types.nil? && self != self.name.constantize
|
217
|
+
raise "attr_types called on a stale class object (#{self.name}). Avoid storing persistent references to classes"
|
218
|
+
end
|
219
|
+
|
220
|
+
attr_types[name] ||
|
221
|
+
if (refl = reflections[name.to_s])
|
222
|
+
if refl.macro.in?([:has_one, :belongs_to]) && !refl.options[:polymorphic]
|
223
|
+
refl.klass
|
224
|
+
else
|
225
|
+
refl
|
226
|
+
end
|
227
|
+
end ||
|
228
|
+
if (col = column(name.to_s))
|
229
|
+
DeclareSchema::PLAIN_TYPES[col.type] || col.klass
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Return the entry from #columns for the named column
|
234
|
+
def column(name)
|
235
|
+
defined?(@table_exists) or @table_exists = table_exists?
|
236
|
+
if @table_exists
|
237
|
+
columns_hash[name.to_s]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|