lib/bindata/base.rb in bindata-0.9.3 vs lib/bindata/base.rb in bindata-0.10.0

- old
+ new

@@ -14,15 +14,10 @@ # == Parameters # # Parameters may be provided at initialisation to control the behaviour of # an object. These params are: # - # [<tt>:readwrite</tt>] Deprecated. An alias for :onlyif. - # [<tt>:onlyif</tt>] Used to indicate a data object is optional. - # if false, calls to #read or #write will not - # perform any I/O, #num_bytes will return 0 and - # #snapshot will return nil. Default is true. # [<tt>:check_offset</tt>] Raise an error if the current IO offset doesn't # meet this criteria. A boolean return indicates # success or failure. Any other return is compared # to the current offset. The variable +offset+ # is made available to any lambda assigned to @@ -33,98 +28,53 @@ # <tt>:check_offset</tt>, except that it will # adjust the IO offset instead of raising an error. class Base class << self - extend Parameters - # Define methods for: - # bindata_mandatory_parameters - # bindata_optional_parameters - # bindata_default_parameters - # bindata_mutually_exclusive_parameters - - define_x_parameters(:bindata_mandatory, []) do |array, args| - args.each { |arg| array << arg.to_sym } - array.uniq! + # Instantiates this class and reads from +io+, returning the newly + # created data object. + def read(io) + data = self.new + data.read(io) + data end - define_x_parameters(:bindata_optional, []) do |array, args| - args.each { |arg| array << arg.to_sym } - array.uniq! + def recursive? + # data objects do not self reference by default + false end - define_x_parameters(:bindata_default, {}) do |hash, args| - params = args.length > 0 ? args[0] : {} - hash.merge!(params) - end + AcceptedParameters.define_all_accessors(self, :internal) - define_x_parameters(:bindata_mutually_exclusive, []) do |array, args| - array << [args[0].to_sym, args[1].to_sym] + def accepted_internal_parameters + internal = AcceptedParameters.get(self, :internal) + internal.all end - # Returns a list of internal parameters that are accepted by this object - def internal_parameters - (bindata_mandatory_parameters + bindata_optional_parameters + - bindata_default_parameters.keys).uniq + def sanitize_parameters!(sanitizer, params) + internal = AcceptedParameters.get(self, :internal) + internal.sanitize_parameters!(sanitizer, params) end - # Ensures that +params+ is of the form expected by #initialize. - def sanitize_parameters!(sanitizer, params) - # replace :readwrite with :onlyif - if params.has_key?(:readwrite) - warn ":readwrite is deprecated. Replacing with :onlyif" - params[:onlyif] = params.delete(:readwrite) - end + #------------- + private - # add default parameters - bindata_default_parameters.each do |k,v| - params[k] = v unless params.has_key?(k) + def warn_replacement_parameter(params, bad_key, suggested_key) + if params.has_key?(bad_key) + warn ":#{bad_key} is not used with #{self}. " + + "You probably want to change this to :#{suggested_key}" end - - # ensure mandatory parameters exist - bindata_mandatory_parameters.each do |prm| - if not params.has_key?(prm) - raise ArgumentError, "parameter ':#{prm}' must be specified " + - "in #{self}" - end - end - - # ensure mutual exclusion - bindata_mutually_exclusive_parameters.each do |param1, param2| - if params.has_key?(param1) and params.has_key?(param2) - raise ArgumentError, "params #{param1} and #{param2} " + - "are mutually exclusive" - end - end end - # Can this data object self reference itself? - def recursive? - false + def register(name, class_to_register) + RegisteredClasses.register(name, class_to_register) end - - # Instantiates this class and reads from +io+. For single value objects - # just the value is returned, otherwise the newly created data object is - # returned. - def read(io) - data = self.new - data.read(io) - data.single_value? ? data.value : data - end - - # Registers the mapping of +name+ to +klass+. - def register(name, klass) - Registry.instance.register(name, klass) - end - private :register end - # Define the parameters we use in this class. - bindata_optional_parameters :check_offset, :adjust_offset - bindata_default_parameters :onlyif => true - bindata_mutually_exclusive_parameters :check_offset, :adjust_offset + optional_parameters :check_offset, :adjust_offset + mutually_exclusive_parameters :check_offset, :adjust_offset # Creates a new data object. # # +params+ is a hash containing symbol keys. Some params may # reference callable objects (methods or procs). +parent+ is the @@ -133,153 +83,166 @@ def initialize(params = {}, parent = nil) @params = Sanitizer.sanitize(self.class, params) @parent = parent end - # The parent data object. - attr_accessor :parent + attr_reader :parent - # Returns all the custom parameters supplied to this data object. - def parameters - @params.extra_parameters + # Returns the result of evaluating the parameter identified by +key+. + # +overrides+ is an optional +parameters+ like hash that allow the + # parameters given at object construction to be overridden. + # Returns nil if +key+ does not refer to any parameter. + def eval_parameter(key, overrides = {}) + LazyEvaluator.eval(self, get_parameter(key), overrides) end - # Reads data into this data object by calling #do_read then #done_read. + # Returns the parameter referenced by +key+. + # Use this method if you are sure the parameter is not to be evaluated. + # You most likely want #eval_parameter. + def get_parameter(key) + @params[key] + end + + # Returns whether +key+ exists in the +parameters+ hash. + def has_parameter?(key) + @params.has_key?(key) + end + + # Reads data into this data object. def read(io) io = BinData::IO.new(io) unless BinData::IO === io do_read(io) done_read self end - # Reads the value for this data from +io+. def do_read(io) - raise ArgumentError, "io must be a BinData::IO" unless BinData::IO === io - + check_or_adjust_offset(io) clear - check_offset(io) + _do_read(io) + end - if eval_param(:onlyif) - _do_read(io) - end + def done_read + _done_read end + protected :do_read, :done_read - # Writes the value for this data to +io+ by calling #do_write. + # Writes the value for this data to +io+. def write(io) io = BinData::IO.new(io) unless BinData::IO === io do_write(io) io.flush self end - - # Writes the value for this data to +io+. def do_write(io) - raise ArgumentError, "io must be a BinData::IO" unless BinData::IO === io - - if eval_param(:onlyif) - _do_write(io) - end + _do_write(io) end + protected :do_write - # Returns the number of bytes it will take to write this data by calling - # #do_num_bytes. + # Returns the number of bytes it will take to write this data. def num_bytes(what = nil) num = do_num_bytes(what) num.ceil end - # Returns the number of bytes it will take to write this data. def do_num_bytes(what = nil) - if eval_param(:onlyif) - _do_num_bytes(what) - else - 0 - end + _do_num_bytes(what) end + protected :do_num_bytes + # Assigns the value of +val+ to this data object. Note that +val+ will + # always be deep copied to ensure no aliasing problems can occur. + def assign(val) + _assign(val) + end + # Returns a snapshot of this data object. - # Returns nil if :onlyif is false def snapshot - if eval_param(:onlyif) - _snapshot - else - nil - end + _snapshot end # Returns the string representation of this data object. - def to_s + def to_binary_s io = StringIO.new write(io) io.rewind io.read end - # Return a human readable representation of this object. + # Return a human readable representation of this data object. def inspect snapshot.inspect end - # Returns the object this object represents. - def obj - self + # Return a string representing this data object. + def to_s + snapshot.to_s end - #--------------- - private + # Returns a user friendly name of this object for debugging purposes. + def debug_name + if parent + parent.debug_name_of(self) + else + "obj" + end + end - # Returns the value of the evaluated parameter. +key+ references a - # parameter from the +params+ hash used when creating the data object. - # +values+ contains data that may be accessed when evaluating +key+. - # Returns nil if +key+ does not refer to any parameter. - def eval_param(key, values = nil) - LazyEvaluator.eval(no_eval_param(key), self, values) + # Returns the offset of this object wrt to its most distant ancestor. + def offset + if parent + parent.offset_of(self) + else + 0 + end end - # Returns the parameter from the +params+ hash referenced by +key+. - # Use this method if you are sure the parameter is not to be evaluated. - # You most likely want #eval_param. - def no_eval_param(key) - @params.internal_parameters[key] + def ==(other) + # double dispatch + other == snapshot end - # Returns whether +key+ exists in the +params+ hash used when creating - # this data object. - def has_param?(key) - @params.internal_parameters.has_key?(key) + #--------------- + private + + def check_or_adjust_offset(io) + if has_parameter?(:check_offset) + check_offset(io) + elsif has_parameter?(:adjust_offset) + adjust_offset(io) + end end - # Checks that the current offset of +io+ is as expected. This should - # be called from #do_read before performing the reading. def check_offset(io) - if has_param?(:check_offset) - actual_offset = io.offset - expected = eval_param(:check_offset, :offset => actual_offset) + actual_offset = io.offset + expected = eval_parameter(:check_offset, :offset => actual_offset) - if not expected - raise ValidityError, "offset not as expected" - elsif actual_offset != expected and expected != true - raise ValidityError, "offset is '#{actual_offset}' but " + - "expected '#{expected}'" + if not expected + raise ValidityError, "offset not as expected for #{debug_name}" + elsif actual_offset != expected and expected != true + raise ValidityError, + "offset is '#{actual_offset}' but " + + "expected '#{expected}' for #{debug_name}" + end + end + + def adjust_offset(io) + actual_offset = io.offset + expected = eval_parameter(:adjust_offset) + if actual_offset != expected + begin + seek = expected - actual_offset + io.seekbytes(seek) + warn "adjusting stream position by #{seek} bytes" if $VERBOSE + rescue + raise ValidityError, + "offset is '#{actual_offset}' but couldn't seek to " + + "expected '#{expected}' for #{debug_name}" end - elsif has_param?(:adjust_offset) - actual_offset = io.offset - expected = eval_param(:adjust_offset) - if actual_offset != expected - begin - seek = expected - actual_offset - io.seekbytes(seek) - warn "adjusting stream position by #{seek} bytes" if $VERBOSE - rescue - # could not seek so raise an error - raise ValidityError, "offset is '#{actual_offset}' but " + - "couldn't seek to expected '#{expected}'" - end - end end end ########################################################################### # To be implemented by subclasses @@ -292,43 +255,61 @@ # Returns true if the object has not been changed since creation. def clear?(*args) raise NotImplementedError end - # Returns whether this data object contains a single value. Single - # value data objects respond to <tt>#value</tt> and <tt>#value=</tt>. - def single_value? + # Returns the debug name of +child+. This only needs to be implemented + # by objects that contain child objects. + def debug_name_of(child) raise NotImplementedError end - # To be called after calling #do_read. - def done_read + # Returns the offset of +child+. This only needs to be implemented + # by objects that contain child objects. + def offset_of(child) raise NotImplementedError end # Reads the data for this data object from +io+. def _do_read(io) raise NotImplementedError end + # Trigger function that is called after #do_read. + def _done_read + raise NotImplementedError + end + # Writes the value for this data to +io+. def _do_write(io) raise NotImplementedError end # Returns the number of bytes it will take to write this data. - def _do_num_bytes + def _do_num_bytes(what) raise NotImplementedError end + # Assigns the value of +val+ to this data object. Note that +val+ will + # always be deep copied to ensure no aliasing problems can occur. + def _assign(val) + raise NotImplementedError + end + # Returns a snapshot of this data object. def _snapshot raise NotImplementedError end # Set visibility requirements of methods to implement - public :clear, :clear?, :single_value?, :done_read - private :_do_read, :_do_write, :_do_num_bytes, :_snapshot + public :clear, :clear?, :debug_name_of, :offset_of + private :_do_read, :_done_read, :_do_write, :_do_num_bytes, :_assign, :_snapshot + +def single_value? + warn "#single_value? is deprecated. It should no longer be needed" + false +end +public :single_value? # End To be implemented by subclasses ########################################################################### end end