-
Notifications
You must be signed in to change notification settings - Fork 92
/
hobo_fields.rb
164 lines (123 loc) · 5.15 KB
/
hobo_fields.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
require 'hobosupport'
ActiveSupport::Dependencies.load_paths |= [ File.dirname(__FILE__) ]
module Hobo
# Empty class to represent the boolean type.
class Boolean; end
end
module HoboFields
VERSION = "0.9.103"
extend self
PLAIN_TYPES = {
:boolean => Hobo::Boolean,
:date => Date,
:datetime => (defined?(ActiveSupport::TimeWithZone) ? ActiveSupport::TimeWithZone : Time),
:time => Time,
:integer => Integer,
:decimal => BigDecimal,
:float => Float,
:string => String
}
ALIAS_TYPES = {
Fixnum => "integer",
Bignum => "integer"
}
# Provide a lookup for these rather than loading them all preemptively
STANDARD_TYPES = {
:raw_html => "RawHtmlString",
:html => "HtmlString",
:raw_markdown => "RawMarkdownString",
:markdown => "MarkdownString",
:textile => "TextileString",
:password => "PasswordString",
:text => "Text",
:email_address => "EmailAddress",
:serialized => "SerializedObject"
}
@field_types = PLAIN_TYPES.with_indifferent_access
@never_wrap_types = Set.new([NilClass, Hobo::Boolean, TrueClass, FalseClass])
attr_reader :field_types
def to_class(type)
if type.is_one_of?(Symbol, String)
type = type.to_sym
field_types[type] || standard_class(type)
else
type # assume it's already a class
end
end
def to_name(type)
field_types.key(type) || ALIAS_TYPES[type]
end
def can_wrap?(type, val)
col_type = type::COLUMN_TYPE
return false if val.blank? && (col_type == :integer || col_type == :float || col_type == :decimal)
klass = Object.instance_method(:class).bind(val).call # Make sure we get the *real* class
arity = type.instance_method(:initialize).arity
(arity == 1 || arity == -1) && !@never_wrap_types.any? { |c| klass <= c }
end
def never_wrap(type)
@never_wrap_types << type
end
def register_type(name, klass)
field_types[name] = klass
end
def plain_type?(type_name)
type_name.in?(PLAIN_TYPES)
end
def standard_class(name)
class_name = STANDARD_TYPES[name]
"HoboFields::#{class_name}".constantize if class_name
end
def enable
require "hobo_fields/enum_string"
require "hobo_fields/fields_declaration"
# Add the fields do declaration to ActiveRecord::Base
ActiveRecord::Base.send(:include, HoboFields::FieldsDeclaration)
# automatically load other rich types from app/rich_types/*.rb
# don't assume we're in a Rails app
if defined?(::Rails)
plugins = Hobo.try.rails_initializer ? Rails.configuration.plugin_loader.new(Hobo.rails_initializer).plugins : []
([::Rails.root] + plugins.map(&:directory)).each do |dir|
Dir[File.join(dir, 'app', 'rich_types', '*.rb')].each do |f|
# TODO: should we complain if field_types doesn't get a new value? Might be useful to warn people if they're missing a register_type
require f
end
end
end
# Monkey patch ActiveRecord so that the attribute read & write methods
# automatically wrap richly-typed fields.
ActiveRecord::AttributeMethods::ClassMethods.class_eval do
# Define an attribute reader method. Cope with nil column.
def define_read_method(symbol, attr_name, column)
cast_code = column.type_cast_code('v') if column
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
unless attr_name.to_s == self.primary_key.to_s
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) " +
"unless @attributes.has_key?('#{attr_name}'); ")
end
# This is the Hobo hook - add a type wrapper around the field
# value if we have a special type defined
src = if connected? && (type_wrapper = try.attr_type(symbol)) &&
type_wrapper.is_a?(Class) && type_wrapper.not_in?(HoboFields::PLAIN_TYPES.values)
"val = begin; #{access_code}; end; wrapper_type = self.class.attr_type(:#{attr_name}); " +
"if HoboFields.can_wrap?(wrapper_type, val); wrapper_type.new(val); else; val; end"
else
access_code
end
evaluate_attribute_method(attr_name,
"def #{symbol}; @attributes_cache['#{attr_name}'] ||= begin; #{src}; end; end")
end
def define_write_method(attr_name)
src = if connected? && (type_wrapper = try.attr_type(attr_name)) &&
type_wrapper.is_a?(Class) && type_wrapper.not_in?(HoboFields::PLAIN_TYPES.values)
"begin; wrapper_type = self.class.attr_type(:#{attr_name}); " +
"if !val.is_a?(wrapper_type) && HoboFields.can_wrap?(wrapper_type, val); wrapper_type.new(val); else; val; end; end"
else
"val"
end
evaluate_attribute_method(attr_name,
"def #{attr_name}=(val); write_attribute('#{attr_name}', #{src});end", "#{attr_name}=")
end
end
end
end
HoboFields.enable if defined? ActiveRecord