jbangert-bindata 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/BSDL +22 -0
- data/COPYING +52 -0
- data/ChangeLog.rdoc +204 -0
- data/Gemfile +2 -0
- data/INSTALL +11 -0
- data/NEWS.rdoc +164 -0
- data/README.md +54 -0
- data/Rakefile +13 -0
- data/bindata.gemspec +31 -0
- data/doc/manual.haml +407 -0
- data/doc/manual.md +1649 -0
- data/examples/NBT.txt +149 -0
- data/examples/gzip.rb +161 -0
- data/examples/ip_address.rb +22 -0
- data/examples/list.rb +124 -0
- data/examples/nbt.rb +178 -0
- data/lib/bindata.rb +33 -0
- data/lib/bindata/alignment.rb +83 -0
- data/lib/bindata/array.rb +335 -0
- data/lib/bindata/base.rb +388 -0
- data/lib/bindata/base_primitive.rb +214 -0
- data/lib/bindata/bits.rb +87 -0
- data/lib/bindata/choice.rb +216 -0
- data/lib/bindata/count_bytes_remaining.rb +35 -0
- data/lib/bindata/deprecated.rb +50 -0
- data/lib/bindata/dsl.rb +312 -0
- data/lib/bindata/float.rb +80 -0
- data/lib/bindata/int.rb +184 -0
- data/lib/bindata/io.rb +274 -0
- data/lib/bindata/lazy.rb +105 -0
- data/lib/bindata/offset.rb +91 -0
- data/lib/bindata/params.rb +135 -0
- data/lib/bindata/primitive.rb +135 -0
- data/lib/bindata/record.rb +110 -0
- data/lib/bindata/registry.rb +92 -0
- data/lib/bindata/rest.rb +35 -0
- data/lib/bindata/sanitize.rb +290 -0
- data/lib/bindata/skip.rb +48 -0
- data/lib/bindata/string.rb +145 -0
- data/lib/bindata/stringz.rb +96 -0
- data/lib/bindata/struct.rb +388 -0
- data/lib/bindata/trace.rb +94 -0
- data/lib/bindata/version.rb +3 -0
- data/setup.rb +1585 -0
- data/spec/alignment_spec.rb +61 -0
- data/spec/array_spec.rb +331 -0
- data/spec/base_primitive_spec.rb +238 -0
- data/spec/base_spec.rb +376 -0
- data/spec/bits_spec.rb +163 -0
- data/spec/choice_spec.rb +263 -0
- data/spec/count_bytes_remaining_spec.rb +38 -0
- data/spec/deprecated_spec.rb +31 -0
- data/spec/example.rb +21 -0
- data/spec/float_spec.rb +37 -0
- data/spec/int_spec.rb +216 -0
- data/spec/io_spec.rb +352 -0
- data/spec/lazy_spec.rb +217 -0
- data/spec/primitive_spec.rb +202 -0
- data/spec/record_spec.rb +530 -0
- data/spec/registry_spec.rb +108 -0
- data/spec/rest_spec.rb +26 -0
- data/spec/skip_spec.rb +27 -0
- data/spec/spec_common.rb +58 -0
- data/spec/string_spec.rb +300 -0
- data/spec/stringz_spec.rb +118 -0
- data/spec/struct_spec.rb +350 -0
- data/spec/system_spec.rb +380 -0
- data/tasks/manual.rake +36 -0
- data/tasks/rspec.rake +17 -0
- metadata +208 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'bindata/dsl'
|
2
|
+
require 'bindata/sanitize'
|
3
|
+
require 'bindata/struct'
|
4
|
+
|
5
|
+
module BinData
|
6
|
+
# Extracts args for Records.
|
7
|
+
#
|
8
|
+
# Foo.new(:bar => "baz) is ambiguous as to whether :bar is a value or parameter.
|
9
|
+
#
|
10
|
+
# BaseArgExtractor always assumes :bar is parameter. This extractor correctly
|
11
|
+
# identifies it as value or parameter.
|
12
|
+
class RecordArgExtractor
|
13
|
+
class << self
|
14
|
+
def extract(the_class, the_args)
|
15
|
+
value, parameters, parent = BaseArgExtractor.extract(the_class, the_args)
|
16
|
+
|
17
|
+
if parameters_is_value?(the_class, value, parameters)
|
18
|
+
value = parameters
|
19
|
+
parameters = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
[value, parameters, parent]
|
23
|
+
end
|
24
|
+
|
25
|
+
def parameters_is_value?(the_class, value, parameters)
|
26
|
+
if value.nil? and parameters.length > 0
|
27
|
+
field_names_in_parameters?(the_class, parameters)
|
28
|
+
else
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def field_names_in_parameters?(the_class, parameters)
|
34
|
+
field_names = the_class.fields.field_names
|
35
|
+
param_keys = parameters.keys
|
36
|
+
|
37
|
+
(field_names & param_keys).length > 0
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# A Record is a declarative wrapper around Struct.
|
43
|
+
#
|
44
|
+
# require 'bindata'
|
45
|
+
#
|
46
|
+
# class SomeDataType < BinData::Record
|
47
|
+
# hide :a
|
48
|
+
#
|
49
|
+
# int32le :a
|
50
|
+
# int16le :b
|
51
|
+
# struct :s do
|
52
|
+
# int8 :x
|
53
|
+
# int8 :y
|
54
|
+
# int8 :z
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# obj = SomeDataType.new
|
59
|
+
# obj.field_names =># ["b", "s"]
|
60
|
+
# obj.s.field_names =># ["x", "y", "z"]
|
61
|
+
#
|
62
|
+
class Record < BinData::Struct
|
63
|
+
include DSLMixin
|
64
|
+
|
65
|
+
unregister_self
|
66
|
+
dsl_parser :struct
|
67
|
+
|
68
|
+
class << self
|
69
|
+
|
70
|
+
def arg_extractor
|
71
|
+
RecordArgExtractor
|
72
|
+
end
|
73
|
+
|
74
|
+
def sanitize_parameters!(params) #:nodoc:
|
75
|
+
params.merge!(dsl_params)
|
76
|
+
|
77
|
+
super(params)
|
78
|
+
|
79
|
+
define_field_accessors(params[:fields].fields)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Defines accessor methods to avoid the overhead of going through
|
83
|
+
# Struct#method_missing. This is purely a speed optimisation.
|
84
|
+
# Removing this method will not have any effect on correctness.
|
85
|
+
def define_field_accessors(fields) #:nodoc:
|
86
|
+
unless method_defined?(:bindata_defined_accessors_for_fields?)
|
87
|
+
fields.each_with_index do |field, i|
|
88
|
+
name = field.name_as_sym
|
89
|
+
if name
|
90
|
+
define_field_accessors_for(name, i)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
define_method(:bindata_defined_accessors_for_fields?) { true }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def define_field_accessors_for(name, index)
|
99
|
+
define_method(name) do
|
100
|
+
instantiate_obj_at(index) unless @field_objs[index]
|
101
|
+
@field_objs[index]
|
102
|
+
end
|
103
|
+
define_method(name.to_s + "=") do |*vals|
|
104
|
+
instantiate_obj_at(index) unless @field_objs[index]
|
105
|
+
@field_objs[index].assign(*vals)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module BinData
|
2
|
+
|
3
|
+
class UnRegisteredTypeError < StandardError ; end
|
4
|
+
|
5
|
+
# This registry contains a register of name -> class mappings.
|
6
|
+
#
|
7
|
+
# Numerics (integers and floating point numbers) have an endian property as
|
8
|
+
# part of their name (e.g. int32be, float_le). The lookup can either be
|
9
|
+
# on the full name, or on the shortened name plus endian (e.g. "int32", :big)
|
10
|
+
#
|
11
|
+
# Names are stored in under_score_style, not camelCase.
|
12
|
+
class Registry
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@registry = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def register(name, class_to_register)
|
19
|
+
return if class_to_register.nil?
|
20
|
+
|
21
|
+
formatted_name = lookup_key(name)
|
22
|
+
warn_if_name_is_already_registered(formatted_name, class_to_register)
|
23
|
+
|
24
|
+
@registry[formatted_name] = class_to_register
|
25
|
+
end
|
26
|
+
|
27
|
+
def unregister(name)
|
28
|
+
formatted_name = lookup_key(name)
|
29
|
+
@registry.delete(formatted_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def lookup(name, endian = nil)
|
33
|
+
key = lookup_key(name, endian)
|
34
|
+
try_registering_key(key) unless @registry.has_key?(key)
|
35
|
+
|
36
|
+
@registry[key] || raise(UnRegisteredTypeError, name.to_s)
|
37
|
+
end
|
38
|
+
|
39
|
+
def normalize_name(name, endian = nil)
|
40
|
+
if lookup(name, endian)
|
41
|
+
lookup_key(name, endian)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Convert CamelCase +name+ to underscore style.
|
46
|
+
def underscore_name(name)
|
47
|
+
name.to_s.sub(/.*::/, "").
|
48
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
49
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
50
|
+
tr("-", "_").
|
51
|
+
downcase
|
52
|
+
end
|
53
|
+
|
54
|
+
#---------------
|
55
|
+
private
|
56
|
+
|
57
|
+
def lookup_key(name, endian = nil)
|
58
|
+
name = underscore_name(name)
|
59
|
+
|
60
|
+
result = name
|
61
|
+
if endian != nil
|
62
|
+
if /^u?int\d+$/ =~ name
|
63
|
+
result = name + ((endian == :little) ? "le" : "be")
|
64
|
+
elsif /^(float|double)$/ =~ name
|
65
|
+
result = name + ((endian == :little) ? "_le" : "_be")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
result
|
69
|
+
end
|
70
|
+
|
71
|
+
def try_registering_key(key)
|
72
|
+
if /^u?int\d+(le|be)$/ =~ key or /^bit\d+(le)?$/ =~ key
|
73
|
+
class_name = key.gsub(/(?:^|_)(.)/) { $1.upcase }
|
74
|
+
begin
|
75
|
+
register(key, BinData::const_get(class_name))
|
76
|
+
rescue NameError
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def warn_if_name_is_already_registered(name, class_to_register)
|
82
|
+
prev_class = @registry[name]
|
83
|
+
if $VERBOSE and prev_class and prev_class != class_to_register
|
84
|
+
warn "warning: replacing registered class #{prev_class} " +
|
85
|
+
"with #{class_to_register}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# A singleton registry of all registered classes.
|
91
|
+
RegisteredClasses = Registry.new
|
92
|
+
end
|
data/lib/bindata/rest.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require "bindata/base_primitive"
|
2
|
+
|
3
|
+
module BinData
|
4
|
+
# Rest will consume the input stream from the current position to the end of
|
5
|
+
# the stream. This will mainly be useful for debugging and developing.
|
6
|
+
#
|
7
|
+
# require 'bindata'
|
8
|
+
#
|
9
|
+
# class A < BinData::Record
|
10
|
+
# string :a, :read_length => 5
|
11
|
+
# rest :rest
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# obj = A.read("abcdefghij")
|
15
|
+
# obj.a #=> "abcde"
|
16
|
+
# obj.rest #=" "fghij"
|
17
|
+
#
|
18
|
+
class Rest < BinData::BasePrimitive
|
19
|
+
|
20
|
+
#---------------
|
21
|
+
private
|
22
|
+
|
23
|
+
def value_to_binary_string(val)
|
24
|
+
val
|
25
|
+
end
|
26
|
+
|
27
|
+
def read_and_return_value(io)
|
28
|
+
io.read_all_bytes
|
29
|
+
end
|
30
|
+
|
31
|
+
def sensible_default
|
32
|
+
""
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,290 @@
|
|
1
|
+
require 'bindata/registry'
|
2
|
+
|
3
|
+
module BinData
|
4
|
+
|
5
|
+
# Subclasses of this are sanitized
|
6
|
+
class SanitizedParameter; end
|
7
|
+
|
8
|
+
class SanitizedPrototype < SanitizedParameter
|
9
|
+
def initialize(obj_type, obj_params, endian)
|
10
|
+
endian = endian.endian if endian.respond_to? :endian
|
11
|
+
obj_params ||= {}
|
12
|
+
if BinData::Base === obj_type or obj_type.is_a?(Class)
|
13
|
+
obj_class = obj_type
|
14
|
+
else
|
15
|
+
obj_class = RegisteredClasses.lookup(obj_type, endian)
|
16
|
+
end
|
17
|
+
|
18
|
+
if BinData::Base === obj_class
|
19
|
+
@factory = obj_class
|
20
|
+
else
|
21
|
+
@obj_class = obj_class
|
22
|
+
@obj_params = SanitizedParameters.new(obj_params, @obj_class, endian)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def instantiate(value = nil, parent = nil)
|
27
|
+
@factory ||= @obj_class.new(@obj_params)
|
28
|
+
|
29
|
+
@factory.new(value, parent)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
#----------------------------------------------------------------------------
|
33
|
+
|
34
|
+
class SanitizedField < SanitizedParameter
|
35
|
+
def initialize(name, field_type, field_params, endian)
|
36
|
+
@name = name
|
37
|
+
@prototype = SanitizedPrototype.new(field_type, field_params, endian)
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :prototype
|
41
|
+
|
42
|
+
def name_as_sym
|
43
|
+
@name.nil? ? nil : @name.to_sym
|
44
|
+
end
|
45
|
+
|
46
|
+
def name
|
47
|
+
@name
|
48
|
+
end
|
49
|
+
|
50
|
+
def instantiate(value = nil, parent = nil)
|
51
|
+
@prototype.instantiate(value, parent)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
#----------------------------------------------------------------------------
|
55
|
+
|
56
|
+
class SanitizedFields < SanitizedParameter
|
57
|
+
def initialize(endian)
|
58
|
+
@fields = []
|
59
|
+
@endian = endian
|
60
|
+
end
|
61
|
+
attr_reader :fields
|
62
|
+
|
63
|
+
def add_field(type, name, params)
|
64
|
+
name = nil if name == ""
|
65
|
+
|
66
|
+
@fields << SanitizedField.new(name, type, params, @endian)
|
67
|
+
end
|
68
|
+
|
69
|
+
def [](idx)
|
70
|
+
@fields[idx]
|
71
|
+
end
|
72
|
+
|
73
|
+
def empty?
|
74
|
+
@fields.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
def length
|
78
|
+
@fields.length
|
79
|
+
end
|
80
|
+
|
81
|
+
def each(&block)
|
82
|
+
@fields.each(&block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def collect(&block)
|
86
|
+
@fields.collect(&block)
|
87
|
+
end
|
88
|
+
|
89
|
+
def field_names
|
90
|
+
@fields.collect { |field| field.name_as_sym }
|
91
|
+
end
|
92
|
+
|
93
|
+
def has_field_name?(name)
|
94
|
+
@fields.detect { |f| f.name_as_sym == name.to_sym }
|
95
|
+
end
|
96
|
+
|
97
|
+
def all_field_names_blank?
|
98
|
+
@fields.all? { |f| f.name == nil }
|
99
|
+
end
|
100
|
+
|
101
|
+
def no_field_names_blank?
|
102
|
+
@fields.all? { |f| f.name != nil }
|
103
|
+
end
|
104
|
+
|
105
|
+
def copy_fields(other)
|
106
|
+
@fields.concat(other.fields)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
#----------------------------------------------------------------------------
|
110
|
+
|
111
|
+
class SanitizedChoices < SanitizedParameter
|
112
|
+
def initialize(choices, endian)
|
113
|
+
@choices = {}
|
114
|
+
choices.each_pair do |key, val|
|
115
|
+
if SanitizedParameter === val
|
116
|
+
prototype = val
|
117
|
+
else
|
118
|
+
type, param = val
|
119
|
+
prototype = SanitizedPrototype.new(type, param, endian)
|
120
|
+
end
|
121
|
+
|
122
|
+
if key == :default
|
123
|
+
@choices.default = prototype
|
124
|
+
else
|
125
|
+
@choices[key] = prototype
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def [](key)
|
131
|
+
@choices[key]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
#----------------------------------------------------------------------------
|
135
|
+
|
136
|
+
class SanitizedBigEndian < SanitizedParameter
|
137
|
+
def endian
|
138
|
+
:big
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class SanitizedLittleEndian < SanitizedParameter
|
143
|
+
def endian
|
144
|
+
:little
|
145
|
+
end
|
146
|
+
end
|
147
|
+
#----------------------------------------------------------------------------
|
148
|
+
|
149
|
+
# BinData objects are instantiated with parameters to determine their
|
150
|
+
# behaviour. These parameters must be sanitized to ensure their values
|
151
|
+
# are valid. When instantiating many objects with identical parameters,
|
152
|
+
# such as an array of records, there is much duplicated sanitizing.
|
153
|
+
#
|
154
|
+
# The purpose of the sanitizing code is to eliminate the duplicated
|
155
|
+
# validation.
|
156
|
+
#
|
157
|
+
# SanitizedParameters is a hash-like collection of parameters. Its purpose
|
158
|
+
# is to recursively sanitize the parameters of an entire BinData object chain
|
159
|
+
# at a single time.
|
160
|
+
class SanitizedParameters < Hash
|
161
|
+
|
162
|
+
# Memoized constants
|
163
|
+
BIG_ENDIAN = SanitizedBigEndian.new
|
164
|
+
LITTLE_ENDIAN = SanitizedLittleEndian.new
|
165
|
+
|
166
|
+
class << self
|
167
|
+
def sanitize(parameters, the_class)
|
168
|
+
if SanitizedParameters === parameters
|
169
|
+
parameters
|
170
|
+
else
|
171
|
+
SanitizedParameters.new(parameters, the_class, nil)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def initialize(parameters, the_class, endian)
|
177
|
+
parameters.each_pair { |key, value| self[key.to_sym] = value }
|
178
|
+
|
179
|
+
@the_class = the_class
|
180
|
+
@endian = endian
|
181
|
+
|
182
|
+
sanitize!
|
183
|
+
end
|
184
|
+
|
185
|
+
alias_method :has_parameter?, :has_key?
|
186
|
+
|
187
|
+
def needs_sanitizing?(key)
|
188
|
+
parameter = self[key]
|
189
|
+
|
190
|
+
parameter and not parameter.is_a?(SanitizedParameter)
|
191
|
+
end
|
192
|
+
|
193
|
+
def warn_replacement_parameter(bad_key, suggested_key)
|
194
|
+
if has_parameter?(bad_key)
|
195
|
+
warn ":#{bad_key} is not used with #{@the_class}. " +
|
196
|
+
"You probably want to change this to :#{suggested_key}"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def warn_renamed_parameter(old_key, new_key)
|
201
|
+
val = delete(old_key)
|
202
|
+
if val
|
203
|
+
self[new_key] = val
|
204
|
+
warn ":#{old_key} has been renamed to :#{new_key} in #{@the_class}. " +
|
205
|
+
"Using :#{old_key} is now deprecated and will be removed in the future"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def endian
|
210
|
+
@endian || self[:endian]
|
211
|
+
end
|
212
|
+
attr_writer :endian
|
213
|
+
|
214
|
+
def create_sanitized_endian(endian)
|
215
|
+
if endian == :big
|
216
|
+
BIG_ENDIAN
|
217
|
+
elsif endian == :little
|
218
|
+
LITTLE_ENDIAN
|
219
|
+
else
|
220
|
+
raise ArgumentError, "unknown value for endian '#{endian}'"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def create_sanitized_params(params, the_class)
|
225
|
+
SanitizedParameters.new(params, the_class, self.endian)
|
226
|
+
end
|
227
|
+
|
228
|
+
def create_sanitized_choices(choices)
|
229
|
+
SanitizedChoices.new(choices, self.endian)
|
230
|
+
end
|
231
|
+
|
232
|
+
def create_sanitized_fields
|
233
|
+
SanitizedFields.new(self.endian)
|
234
|
+
end
|
235
|
+
|
236
|
+
def create_sanitized_object_prototype(obj_type, obj_params)
|
237
|
+
SanitizedPrototype.new(obj_type, obj_params, self.endian)
|
238
|
+
end
|
239
|
+
|
240
|
+
#---------------
|
241
|
+
private
|
242
|
+
|
243
|
+
def sanitize!
|
244
|
+
ensure_no_nil_values
|
245
|
+
merge_default_parameters!
|
246
|
+
|
247
|
+
@the_class.sanitize_parameters!(self)
|
248
|
+
|
249
|
+
ensure_mandatory_parameters_exist
|
250
|
+
ensure_mutual_exclusion_of_parameters
|
251
|
+
end
|
252
|
+
|
253
|
+
def ensure_no_nil_values
|
254
|
+
each do |key, value|
|
255
|
+
if value.nil?
|
256
|
+
raise ArgumentError,
|
257
|
+
"parameter '#{key}' has nil value in #{@the_class}"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def merge_default_parameters!
|
263
|
+
@the_class.default_parameters.each do |key, value|
|
264
|
+
self[key] ||= value
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def ensure_mandatory_parameters_exist
|
269
|
+
@the_class.mandatory_parameters.each do |key|
|
270
|
+
unless has_parameter?(key)
|
271
|
+
raise ArgumentError,
|
272
|
+
"parameter '#{key}' must be specified in #{@the_class}"
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def ensure_mutual_exclusion_of_parameters
|
278
|
+
return if length < 2
|
279
|
+
|
280
|
+
@the_class.mutually_exclusive_parameters.each do |key1, key2|
|
281
|
+
if has_parameter?(key1) and has_parameter?(key2)
|
282
|
+
raise ArgumentError, "params '#{key1}' and '#{key2}' " +
|
283
|
+
"are mutually exclusive in #{@the_class}"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
#----------------------------------------------------------------------------
|
289
|
+
|
290
|
+
end
|