-
Notifications
You must be signed in to change notification settings - Fork 21.4k
/
generated_attribute.rb
244 lines (203 loc) · 6.93 KB
/
generated_attribute.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# frozen_string_literal: true
require "active_support/time"
module Rails
module Generators
class GeneratedAttribute # :nodoc:
INDEX_OPTIONS = %w(index uniq)
UNIQ_INDEX_OPTIONS = %w(uniq)
DEFAULT_TYPES = %w(
attachment
attachments
belongs_to
boolean
date
datetime
decimal
digest
float
integer
references
rich_text
string
text
time
timestamp
token
)
attr_accessor :name, :type
attr_reader :attr_options
attr_writer :index_name
class << self
def parse(column_definition)
name, type, index_type = column_definition.split(":")
# if user provided "name:index" instead of "name:string:index"
# type should be set blank so GeneratedAttribute's constructor
# could set it to :string
index_type, type = type, nil if valid_index_type?(type)
type, attr_options = *parse_type_and_options(type)
type = type.to_sym if type
if dangerous_name?(name)
raise Error, "Could not generate field '#{name}', as it is already defined by Active Record."
end
if type && !valid_type?(type)
raise Error, "Could not generate field '#{name}' with unknown type '#{type}'."
end
if index_type && !valid_index_type?(index_type)
raise Error, "Could not generate field '#{name}' with unknown index '#{index_type}'."
end
if type && reference?(type)
if UNIQ_INDEX_OPTIONS.include?(index_type)
attr_options[:index] = { unique: true }
end
end
new(name, type, index_type, attr_options)
end
def dangerous_name?(name)
defined?(ActiveRecord::Base) &&
ActiveRecord::Base.dangerous_attribute_method?(name)
end
def valid_type?(type)
DEFAULT_TYPES.include?(type.to_s) ||
!defined?(ActiveRecord::Base) ||
ActiveRecord::Base.connection.valid_type?(type)
end
def valid_index_type?(index_type)
INDEX_OPTIONS.include?(index_type.to_s)
end
def reference?(type)
[:references, :belongs_to].include? type
end
private
# parse possible attribute options like :limit for string/text/binary/integer, :precision/:scale for decimals or :polymorphic for references/belongs_to
# when declaring options curly brackets should be used
def parse_type_and_options(type)
case type
when /(text|binary)\{([a-z]+)\}/
return $1, size: $2.to_sym
when /(string|text|binary|integer)\{(\d+)\}/
return $1, limit: $2.to_i
when /decimal\{(\d+)[,.-](\d+)\}/
return :decimal, precision: $1.to_i, scale: $2.to_i
when /(references|belongs_to)\{(.+)\}/
type = $1
provided_options = $2.split(/[,.-]/)
options = Hash[provided_options.map { |opt| [opt.to_sym, true] }]
return type, options
else
return type, {}
end
end
end
def initialize(name, type = nil, index_type = false, attr_options = {})
@name = name
@type = type || :string
@has_index = INDEX_OPTIONS.include?(index_type)
@has_uniq_index = UNIQ_INDEX_OPTIONS.include?(index_type)
@attr_options = attr_options
end
def field_type
@field_type ||= case type
when :integer then :number_field
when :float, :decimal then :text_field
when :time then :time_field
when :datetime, :timestamp then :datetime_field
when :date then :date_field
when :text then :text_area
when :rich_text then :rich_text_area
when :boolean then :check_box
when :attachment, :attachments then :file_field
else
:text_field
end
end
def default
@default ||= case type
when :integer then 1
when :float then 1.5
when :decimal then "9.99"
when :datetime, :timestamp, :time then Time.now.to_fs(:db)
when :date then Date.today.to_fs(:db)
when :string then name == "type" ? "" : "MyString"
when :text then "MyText"
when :boolean then false
when :references, :belongs_to,
:attachment, :attachments,
:rich_text then nil
else
""
end
end
def plural_name
name.delete_suffix("_id").pluralize
end
def singular_name
name.delete_suffix("_id").singularize
end
def human_name
name.humanize
end
def index_name
@index_name ||= if polymorphic?
%w(id type).map { |t| "#{name}_#{t}" }
else
column_name
end
end
def column_name
@column_name ||= reference? ? "#{name}_id" : name
end
def foreign_key?
name.end_with?("_id")
end
def reference?
self.class.reference?(type)
end
def polymorphic?
attr_options[:polymorphic]
end
def required?
reference? && Rails.application.config.active_record.belongs_to_required_by_default
end
def has_index?
@has_index
end
def has_uniq_index?
@has_uniq_index
end
def password_digest?
name == "password" && type == :digest
end
def token?
type == :token
end
def rich_text?
type == :rich_text
end
def attachment?
type == :attachment
end
def attachments?
type == :attachments
end
def virtual?
rich_text? || attachment? || attachments?
end
def inject_options
(+"").tap { |s| options_for_migration.each { |k, v| s << ", #{k}: #{v.inspect}" } }
end
def inject_index_options
has_uniq_index? ? ", unique: true" : ""
end
def options_for_migration
@attr_options.dup.tap do |options|
if required?
options[:null] = false
end
if reference? && !polymorphic?
options[:foreign_key] = true
end
end
end
end
end
end