declare_schema 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.dependabot/config.yml +10 -0
  3. data/.github/workflows/gem_release.yml +38 -0
  4. data/.gitignore +14 -0
  5. data/.jenkins/Jenkinsfile +72 -0
  6. data/.jenkins/ruby_build_pod.yml +19 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +189 -0
  9. data/.ruby-version +1 -0
  10. data/Appraisals +14 -0
  11. data/CHANGELOG.md +11 -0
  12. data/Gemfile +24 -0
  13. data/Gemfile.lock +203 -0
  14. data/LICENSE.txt +22 -0
  15. data/README.md +11 -0
  16. data/Rakefile +56 -0
  17. data/bin/declare_schema +11 -0
  18. data/declare_schema.gemspec +25 -0
  19. data/gemfiles/.bundle/config +2 -0
  20. data/gemfiles/rails_4.gemfile +25 -0
  21. data/gemfiles/rails_5.gemfile +25 -0
  22. data/gemfiles/rails_6.gemfile +25 -0
  23. data/lib/declare_schema.rb +44 -0
  24. data/lib/declare_schema/command.rb +65 -0
  25. data/lib/declare_schema/extensions/active_record/fields_declaration.rb +28 -0
  26. data/lib/declare_schema/extensions/module.rb +36 -0
  27. data/lib/declare_schema/field_declaration_dsl.rb +40 -0
  28. data/lib/declare_schema/model.rb +242 -0
  29. data/lib/declare_schema/model/field_spec.rb +162 -0
  30. data/lib/declare_schema/model/index_spec.rb +175 -0
  31. data/lib/declare_schema/railtie.rb +12 -0
  32. data/lib/declare_schema/version.rb +5 -0
  33. data/lib/generators/declare_schema/migration/USAGE +47 -0
  34. data/lib/generators/declare_schema/migration/migration_generator.rb +184 -0
  35. data/lib/generators/declare_schema/migration/migrator.rb +567 -0
  36. data/lib/generators/declare_schema/migration/templates/migration.rb.erb +9 -0
  37. data/lib/generators/declare_schema/model/USAGE +19 -0
  38. data/lib/generators/declare_schema/model/model_generator.rb +12 -0
  39. data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +25 -0
  40. data/lib/generators/declare_schema/support/eval_template.rb +21 -0
  41. data/lib/generators/declare_schema/support/model.rb +64 -0
  42. data/lib/generators/declare_schema/support/thor_shell.rb +39 -0
  43. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +28 -0
  44. data/spec/spec_helper.rb +28 -0
  45. data/test/api.rdoctest +136 -0
  46. data/test/doc-only.rdoctest +76 -0
  47. data/test/generators.rdoctest +60 -0
  48. data/test/interactive_primary_key.rdoctest +56 -0
  49. data/test/migration_generator.rdoctest +846 -0
  50. data/test/migration_generator_comments.rdoctestDISABLED +74 -0
  51. data/test/prepare_testapp.rb +15 -0
  52. data/test_responses.txt +2 -0
  53. metadata +109 -0
@@ -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.
@@ -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`.)
@@ -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
@@ -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,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -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