bindata 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bindata might be problematic. Click here for more details.

data/lib/bindata/lazy.rb CHANGED
@@ -1,110 +1,98 @@
1
1
  module BinData
2
- # The enviroment in which a lazily evaluated lamba is called. These lambdas
3
- # are those that are passed to data objects as parameters. Each lambda
4
- # has access to the following:
2
+ # A LazyEvaluator is bound to a data object. The evaluator will evaluate
3
+ # lambdas in the context of this data object. These lambdas
4
+ # are those that are passed to data objects as parameters, e.g.:
5
5
  #
6
- # parent:: the environment of the parent data object
7
- # params:: any extra parameters that have been passed to the data object.
8
- # The value of a parameter is either a lambda, a symbol or a
9
- # literal value (such as a Fixnum).
6
+ # BinData::String.new(:value => lambda { %w{a test message}.join(" ") })
10
7
  #
11
- # Unknown methods are resolved in the context of the parent environment,
12
- # first as keys in the extra parameters, and secondly as methods in the
13
- # parent data object. This makes the lambda easier to read as we just write
8
+ # When evaluating lambdas, unknown methods are resolved in the context of
9
+ # the parent of the bound data object, first as keys in #parameters, and
10
+ # secondly as methods in this parent. This resolution propagates up the
11
+ # chain of parent data objects.
12
+ #
13
+ # This resolution process makes the lambda easier to read as we just write
14
14
  # <tt>field</tt> instead of <tt>obj.field</tt>.
15
- class LazyEvalEnv
15
+ class LazyEvaluator
16
+ class << self
17
+ # Lazily evaluates +val+ in the context of +obj+, with possibility of
18
+ # +overrides+.
19
+ def eval(val, obj, overrides = nil)
20
+ env = self.new(obj)
21
+ env.lazy_eval(val, overrides)
22
+ end
23
+ end
24
+
16
25
  # An empty hash shared by all instances
17
26
  @@empty_hash = Hash.new.freeze
18
27
 
19
- @@variables_cache = {}
20
-
21
- # Creates a new environment. +parent+ is the environment of the
22
- # parent data object.
23
- def initialize(parent = nil)
24
- @parent = parent
25
- @variables = @@empty_hash
28
+ # Creates a new evaluator. All lazy evaluation is performed in the
29
+ # context of +obj+.
30
+ def initialize(obj)
31
+ @obj = obj
26
32
  @overrides = @@empty_hash
27
- @params = @@empty_hash
28
33
  end
29
- attr_reader :parent, :params
30
- attr_accessor :data_object
31
34
 
32
- # only accessible by another LazyEvalEnv
33
- protected :data_object
34
-
35
- # Set the parameters for this environment.
36
- def params=(p)
37
- @params = (p.nil? or p.empty?) ? @@empty_hash : p
38
- end
39
-
40
- # Add a variable with a pre-assigned value to this environment. +sym+
41
- # will be accessible as a variable for any lambda evaluated
42
- # with #lazy_eval.
43
- def add_variable(sym, value)
44
- sym = sym.to_sym
45
- if @variables.equal?(@@empty_hash)
46
- # optimise the case where only 1 variable is added as this
47
- # is the most common occurance (BinData::Arrays adding index)
48
- key = [sym, value]
49
- @variables = @@variables_cache[key]
50
- if @variables.nil?
51
- # cache this variable and value so it can be shared with
52
- # other LazyEvalEnvs to keep memory usage down
53
- @variables = {sym => value}.freeze
54
- @@variables_cache[key] = @variables
55
- end
35
+ # Returns a LazyEvaluator for the parent of this data object.
36
+ def parent
37
+ if @obj.parent
38
+ LazyEvaluator.new(@obj.parent)
56
39
  else
57
- if @variables.length == 1
58
- key = @variables.keys[0]
59
- @variables = {key => @variables[key]}
60
- end
61
- @variables[sym] = value
40
+ nil
62
41
  end
63
42
  end
64
43
 
65
- # TODO: offset_of needs to be better thought out
66
- def offset_of(sym)
67
- @parent.data_object.offset_of(sym)
68
- rescue
69
- nil
70
- end
71
-
72
- # Returns the data_object for the parent environment.
73
- def parent_data_object
74
- @parent.nil? ? nil : @parent.data_object
75
- end
76
-
77
- # Evaluates +obj+ in the context of this environment. Evaluation
44
+ # Evaluates +val+ in the context of this data object. Evaluation
78
45
  # recurses until it yields a value that is not a symbol or lambda.
79
- # +overrides+ is an optional +params+ like hash
80
- def lazy_eval(obj, overrides = nil)
46
+ # +overrides+ is an optional +obj.parameters+ like hash.
47
+ def lazy_eval(val, overrides = nil)
48
+ result = val
81
49
  @overrides = overrides if overrides
82
- if obj.is_a? Symbol
50
+ if val.is_a? Symbol
83
51
  # treat :foo as lambda { foo }
84
- obj = __send__(obj)
85
- elsif obj.respond_to? :arity
86
- obj = instance_eval(&obj)
52
+ result = __send__(val)
53
+ elsif val.respond_to? :arity
54
+ result = instance_eval(&val)
87
55
  end
88
56
  @overrides = @@empty_hash
89
- obj
57
+ result
90
58
  end
91
59
 
92
60
  def method_missing(symbol, *args)
93
61
  if @overrides.include?(symbol)
94
62
  @overrides[symbol]
95
- elsif @variables.include?(symbol)
96
- @variables[symbol]
97
- elsif @parent
98
- obj = symbol
99
- if @parent.params and @parent.params.has_key?(symbol)
100
- obj = @parent.params[symbol]
101
- elsif @parent.data_object and @parent.data_object.respond_to?(symbol)
102
- obj = @parent.data_object.__send__(symbol, *args)
63
+ elsif symbol == :index
64
+ array_index
65
+ elsif @obj.parent
66
+ val = symbol
67
+ if @obj.parent.parameters and @obj.parent.parameters.has_key?(symbol)
68
+ val = @obj.parent.parameters[symbol]
69
+ elsif @obj.parent and @obj.parent.respond_to?(symbol)
70
+ val = @obj.parent.__send__(symbol, *args)
103
71
  end
104
- @parent.lazy_eval(obj)
72
+ LazyEvaluator.eval(val, @obj.parent)
105
73
  else
106
74
  super
107
75
  end
108
76
  end
77
+
78
+ #---------------
79
+ private
80
+
81
+ # Returns the index in the closest ancestor array of this data object.
82
+ def array_index
83
+ bindata_array_klass = BinData.const_defined?("Array") ?
84
+ BinData.const_get("Array") : nil
85
+ child = @obj
86
+ parent = @obj.parent
87
+ while parent
88
+ if parent.class == bindata_array_klass
89
+ return parent.index(child)
90
+ end
91
+ child = parent
92
+ parent = parent.parent
93
+ end
94
+ raise NoMethodError, "no index found"
95
+ end
96
+
109
97
  end
110
98
  end
@@ -1,3 +1,4 @@
1
+ require 'bindata/params'
1
2
  require 'bindata/struct'
2
3
 
3
4
  module BinData
@@ -44,11 +45,18 @@ module BinData
44
45
  class MultiValue < BinData::Struct
45
46
 
46
47
  class << self
48
+ extend Parameters
49
+
47
50
  # Register the names of all subclasses of this class.
48
51
  def inherited(subclass) #:nodoc:
49
52
  register(subclass.name, subclass)
50
53
  end
51
54
 
55
+ # Can this data object self reference itself?
56
+ def recursive?
57
+ true
58
+ end
59
+
52
60
  # Returns or sets the endianess of numerics used in this stucture.
53
61
  # Endianess is applied to the fields of this structure.
54
62
  # Valid values are :little and :big.
@@ -117,11 +125,8 @@ module BinData
117
125
  @fields.push([type, name, params])
118
126
  end
119
127
 
120
- # Returns a sanitized +params+ that is of the form expected
121
- # by #initialize.
122
- def sanitize_parameters(sanitizer, params)
123
- params = params.dup
124
-
128
+ # Ensures that +params+ is of the form expected by #initialize.
129
+ def sanitize_parameters!(sanitizer, params)
125
130
  endian = params[:endian] || self.endian
126
131
  fields = params[:fields] || self.fields
127
132
  hide = params[:hide] || self.hide
@@ -130,8 +135,37 @@ module BinData
130
135
  params[:fields] = fields
131
136
  params[:hide] = hide
132
137
 
138
+ # add default parameters
139
+ default_parameters.each do |k,v|
140
+ params[k] = v unless params.has_key?(k)
141
+ end
142
+
143
+ # ensure mandatory parameters exist
144
+ mandatory_parameters.each do |prm|
145
+ if not params.has_key?(prm)
146
+ raise ArgumentError, "parameter ':#{prm}' must be specified " +
147
+ "in #{self}"
148
+ end
149
+ end
150
+
133
151
  super(sanitizer, params)
134
152
  end
153
+
154
+ # Sets the mandatory parameters used by this class.
155
+ def mandatory_parameters(*args) ; end
156
+
157
+ define_x_parameters(:mandatory, []) do |array, args|
158
+ args.each { |arg| array << arg.to_sym }
159
+ array.uniq!
160
+ end
161
+
162
+ # Sets the default parameters used by this class.
163
+ def default_parameters(params = {}); end
164
+
165
+ define_x_parameters(:default, {}) do |hash, args|
166
+ params = args.length > 0 ? args[0] : {}
167
+ hash.merge!(params)
168
+ end
135
169
  end
136
170
  end
137
171
  end
@@ -0,0 +1,36 @@
1
+ module BinData
2
+ module Parameters
3
+
4
+ # Definition method for creating a method that maintains a collection
5
+ # of parameters. This is used for creating collections of mandatory,
6
+ # optional, default etc parameters. The parameters for a class will
7
+ # include the parameters of its ancestors.
8
+ def define_x_parameters(name, empty, &block) #:nodoc:
9
+ full_name_singular = "#{name.to_s}_parameter"
10
+ full_name_plural = "#{name.to_s}_parameters"
11
+
12
+ sym = full_name_plural.to_sym
13
+ iv = "@#{full_name_plural}".to_sym
14
+
15
+ body = Proc.new do |*args|
16
+ # initialize collection to duplicate ancestor's collection
17
+ unless instance_variable_defined?(iv)
18
+ ancestor = ancestors[1..-1].find { |a| a.respond_to?(sym) }
19
+ val = ancestor.nil? ? empty : ancestor.send(sym).dup
20
+ instance_variable_set(iv, val)
21
+ end
22
+
23
+ # add new parameters to the collection
24
+ if not args.empty?
25
+ block.call(instance_variable_get(iv), args)
26
+ end
27
+
28
+ # return collection
29
+ instance_variable_get(iv)
30
+ end
31
+
32
+ define_method(sym, body)
33
+ alias_method(full_name_singular.to_sym, sym)
34
+ end
35
+ end
36
+ end
@@ -23,15 +23,15 @@ module BinData
23
23
  # Returns the converted name
24
24
  def register(name, klass)
25
25
  # convert camelCase name to underscore style
26
- name = underscore_name(name)
26
+ key = underscore_name(name)
27
27
 
28
28
  # warn if replacing an existing class
29
- if $VERBOSE and (existing = @registry[name])
29
+ if $VERBOSE and (existing = @registry[key])
30
30
  warn "warning: replacing registered class #{existing} with #{klass}"
31
31
  end
32
32
 
33
- @registry[name] = klass
34
- name.dup
33
+ @registry[key] = klass
34
+ key.dup
35
35
  end
36
36
 
37
37
  # Returns the class matching a previously registered +name+.
@@ -1,14 +1,56 @@
1
1
  require 'forwardable'
2
2
 
3
3
  module BinData
4
+
5
+ # A BinData object accepts arbitrary parameters. This class only contains
6
+ # parameters that have been sanitized, and categorizes them according to
7
+ # whether they are BinData::Base.internal_parameters or are extra.
8
+ class SanitizedParameters
9
+ extend Forwardable
10
+
11
+ # Sanitize the given parameters.
12
+ def initialize(klass, params)
13
+ @hash = params
14
+ @internal_parameters = {}
15
+ @extra_parameters = {}
16
+
17
+ # partition parameters into known and extra parameters
18
+ @hash.each do |k,v|
19
+ k = k.to_sym
20
+ if v.nil?
21
+ raise ArgumentError, "parameter :#{k} has nil value in #{klass}"
22
+ end
23
+
24
+ if klass.internal_parameters.include?(k)
25
+ @internal_parameters[k] = v
26
+ else
27
+ @extra_parameters[k] = v
28
+ end
29
+ end
30
+ end
31
+
32
+ attr_reader :internal_parameters, :extra_parameters
33
+
34
+ def_delegators :@hash, :[], :has_key?, :include?, :keys
35
+ end
36
+
37
+ # The Sanitizer sanitizes the parameters that are passed when creating a
38
+ # BinData object. Sanitizing consists of checking for mandatory, optional
39
+ # and default parameters and ensuring the values of known parameters are
40
+ # valid.
4
41
  class Sanitizer
42
+
5
43
  class << self
44
+
6
45
  # Sanitize +params+ for +obj+.
7
46
  # Returns sanitized parameters.
8
- def sanitize(obj, params)
9
- sanitizer = self.new
10
- klass, new_params = sanitizer.sanitize(obj.class, params)
11
- new_params
47
+ def sanitize(klass, params)
48
+ if SanitizedParameters === params
49
+ params
50
+ else
51
+ sanitizer = self.new
52
+ sanitizer.sanitize_params(klass, params)
53
+ end
12
54
  end
13
55
 
14
56
  # Returns true if +type+ is registered.
@@ -52,70 +94,44 @@ module BinData
52
94
  end
53
95
  end
54
96
 
55
- # Sanitizes +params+ for +type+.
56
- # Returns [klass, sanitized_params]
57
- def sanitize(type, params)
58
- if Class === type
59
- klass = type
60
- else
61
- klass = self.class.lookup(type, @endian)
62
- raise TypeError, "unknown type '#{type}'" if klass.nil?
63
- end
97
+ # Converts +type+ into the appropriate class.
98
+ def lookup_klass(type)
99
+ klass = self.class.lookup(type, @endian)
100
+ raise TypeError, "unknown type '#{type}'" if klass.nil?
101
+ klass
102
+ end
103
+
104
+ # Sanitizes +params+ for +klass+.
105
+ # Returns +sanitized_params+.
106
+ def sanitize_params(klass, params)
107
+ new_params = params.nil? ? {} : params.dup
64
108
 
65
- params ||= {}
66
- if @seen.include?(klass)
109
+ if klass.recursive? and @seen.include?(klass)
67
110
  # This klass is defined recursively. Remember the current endian
68
111
  # and delay sanitizing the parameters until later.
69
- if @endian != nil and klass.accepted_parameters.include?(:endian) and
70
- not params.has_key?(:endian)
71
- params = params.dup
72
- params[:endian] = @endian
73
- end
112
+ new_params[:endian] = @endian if can_store_endian?(klass, new_params)
113
+ ret_val = new_params
74
114
  else
75
- # subclasses of MultiValue may be defined recursively
76
- # TODO: define a class field instead
77
- possibly_recursive = (BinData.const_defined?(:MultiValue) and
78
- klass.ancestors.include?(BinData.const_get(:MultiValue)))
79
- @seen.push klass if possibly_recursive
80
-
81
- new_params = klass.sanitize_parameters(self, params)
82
- params = SanitizedParameters.new(klass, new_params)
83
- end
115
+ @seen.push(klass)
84
116
 
85
- [klass, params]
86
- end
87
- end
117
+ # Sanitize new_params. This may recursively call this method again.
118
+ klass.sanitize_parameters!(self, new_params)
119
+ ret_val = SanitizedParameters.new(klass, new_params)
88
120
 
89
- # A BinData object accepts arbitrary parameters. This class ensures that
90
- # the parameters have been sanitized, and categorizes them according to
91
- # whether they are BinData::Base.accepted_parameters or are extra.
92
- class SanitizedParameters
93
- extend Forwardable
94
-
95
- # Sanitize the given parameters.
96
- def initialize(klass, params)
97
- @hash = params
98
- @accepted_parameters = {}
99
- @extra_parameters = {}
100
-
101
- # partition parameters into known and extra parameters
102
- @hash.each do |k,v|
103
- k = k.to_sym
104
- if v.nil?
105
- raise ArgumentError, "parameter :#{k} has nil value in #{klass}"
106
- end
107
-
108
- if klass.accepted_parameters.include?(k)
109
- @accepted_parameters[k] = v
110
- else
111
- @extra_parameters[k] = v
112
- end
121
+ @seen.pop
113
122
  end
123
+
124
+ ret_val
114
125
  end
115
126
 
116
- attr_reader :accepted_parameters, :extra_parameters
127
+ #---------------
128
+ private
117
129
 
118
- def_delegators :@hash, :[], :has_key?, :include?, :keys
130
+ # Can we store the current endian for later?
131
+ def can_store_endian?(klass, params)
132
+ (@endian != nil and klass.internal_parameters.include?(:endian) and
133
+ not params.has_key?(:endian))
134
+ end
119
135
  end
120
136
  end
121
137