Module: HoboFields
- Extended by:
- HoboFields
- Included in:
- HoboFields
- Defined in:
- lib/hobo_fields.rb,
lib/hobo_fields/text.rb,
lib/hobo_fields/field_spec.rb,
lib/hobo_fields/index_spec.rb,
lib/hobo_fields/enum_string.rb,
lib/hobo_fields/html_string.rb,
lib/hobo_fields/email_address.rb,
lib/hobo_fields/sanitize_html.rb,
lib/hobo_fields/textile_string.rb,
lib/hobo_fields/lifecycle_state.rb,
lib/hobo_fields/markdown_string.rb,
lib/hobo_fields/password_string.rb,
lib/hobo_fields/raw_html_string.rb,
lib/hobo_fields/model_extensions.rb,
lib/hobo_fields/serialized_object.rb,
lib/hobo_fields/fields_declaration.rb,
lib/hobo_fields/migration_generator.rb,
lib/hobo_fields/raw_markdown_string.rb,
lib/hobo_fields/field_declaration_dsl.rb
Defined Under Namespace
Modules: SanitizeHtml Classes: EmailAddress, EnumString, FieldDeclarationDsl, FieldSpec, HabtmModelShim, HtmlString, IndexSpec, LifecycleState, MarkdownString, MigrationGenerator, MigrationGeneratorError, PasswordString, RawHtmlString, RawMarkdownString, SerializedObject, Text, TextileString
Constant Summary collapse
- VERSION =
"1.1.0"
- 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" }
- STANDARD_TYPES =
Provide a lookup for these rather than loading them all preemptively
{ :raw_html => "RawHtmlString", :html => "HtmlString", :raw_markdown => "RawMarkdownString", :markdown => "MarkdownString", :textile => "TextileString", :password => "PasswordString", :text => "Text", :email_address => "EmailAddress", :serialized => "SerializedObject" }
- ModelExtensions =
classy_module do # ignore the model in the migration until somebody sets # @include_in_migration via the fields declaration inheriting_cattr_reader :include_in_migration => false # attr_types holds the type class for any attribute reader (i.e. getter # method) that returns rich-types inheriting_cattr_reader :attr_types => HashWithIndifferentAccess.new inheriting_cattr_reader :attr_order => [] # field_specs holds FieldSpec objects for every declared # field. Note that attribute readers are created (by ActiveRecord) # for all fields, so there is also an entry for the field in # attr_types. This is redundant but simplifies the implementation # and speeds things up a little. inheriting_cattr_reader :field_specs => HashWithIndifferentAccess.new # index_specs holds IndexSpec objects for all the declared indexes. inheriting_cattr_reader :index_specs => [] inheriting_cattr_reader :ignore_indexes => [] def self.inherited(klass) fields do |f| f.field(inheritance_column, :string) end index(inheritance_column) super end def self.index(fields, = {}) # don't double-index fields index_specs << HoboFields::IndexSpec.new(self, fields, ) unless index_specs.*.fields.include?(Array.wrap(fields).*.to_s) end # tell the migration generator to ignore the named index. Useful for existing indexes, or for indexes # that can't be automatically generated (for example: an prefix index in MySQL) def self.ignore_index(index_name) ignore_indexes << index_name.to_s end private # Declares that a virtual field that has a rich type (e.g. created # by attr_accessor :foo, :type => :email_address) should be subject # to validation (note that the rich types know how to validate themselves) def self.validate_virtual_field(*args) validates_each(*args) {|record, field, value| msg = value.validate and record.errors.add(field, msg) if value.respond_to?(:validate) } end # This adds a ":type => t" option to attr_accessor, where t is # either a class or a symbolic name of a rich type. If this option # is given, the setter will wrap values that are not of the right # type. def self.attr_accessor_with_rich_types(*attrs) = attrs. type = .delete(:type) attrs << unless .empty? public attr_accessor_without_rich_types(*attrs) private if type type = HoboFields.to_class(type) attrs.each do |attr| declare_attr_type attr, type, type_wrapper = attr_type(attr) define_method "#{attr}=" do |val| if type_wrapper.not_in?(HoboFields::PLAIN_TYPES.values) && !val.is_a?(type) && HoboFields.can_wrap?(type, val) val = type.new(val.to_s) end instance_variable_set("@#{attr}", val) end end end end # Extend belongs_to so that it creates a FieldSpec for the foreign key def self.belongs_to_with_field_declarations(name, ={}, &block) = {} [:null] = .delete(:null) if .has_key?(:null) [:comment] = .delete(:comment) if .has_key?(:comment) = {} [:name] = .delete(:index) if .has_key?(:index) bt = belongs_to_without_field_declarations(name, , &block) refl = reflections[name.to_sym] fkey = refl.primary_key_name declare_field(fkey.to_sym, :integer, ) if refl.[:polymorphic] declare_polymorphic_type_field(name, ) index(["#{name}_type", fkey], ) if [:name]!=false else index(fkey, ) if [:name]!=false end bt end class << self alias_method_chain :belongs_to, :field_declarations end # Declares the "foo_type" field that accompanies the "foo_id" # field for a polyorphic belongs_to def self.declare_polymorphic_type_field(name, ) type_col = "#{name}_type" declare_field(type_col, :string, ) # FIXME: Before hobofields was extracted, this used to now do: # never_show(type_col) # That needs doing somewhere end # Declare a rich-type for any attribute (i.e. getter method). This # does not effect the attribute in any way - it just records the # metadata. def self.declare_attr_type(name, type, ={}) klass = HoboFields.to_class(type) attr_types[name] = HoboFields.to_class(type) klass.try.declared(self, name, ) end # Declare named field with a type and an arbitrary set of # arguments. The arguments are forwarded to the #field_added # callback, allowing custom metadata to be added to field # declarations. def self.declare_field(name, type, *args) = args. try.field_added(name, type, args, ) add_formatting_for_field(name, type, args) add_validations_for_field(name, type, args) add_index_for_field(name, args, ) declare_attr_type(name, type, ) unless HoboFields.plain_type?(type) field_specs[name] = HoboFields::FieldSpec.new(self, name, type, ) attr_order << name unless name.in?(attr_order) end # Add field validations according to arguments in the # field declaration def self.add_validations_for_field(name, type, args) validates_presence_of name if :required.in?(args) validates_uniqueness_of name, :allow_nil => !:required.in?(args) if :unique.in?(args) type_class = HoboFields.to_class(type) if type_class && type_class.public_method_defined?("validate") self.validate do |record| v = record.send(name)._?.validate record.errors.add(name, v) if v.is_a?(String) end end end def self.add_formatting_for_field(name, type, args) type_class = HoboFields.to_class(type) if type_class && "format".in?(type_class.instance_methods) self.before_validation do |record| record.send("#{name}=", record.send(name)._?.format) end end end def self.add_index_for_field(name, args, ) to_name = .delete(:index) return unless to_name index_opts = {} index_opts[:unique] = :unique.in?(args) || .delete(:unique) # support :index => true declaration index_opts[:name] = to_name unless to_name == true index(name, index_opts) end # Extended version of the acts_as_list declaration that # automatically delcares the 'position' field def self.acts_as_list_with_field_declaration( = {}) declare_field(.fetch(:column, "position"), :integer) acts_as_list_without_field_declaration() end # Returns the type (a class) for a given field or association. If # the association is a collection (has_many or habtm) return the # AssociationReflection instead def self.attr_type(name) if attr_types.nil? && self != self.name.constantize raise RuntimeError, "attr_types called on a stale class object (#{self.name}). Avoid storing persistent references to classes" end attr_types[name] or if (refl = reflections[name.to_sym]) if refl.macro.in?([:has_one, :belongs_to]) && !refl.[:polymorphic] refl.klass else refl end end or (col = column(name.to_s) and HoboFields::PLAIN_TYPES[col.type] || col.klass) end # Return the entry from #columns for the named column def self.column(name) return unless (@table_exists ||= table_exists?) name = name.to_s columns.find {|c| c.name == name } end class << self alias_method_chain :acts_as_list, :field_declaration if defined?(ActiveRecord::Acts::List) alias_method_chain :attr_accessor, :rich_types end end
- FieldsDeclaration =
classy_module do def self.fields(include_in_migration = true, &b) # Any model that calls 'fields' gets a bunch of other # functionality included automatically, but make sure we only # include it once include HoboFields::ModelExtensions unless HoboFields::ModelExtensions.in?(included_modules) @include_in_migration ||= include_in_migration if b dsl = HoboFields::FieldDeclarationDsl.new(self) if b.arity == 1 yield dsl else dsl.instance_eval(&b) end end end end
Instance Attribute Summary collapse
-
#field_types ⇒ Object
readonly
Returns the value of attribute field_types.
Instance Method Summary collapse
-
#can_wrap?(type, val) ⇒ Boolean
Ruby 1.8.
- #enable ⇒ Object
- #never_wrap(type) ⇒ Object
- #plain_type?(type_name) ⇒ Boolean
- #register_type(name, klass) ⇒ Object
- #standard_class(name) ⇒ Object
- #to_class(type) ⇒ Object
- #to_name(type) ⇒ Object
Instance Attribute Details
#field_types ⇒ Object (readonly)
Returns the value of attribute field_types.
54 55 56 |
# File 'lib/hobo_fields.rb', line 54 def field_types @field_types end |
Instance Method Details
#can_wrap?(type, val) ⇒ Boolean
Ruby 1.8. 1.8.6 doesn’t include Method#owner. 1.8.7 could use the 1.9 function, but this one is faster.
73 74 75 76 77 78 79 80 81 |
# File 'lib/hobo_fields.rb', line 73 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 init_method = type.instance_method(:initialize) [-1,1].include?(init_method.arity) && init_method.owner != Object.instance_method(:initialize).owner && !@never_wrap_types.any? { |c| klass <= c } end |
#enable ⇒ Object
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 |
# File 'lib/hobo_fields.rb', line 115 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 = Rails.configuration.plugin_loader.new(HoboFields.rails_initializer).plugins ([::Rails.root] + plugins.map(&:directory)).each do |dir| if ActiveSupport::Dependencies.respond_to?(:autoload_paths) ActiveSupport::Dependencies.autoload_paths << File.join(dir, 'app', 'rich_types') else ActiveSupport::Dependencies.load_paths << File.join(dir, 'app', 'rich_types') end 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_dependency 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 |
#never_wrap(type) ⇒ Object
95 96 97 |
# File 'lib/hobo_fields.rb', line 95 def never_wrap(type) @never_wrap_types << type end |
#plain_type?(type_name) ⇒ Boolean
105 106 107 |
# File 'lib/hobo_fields.rb', line 105 def plain_type?(type_name) type_name.in?(PLAIN_TYPES) end |
#register_type(name, klass) ⇒ Object
100 101 102 |
# File 'lib/hobo_fields.rb', line 100 def register_type(name, klass) field_types[name] = klass end |
#standard_class(name) ⇒ Object
110 111 112 113 |
# File 'lib/hobo_fields.rb', line 110 def standard_class(name) class_name = STANDARD_TYPES[name] "HoboFields::#{class_name}".constantize if class_name end |
#to_class(type) ⇒ Object
56 57 58 59 60 61 62 63 |
# File 'lib/hobo_fields.rb', line 56 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 |
#to_name(type) ⇒ Object
66 67 68 |
# File 'lib/hobo_fields.rb', line 66 def to_name(type) field_types.key(type) || ALIAS_TYPES[type] end |