lib/bindata/sanitize.rb in bindata-0.10.0 vs lib/bindata/sanitize.rb in bindata-0.11.0

- old
+ new

@@ -1,62 +1,165 @@ require 'bindata/registry' -require 'forwardable' module BinData - # A BinData object accepts arbitrary parameters. This class only contains - # parameters that have been sanitized. + # A BinData object accepts arbitrary parameters. This class sanitizes + # those parameters so they can be used by the BinData object. class SanitizedParameters - extend Forwardable - def initialize(the_class, params) + def initialize(params, the_class) + @all_sanitized = false + @the_class = the_class + @parameters = {} + params.each { |param, value| @parameters[param.to_sym] = value } - params.each do |k,v| - k = k.to_sym - if v.nil? - raise ArgumentError, "parameter :#{k} has nil value in #{the_class}" + ensure_no_nil_values + end + + def length + @parameters.size + end + alias_method :size, :length + + def [](param) + @parameters[param] + end + + def []=(param, value) + @parameters[param] = value unless @all_sanitized + end + + def has_parameter?(param) + @parameters.has_key?(param) + end + + def needs_sanitizing?(param) + has_parameter?(param) and not self[param].is_a?(SanitizedParameter) + end + + def all_sanitized? + @all_sanitized + end + + def sanitize!(sanitizer) + unless @all_sanitized + merge_default_parameters! + + @the_class.sanitize_parameters!(self, sanitizer) + + ensure_mandatory_parameters_exist + ensure_mutual_exclusion_of_parameters + + @all_sanitized = true + end + end + + def move_unknown_parameters_to(dest) + unless @all_sanitized + unused_keys = @parameters.keys - @the_class.accepted_parameters.all + unused_keys.each do |param| + dest[param] = @parameters.delete(param) end + end + end - @parameters[k] = v + def delete(param) + @parameters.delete(param) + end + + #--------------- + private + + def ensure_no_nil_values + @parameters.each do |param, value| + if value.nil? + raise ArgumentError, + "parameter '#{param}' has nil value in #{@the_class}" + end end end - attr_reader :parameters + def merge_default_parameters! + @the_class.default_parameters.each do |param, value| + self[param] ||= value + end + end - def_delegators :@parameters, :[], :has_key?, :include?, :keys + def ensure_mandatory_parameters_exist + @the_class.mandatory_parameters.each do |param| + unless has_parameter?(param) + raise ArgumentError, + "parameter '#{param}' must be specified in #{@the_class}" + end + end + end + + def ensure_mutual_exclusion_of_parameters + return if length < 2 + + @the_class.mutually_exclusive_parameters.each do |param1, param2| + if has_parameter?(param1) and has_parameter?(param2) + raise ArgumentError, "params '#{param1}' and '#{param2}' " + + "are mutually exclusive in #{@the_class}" + end + end + end + end + #---------------------------------------------------------------------------- # The Sanitizer sanitizes the parameters that are passed when creating a # BinData object. Sanitizing consists of checking for mandatory, optional # and default parameters and ensuring the values of known parameters are # valid. class Sanitizer class << self # Sanitize +params+ for +the_class+. # Returns sanitized parameters. - def sanitize(the_class, params) - if SanitizedParameters === params + def sanitize(params, the_class) + if params.is_a?(SanitizedParameters) and params.all_sanitized? params else sanitizer = self.new - sanitizer.sanitized_params(the_class, params) + sanitizer.create_sanitized_params(params, the_class) end end end def initialize @endian = nil - @seen = [] end - # Executes the given block with +endian+ set as the current endian. + def create_sanitized_params(params, the_class) + sanitized_params = as_sanitized_params(params, the_class) + sanitized_params.sanitize!(self) + + sanitized_params + end + + def create_sanitized_endian(endian) + SanitizedEndian.new(endian) + end + + def create_sanitized_choices(choices) + SanitizedChoices.new(self, choices) + end + + def create_sanitized_fields(endian = nil) + SanitizedFields.new(self, endian) + end + + def create_sanitized_object_prototype(obj_type, obj_params, endian = nil) + SanitizedPrototype.new(self, obj_type, obj_params, endian) + end + def with_endian(endian, &block) if endian != nil saved_endian = @endian - @endian = endian + @endian = endian.is_a?(SanitizedEndian) ? endian.endian : endian yield @endian = saved_endian else yield end @@ -68,55 +171,98 @@ raise TypeError, "unknown type '#{type}'" end registered_class end - def sanitized_params(the_class, params) - new_params = params.nil? ? {} : params.dup + #--------------- + private - if can_sanitize_parameters?(the_class) - get_sanitized_params(the_class, new_params) + def as_sanitized_params(params, the_class) + if SanitizedParameters === params + params else - store_current_endian!(the_class, new_params) - new_params + SanitizedParameters.new(params || {}, the_class) end end + end + #---------------------------------------------------------------------------- - #--------------- - private + class SanitizedParameter; end - def can_sanitize_parameters?(the_class) - not need_to_delay_sanitizing?(the_class) + class SanitizedPrototype < SanitizedParameter + def initialize(sanitizer, obj_type, obj_params, endian = nil) + sanitizer.with_endian(endian) do + @obj_class = sanitizer.lookup_class(obj_type) + @obj_params = sanitizer.create_sanitized_params(obj_params, @obj_class) + end end - def need_to_delay_sanitizing?(the_class) - the_class.recursive? and @seen.include?(the_class) + def instantiate(parent = nil) + @obj_class.new(@obj_params, parent) end + end + #---------------------------------------------------------------------------- - def get_sanitized_params(the_class, params) - result = nil - with_class_to_sanitize(the_class) do - the_class.sanitize_parameters!(self, params) - result = SanitizedParameters.new(the_class, params) + class SanitizedField < SanitizedParameter + def initialize(sanitizer, name, field_type, field_params) + @name = name.to_s + @prototype = sanitizer.create_sanitized_object_prototype(field_type, field_params) + end + attr_reader :name + + def instantiate(parent = nil) + @prototype.instantiate(parent) + end + end + #---------------------------------------------------------------------------- + + class SanitizedFields < SanitizedParameter + def initialize(sanitizer, endian) + @sanitizer = sanitizer + @endian = endian + @fields = [] + end + + def add_field(type, name, params) + @sanitizer.with_endian(@endian) do + @fields << SanitizedField.new(@sanitizer, name, type, params) end - result end - def with_class_to_sanitize(the_class, &block) - @seen.push(the_class) - yield - @seen.pop + def [](idx) + @fields[idx] end - def store_current_endian!(the_class, params) - if can_store_endian?(the_class, params) - params[:endian] = @endian + def field_names + @fields.collect { |field| field.name } + end + end + #---------------------------------------------------------------------------- + + class SanitizedChoices < SanitizedParameter + def initialize(sanitizer, choices) + @choices = {} + choices.each_pair do |key, val| + type, param = val + prototype = sanitizer.create_sanitized_object_prototype(type, param) + @choices[key] = prototype end end - def can_store_endian?(the_class, params) - (@endian != nil and - the_class.accepted_internal_parameters.include?(:endian) and - not params.has_key?(:endian)) + def [](key) + @choices[key] end + end + #---------------------------------------------------------------------------- + + class SanitizedEndian < SanitizedParameter + def initialize(endian) + unless [:little, :big].include?(endian) + raise ArgumentError, "unknown value for endian '#{endian}'" + end + + @endian = endian + end + + attr_reader :endian end end