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.
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