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
|
require 'roxml/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 ContradictoryNamespaces < StandardError
end
class Definition # :nodoc:
attr_reader :name, :sought_type, :wrapper, :hash, :blocks, :accessor, :to_xml, :attr_name, :namespace
bool_attr_reader :name_explicit, :array, :cdata, :required, :frozen
def initialize(sym, opts = {}, &block)
opts.assert_valid_keys(:from, :in, :as, :namespace,
:else, :required, :frozen, :cdata, :to_xml)
@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)
@namespace = opts.delete(:namespace)
@accessor = sym.to_s
opts[:as] ||=
if @accessor.ends_with?('?')
:bool
elsif @accessor.ends_with?('_on')
Date
elsif @accessor.ends_with?('_at')
DateTime
end
@array = opts[:as].is_a?(Array)
@blocks = collect_blocks(block, opts[:as])
@sought_type = extract_type(opts[:as])
if @sought_type.respond_to?(:roxml_tag_name)
opts[:from] ||= @sought_type.roxml_tag_name
end
if opts[:from] == :content
opts[:from] = '.'
elsif opts[:from] == :name
opts[:from] = '*'
elsif opts[:from] == :attr
@sought_type = :attr
opts[:from] = nil
elsif opts[:from] == :namespace
opts[:from] = '*'
@sought_type = :namespace
elsif opts[:from].to_s.starts_with?('@')
@sought_type = :attr
opts[:from] = opts[:from].sub('@', '')
end
@name = @attr_name = accessor.to_s.chomp('?')
@name = @name.singularize if hash? || array?
@name = (opts[:from] || @name).to_s
if hash? && (hash.key.name? || hash.value.name?)
@name = '*'
end
raise ContradictoryNamespaces if @name.include?(':') && (@namespace.present? || @namespace == false)
raise ArgumentError, "Can't specify both :else default and :required" if required? && @default
end
def instance_variable_name
:"@#{attr_name}"
end
def setter
:"#{attr_name}="
end
def hash
if hash?
@sought_type.wrapper ||= name
@sought_type
end
end
def hash?
@sought_type.is_a?(HashDefinition)
end
def name?
@name == '*'
end
def content?
@name == '.'
end
def default
if @default.nil?
@default = [] if array?
@default = {} if hash?
end
@default.duplicable? ? @default.dup : @default
end
def to_ref(inst)
case sought_type
when :attr then XMLAttributeRef
when :text then XMLTextRef
when :namespace then XMLNameSpaceRef
when HashDefinition then XMLHashRef
when Symbol then raise ArgumentError, "Invalid type argument #{sought_type}"
else XMLObjectRef
end.new(self, inst)
end
private
def self.all(items, &block)
array = items.is_a?(Array)
results = (array ? items : [items]).map do |item|
yield item
end
array ? results : results.first
end
def self.fetch_bool(value, default)
value = value.to_s.downcase
if %w{true yes 1 t}.include? value
true
elsif %w{false no 0 f}.include? value
false
else
default
end
end
CORE_BLOCK_SHORTHANDS = {
# Core Shorthands
Integer => lambda do |val|
all(val) do |v|
v.to_i unless v.blank?
end
end,
Float => lambda do |val|
all(val) do |v|
Float(v) unless v.blank?
end
end,
Time => lambda do |val|
all(val) {|v| Time.parse(v) unless v.blank? }
end,
:bool => nil,
:bool_standalone => lambda do |val|
all(val) do |v|
fetch_bool(v, nil)
end
end,
:bool_combined => lambda do |val|
all(val) do |v|
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
CORE_BLOCK_SHORTHANDS.tap do |blocks|
blocks.reverse_merge!(BigDecimal => lambda do |val|
all(val) do |v|
BigDecimal(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)
if as.is_a?(Array)
if as.size > 1
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
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
as = (block ? :bool_combined : :bool_standalone)
end
as = self.class.block_shorthands.fetch(as) do
unless (as == :text) || as.respond_to?(:from_xml) || (as.respond_to?(:first) && as.first.respond_to?(:from_xml)) || (as.is_a?(Hash) && !(as.keys & [:key, :value]).empty?)
raise ArgumentError, "Invalid :as argument #{as}" unless as.nil?
end
nil
end
[as, block].compact
end
def extract_type(as)
if as.is_a?(Hash)
return HashDefinition.new(as)
elsif as.respond_to?(:from_xml)
return as
elsif as.is_a?(Array) && as.first.respond_to?(:from_xml)
@array = true
return as.first
else
:text
end
end
end
end
|