lib/roxml/definition.rb in roxml-2.4.3 vs lib/roxml/definition.rb in roxml-2.5.0
- old
+ new
@@ -1,10 +1,21 @@
require File.join(File.dirname(__FILE__), 'hash_definition')
+class Module
+ def bool_attr_reader(*attrs)
+ attrs.each do |attr|
+ define_method :"#{attr}?" do
+ instance_variable_get(:"@#{attr}") || false
+ end
+ end
+ end
+end
+
module ROXML
class Definition # :nodoc:
- attr_reader :name, :type, :hash, :blocks, :accessor, :to_xml
+ attr_reader :name, :type, :wrapper, :hash, :blocks, :accessor, :to_xml
+ bool_attr_reader :name_explicit, :array, :cdata, :required, :frozen
class << self
def silence_xml_name_warning?
@silence_xml_name_warning || (ROXML.const_defined?('SILENCE_XML_NAME_WARNING') && ROXML::SILENCE_XML_NAME_WARNING)
end
@@ -14,106 +25,97 @@
end
end
def initialize(sym, *args, &block)
@accessor = sym
- @opts = extract_options!(args)
- @default = @opts.delete(:else)
- @to_xml = @opts.delete(:to_xml)
- @name_explicit = @opts.has_key?(:from)
-
- if @opts.has_key?(:readonly)
- raise ArgumentError, "There is no 'readonly' option. You probably mean to use :frozen => true"
+ if @accessor.to_s.ends_with?('_on')
+ ActiveSupport::Deprecation.warn "In 3.0, attributes with names ending with _on will default to Date type, rather than :text"
end
+ if @accessor.to_s.ends_with?('_at')
+ ActiveSupport::Deprecation.warn "In 3.0, attributes with names ending with _at will default to DateTime type, rather than :text"
+ end
- @opts.reverse_merge!(:as => [], :in => nil)
- @opts[:as] = [*@opts[:as]]
+ opts = extract_options!(args)
+ opts[:as] ||= :bool if @accessor.to_s.ends_with?('?')
- @type = extract_type(args)
- @opts[:as] << :bool if @accessor.to_s.ends_with?('?')
+ @array = opts[:as].is_a?(Array) || extract_from_as(opts, :array, "Please use [] around your usual type declaration")
+ @blocks = collect_blocks(block, opts[:as])
+ if opts.has_key?(:readonly)
+ raise ArgumentError, "There is no 'readonly' option. You probably mean to use :frozen => true"
+ end
+
+ @type = extract_type(args, opts)
if @type.try(:xml_name_without_deprecation?)
unless self.class.silence_xml_name_warning?
warn "WARNING: As of 2.3, a breaking change has been in the naming of sub-objects. " +
"ROXML now considers the xml_name of the sub-object before falling back to the accessor name of the parent. " +
"Use :from on the parent declaration to override this behavior. Set ROXML::SILENCE_XML_NAME_WARNING to avoid this message."
self.class.silence_xml_name_warning!
end
- @opts[:from] ||= @type.tag_name
- else
- @opts[:from] ||= variable_name
+ opts[:from] ||= @type.tag_name
end
- @blocks = collect_blocks(block, @opts[:as])
+ if opts[:from] == :content
+ opts[:from] = '.'
+ elsif opts[:from] == :name
+ opts[:from] = '*'
+ elsif opts[:from] == :attr
+ @type = :attr
+ opts[:from] = nil
+ elsif opts[:from].to_s.starts_with?('@')
+ @type = :attr
+ opts[:from].sub!('@', '')
+ end
- @name = @opts[:from].to_s
+ @name = (opts[:from] || variable_name).to_s
@name = @name.singularize if hash? || array?
if hash? && (hash.key.name? || hash.value.name?)
@name = '*'
end
raise ArgumentError, "Can't specify both :else default and :required" if required? && @default
end
def variable_name
- accessor.to_s.ends_with?('?') ? accessor.to_s.chomp('?') : accessor.to_s
+ accessor.to_s.chomp('?')
end
def hash
- @hash ||= HashDefinition.new(@opts.delete(:hash), name) if hash?
+ if hash?
+ @type.wrapper ||= name
+ @type
+ end
end
def hash?
- @type == :hash
+ @type.is_a?(HashDefinition)
end
def name?
@name == '*'
end
- def name_explicit?
- @name_explicit
- end
-
def content?
- @type == :content
+ @name == '.'
end
- def array?
- @opts[:as].include? :array
- end
-
- def cdata?
- @opts[:as].include? :cdata
- end
-
- def wrapper
- @opts[:in]
- end
-
- def required?
- @opts[:required]
- end
-
- def freeze?
- @opts[:frozen]
- end
-
def default
- @default ||= [] if array?
- @default ||= {} if hash?
+ if @default.nil?
+ @default = [] if array?
+ @default = {} if hash?
+ end
@default.duplicable? ? @default.dup : @default
end
def to_ref(inst)
case type
- when :attr then XMLAttributeRef
- when :content then XMLTextRef
- when :text then XMLTextRef
- when :hash then XMLHashRef
- when Symbol then raise ArgumentError, "Invalid type argument #{opts.type}"
- else XMLObjectRef
+ when :attr then XMLAttributeRef
+ when :text then XMLTextRef
+ when HashDefinition then XMLHashRef
+ when Symbol then raise ArgumentError, "Invalid type argument #{type}"
+ else XMLObjectRef
end.new(self, inst)
end
private
def self.all(items, &block)
@@ -125,17 +127,17 @@
array ? results : results.first
end
BLOCK_TO_FLOAT = lambda do |val|
all(val) do |v|
- Float(v) unless blank_string?(v)
+ Float(v) unless v.blank?
end
end
BLOCK_TO_INT = lambda do |val|
all(val) do |v|
- Integer(v) unless blank_string?(v)
+ Integer(v) unless v.blank?
end
end
def self.fetch_bool(value, default)
value = value.try(:downcase)
@@ -145,34 +147,24 @@
false
else
default
end
end
-
- def self.blank_string?(value)
- value.is_a?(String) && value.blank?
- end
-
- BLOCK_SHORTHANDS = {
+
+ CORE_BLOCK_SHORTHANDS = {
+ # Core Shorthands
:integer => BLOCK_TO_INT, # deprecated
Integer => BLOCK_TO_INT,
:float => BLOCK_TO_FLOAT, # deprecated
Float => BLOCK_TO_FLOAT,
- Date => lambda do |val|
- if defined?(Date)
- all(val) {|v| Date.parse(v) unless blank_string?(v) }
+ Fixnum => lambda do |val|
+ all(val) do |v|
+ v.to_i unless v.blank?
end
end,
- DateTime => lambda do |val|
- if defined?(DateTime)
- all(val) {|v| DateTime.parse(v) unless blank_string?(v) }
- end
- end,
Time => lambda do |val|
- if defined?(Time)
- all(val) {|v| Time.parse(v) unless blank_string?(v) }
- end
+ all(val) {|v| Time.parse(v) unless v.blank? }
end,
:bool => nil,
:bool_standalone => lambda do |val|
all(val) do |v|
@@ -184,65 +176,149 @@
fetch_bool(v, v)
end
end
}
+ def self.block_shorthands
+ # dynamically load these shorthands at class definition time, but
+ # only if they're already availbable
+ returning CORE_BLOCK_SHORTHANDS do |blocks|
+ blocks.reverse_merge!(BigDecimal => lambda do |val|
+ all(val) do |v|
+ BigDecimal.new(v) unless v.blank?
+ end
+ end) if defined?(BigDecimal)
+
+ blocks.reverse_merge!(DateTime => lambda do |val|
+ if defined?(DateTime)
+ all(val) {|v| DateTime.parse(v) unless v.blank? }
+ end
+ end) if defined?(DateTime)
+
+ blocks.reverse_merge!(Date => lambda do |val|
+ if defined?(Date)
+ all(val) {|v| Date.parse(v) unless v.blank? }
+ end
+ end) if defined?(Date)
+ end
+ end
+
def collect_blocks(block, as)
- ActiveSupport::Deprecation.warn ":as => :float is deprecated. Use :as => Float instead" if as.include?(:float)
- ActiveSupport::Deprecation.warn ":as => :integer is deprecated. Use :as => Integer instead" if as.include?(:integer)
+ ActiveSupport::Deprecation.warn ":as => :float is deprecated. Use :as => Float instead" if as == :float
+ ActiveSupport::Deprecation.warn ":as => :integer is deprecated. Use :as => Integer instead" if as == :integer
- shorthands = as & BLOCK_SHORTHANDS.keys
- if shorthands.size > 1
- raise ArgumentError, "multiple block shorthands supplied #{shorthands.map(&:to_s).join(', ')}"
+ if as.is_a?(Array)
+ unless as.one? || as.empty?
+ raise ArgumentError, "multiple :as types (#{as.map(&:inspect).join(', ')}) is not supported. Use a block if you want more complicated behavior."
+ end
+
+ as = as.first
end
- shorthand = shorthands.first
- if shorthand == :bool
+ if as == :bool
# if a second block is present, and we can't coerce the xml value
# to bool, we need to be able to pass it to the user-provided block
- shorthand = block ? :bool_combined : :bool_standalone
+ as = (block ? :bool_combined : :bool_standalone)
end
- [BLOCK_SHORTHANDS[shorthand], block].compact
+ as = self.class.block_shorthands.fetch(as) do
+ unless as.respond_to?(:from_xml) || as.try(:first).respond_to?(:from_xml) || (as.is_a?(Hash) && !(as.keys & HASH_KEYS).empty?)
+ ActiveSupport::Deprecation.warn "#{as.inspect} is not a valid type declaration. ROXML will raise in this case in version 3.0" unless as.nil?
+ end
+ nil
+ end
+ [as, block].compact
end
def extract_options!(args)
opts = args.extract_options!
unless (opts.keys & HASH_KEYS).empty?
args.push(opts)
opts = {}
end
+
+ @default = opts.delete(:else)
+ @to_xml = opts.delete(:to_xml)
+ @name_explicit = opts.has_key?(:from) && opts[:from].is_a?(String)
+ @cdata = opts.delete(:cdata)
+ @required = opts.delete(:required)
+ @frozen = opts.delete(:frozen)
+ @wrapper = opts.delete(:in)
+
+ @cdata ||= extract_from_as(opts, :cdata, "Please use :cdata => true")
+
+ if opts[:as].is_a?(Array) && opts[:as].size > 1
+ ActiveSupport::Deprecation.warn ":as should point to a single item. #{opts[:as].join(', ')} should be declared some other way."
+ end
+
opts
end
- def extract_type(args)
- types = (@opts.keys & TYPE_KEYS)
+ def extract_from_as(opts, entry, message)
+ # remove with deprecateds...
+ if [*opts[:as]].include?(entry)
+ ActiveSupport::Deprecation.warn ":as => #{entry.inspect} is deprecated. #{message}"
+ if opts[:as] == entry
+ opts[:as] = nil
+ else
+ opts[:as].delete(entry)
+ end
+ true
+ end
+ end
+
+ def extract_type(args, opts)
+ types = (opts.keys & TYPE_KEYS)
# type arg
if args.one? && types.empty?
type = args.first
if type.is_a? Array
- @opts[:as] << :array
- return type.first
+ ActiveSupport::Deprecation.warn "Array declarations should be passed as the :as parameter, for future release."
+ @array = true
+ return type.first || :text
elsif type.is_a? Hash
- @opts[:hash] = type
- return :hash
+ ActiveSupport::Deprecation.warn "Hash declarations should be passed as the :as parameter, for future release."
+ return HashDefinition.new(type)
+ elsif type == :content
+ ActiveSupport::Deprecation.warn ":content as a type declaration is deprecated. Use :from => '.' or :from => :content instead"
+ opts[:from] = :content
+ return :text
+ elsif type == :attr
+ ActiveSupport::Deprecation.warn ":attr as a type declaration is deprecated. Use :from => '@attr_name' or :from => :attr instead"
+ opts[:from].sub!('@', '') if opts[:from].to_s.starts_with?('@') # this is added back next line...
+ opts[:from] = opts[:from].nil? ? :attr : "@#{opts[:from]}"
+ return :attr
else
+ ActiveSupport::Deprecation.warn "Type declarations should be passed as the :as parameter, for future release."
return type
end
end
unless args.empty?
raise ArgumentError, "too many arguments (#{(args + types).join(', ')}). Should be name, type, and " +
"an options hash, with the type and options optional"
end
+ if opts[:as].is_a?(Hash)
+ return HashDefinition.new(opts[:as])
+ elsif opts[:as].respond_to?(:from_xml)
+ return opts[:as]
+ elsif opts[:as].is_a?(Array) && opts[:as].first.respond_to?(:from_xml)
+ @array = true
+ return opts[:as].first
+ end
+
# type options
if types.one?
- @opts[:from] = @opts.delete(types.first)
+ opts[:from] = opts.delete(types.first)
+ if opts[:from] == :content
+ opts[:from] = 'content'
+ ActiveSupport::Deprecation.warn ":content is now a reserved as an alias for '.'. Use the string 'content' instead"
+ end
types.first
elsif types.empty?
:text
else
raise ArgumentError, "more than one type option specified: #{types.join(', ')}"
end
end
end
-end
\ No newline at end of file
+end