transit-ruby 0.8.467

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ # Copyright 2014 Cognitect. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS-IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Transit
16
+ # @api private
17
+ module DateTimeUtil
18
+ def to_millis(v)
19
+ case v
20
+ when DateTime
21
+ t = v.new_offset(0).to_time
22
+ when Date
23
+ t = Time.gm(v.year, v.month, v.day)
24
+ when Time
25
+ t = v.getutc
26
+ else
27
+ raise "Don't know how to get millis from #{t.inspect}"
28
+ end
29
+ (t.to_i * 1000) + (t.usec / 1000)
30
+ end
31
+
32
+ def from_millis(millis)
33
+ t = Time.at(millis / 1000).utc
34
+ DateTime.new(t.year, t.month, t.day, t.hour, t.min, t.sec + (millis % 1000 * 0.001))
35
+ end
36
+
37
+ module_function :to_millis, :from_millis
38
+ end
39
+ end
@@ -0,0 +1,115 @@
1
+ # Copyright 2014 Cognitect. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS-IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Transit
16
+ # Converts a transit value to an instance of a type
17
+ # @api private
18
+ class Decoder
19
+ ESC_ESC = "#{ESC}#{ESC}"
20
+ ESC_SUB = "#{ESC}#{SUB}"
21
+ ESC_RES = "#{ESC}#{RES}"
22
+
23
+ IDENTITY = ->(v){v}
24
+
25
+ GROUND_TAGS = %w[_ s ? i d b ' array map]
26
+
27
+ def initialize(options={})
28
+ custom_handlers = options[:handlers] || {}
29
+ custom_handlers.each {|k,v| validate_handler(k,v)}
30
+ @handlers = ReadHandlers::DEFAULT_READ_HANDLERS.merge(custom_handlers)
31
+ @default_handler = options[:default_handler] || ReadHandlers::DEFAULT_READ_HANDLER
32
+ end
33
+
34
+ # @api private
35
+ class Tag < String; end
36
+
37
+ # Decodes a transit value to a corresponding object
38
+ #
39
+ # @param node a transit value to be decoded
40
+ # @param cache
41
+ # @param as_map_key
42
+ # @return decoded object
43
+ def decode(node, cache=RollingCache.new, as_map_key=false)
44
+ case node
45
+ when String
46
+ if cache.has_key?(node)
47
+ cache.read(node)
48
+ else
49
+ parsed = begin
50
+ if !node.start_with?(ESC)
51
+ node
52
+ elsif node.start_with?(TAG)
53
+ Tag.new(node[2..-1])
54
+ elsif handler = @handlers[node[1]]
55
+ handler.from_rep(node[2..-1])
56
+ elsif node.start_with?(ESC_ESC, ESC_SUB, ESC_RES)
57
+ node[1..-1]
58
+ else
59
+ @default_handler.from_rep(node[1], node[2..-1])
60
+ end
61
+ end
62
+ if cache.cacheable?(node, as_map_key)
63
+ cache.write(parsed)
64
+ end
65
+ parsed
66
+ end
67
+ when Array
68
+ return node if node.empty?
69
+ e0 = decode(node.shift, cache, false)
70
+ if e0 == MAP_AS_ARRAY
71
+ decode(Hash[*node], cache)
72
+ elsif Tag === e0
73
+ v = decode(node.shift, cache)
74
+ if handler = @handlers[e0]
75
+ handler.from_rep(v)
76
+ else
77
+ @default_handler.from_rep(e0,v)
78
+ end
79
+ else
80
+ [e0] + node.map {|e| decode(e, cache, as_map_key)}
81
+ end
82
+ when Hash
83
+ if node.size == 1
84
+ k = decode(node.keys.first, cache, true)
85
+ v = decode(node.values.first, cache, false)
86
+ if Tag === k
87
+ if handler = @handlers[k]
88
+ handler.from_rep(v)
89
+ else
90
+ @default_handler.from_rep(k,v)
91
+ end
92
+ else
93
+ {k => v}
94
+ end
95
+ else
96
+ node.keys.each do |k|
97
+ node.store(decode(k, cache, true), decode(node.delete(k), cache))
98
+ end
99
+ node
100
+ end
101
+ else
102
+ node
103
+ end
104
+ end
105
+
106
+ def validate_handler(key, handler)
107
+ raise ArgumentError.new(CAN_NOT_OVERRIDE_GROUND_TYPES_MESSAGE) if GROUND_TAGS.include?(key)
108
+ end
109
+
110
+ CAN_NOT_OVERRIDE_GROUND_TYPES_MESSAGE = <<-MSG
111
+ You can not supply custom read handlers for ground types.
112
+ MSG
113
+
114
+ end
115
+ end
@@ -0,0 +1,98 @@
1
+ # Copyright 2014 Cognitect. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS-IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Transit
16
+ # @see Transit::WriteHandlers
17
+ module ReadHandlers
18
+ class Default
19
+ def from_rep(tag,val) TaggedValue.new(tag, val) end
20
+ end
21
+ class NilHandler
22
+ def from_rep(_) nil end
23
+ end
24
+ class KeywordHandler
25
+ def from_rep(v) v.to_sym end
26
+ end
27
+ class BooleanHandler
28
+ def from_rep(v) v == "t" end
29
+ end
30
+ class ByteArrayHandler
31
+ def from_rep(v) ByteArray.from_base64(v) end
32
+ end
33
+ class FloatHandler
34
+ def from_rep(v) Float(v) end
35
+ end
36
+ class IntegerHandler
37
+ def from_rep(v) v.to_i end
38
+ end
39
+ class BigIntegerHandler
40
+ def from_rep(v) v.to_i end
41
+ end
42
+ class BigDecimalHandler
43
+ def from_rep(v) BigDecimal.new(v) end
44
+ end
45
+ class IdentityHandler
46
+ def from_rep(v) v end
47
+ end
48
+ class SymbolHandler
49
+ def from_rep(v) Transit::Symbol.new(v) end
50
+ end
51
+ class TimeStringHandler
52
+ def from_rep(v) DateTime.iso8601(v) end
53
+ end
54
+ class TimeIntHandler
55
+ def from_rep(v) DateTimeUtil.from_millis(v.to_i) end
56
+ end
57
+ class UuidHandler
58
+ def from_rep(v) UUID.new(v) end
59
+ end
60
+ class UriHandler
61
+ def from_rep(v) Addressable::URI.parse(v) end
62
+ end
63
+ class SetHandler
64
+ def from_rep(v) Set.new(v) end
65
+ end
66
+ class LinkHandler
67
+ def from_rep(v) Link.new(v) end
68
+ end
69
+ class CmapHandler
70
+ def from_rep(v) Hash[*v] end
71
+ end
72
+
73
+ DEFAULT_READ_HANDLERS = {
74
+ "_" => NilHandler.new,
75
+ ":" => KeywordHandler.new,
76
+ "?" => BooleanHandler.new,
77
+ "b" => ByteArrayHandler.new,
78
+ "d" => FloatHandler.new,
79
+ "i" => IntegerHandler.new,
80
+ "n" => BigIntegerHandler.new,
81
+ "f" => BigDecimalHandler.new,
82
+ "c" => IdentityHandler.new,
83
+ "$" => SymbolHandler.new,
84
+ "t" => TimeStringHandler.new,
85
+ "m" => TimeIntHandler.new,
86
+ "u" => UuidHandler.new,
87
+ "r" => UriHandler.new,
88
+ "'" => IdentityHandler.new,
89
+ "set" => SetHandler.new,
90
+ "link" => LinkHandler.new,
91
+ "list" => IdentityHandler.new,
92
+ "cmap" => CmapHandler.new
93
+ }.freeze
94
+
95
+ DEFAULT_READ_HANDLER = Default.new
96
+
97
+ end
98
+ end
@@ -0,0 +1,117 @@
1
+ # Copyright 2014 Cognitect. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS-IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Transit
16
+ # Transit::Reader converts incoming transit data into appropriate
17
+ # values/objects in Ruby.
18
+ # @see https://github.com/cognitect/transit-format
19
+ class Reader
20
+ # @api private
21
+ class JsonUnmarshaler
22
+ class ParseHandler
23
+ def each(&block) @yield_v = block end
24
+ def add_value(v) @yield_v[v] if @yield_v end
25
+
26
+ def hash_start() {} end
27
+ def hash_set(h,k,v) h.store(k,v) end
28
+ def array_start() [] end
29
+ def array_append(a,v) a << v end
30
+
31
+ def error(message, line, column)
32
+ raise Exception.new(message, line, column)
33
+ end
34
+ end
35
+
36
+ def initialize(io, opts)
37
+ @io = io
38
+ @decoder = Transit::Decoder.new(opts)
39
+ @parse_handler = ParseHandler.new
40
+ end
41
+
42
+ # @see Reader#read
43
+ def read
44
+ if block_given?
45
+ @parse_handler.each {|v| yield @decoder.decode(v)}
46
+ else
47
+ @parse_handler.each {|v| return @decoder.decode(v)}
48
+ end
49
+ Oj.sc_parse(@parse_handler, @io)
50
+ end
51
+ end
52
+
53
+ # @api private
54
+ class MessagePackUnmarshaler
55
+ def initialize(io, opts)
56
+ @decoder = Transit::Decoder.new(opts)
57
+ @unpacker = MessagePack::Unpacker.new(io)
58
+ end
59
+
60
+ # @see Reader#read
61
+ def read
62
+ if block_given?
63
+ @unpacker.each {|v| yield @decoder.decode(v)}
64
+ else
65
+ @decoder.decode(@unpacker.read)
66
+ end
67
+ end
68
+ end
69
+
70
+ extend Forwardable
71
+
72
+ # @!method read
73
+ # Reads transit values from an IO (file, stream, etc), and
74
+ # converts each one to the appropriate Ruby object.
75
+ #
76
+ # With a block, yields each object to the block as it is processed.
77
+ #
78
+ # Without a block, returns a single object.
79
+ #
80
+ # @example
81
+ # reader = Transit::Reader.new(:json, io)
82
+ # reader.read {|obj| do_something_with(obj)}
83
+ #
84
+ # reader = Transit::Reader.new(:json, io)
85
+ # obj = reader.read
86
+ def_delegators :@reader, :read
87
+
88
+ # @param [Symbol] format required any of :msgpack, :json, :json_verbose
89
+ # @param [IO] io required
90
+ # @param [Hash] opts optional
91
+ # Creates a new Reader configured to read from <tt>io</tt>,
92
+ # expecting <tt>format</tt> (<tt>:json</tt>, <tt>:msgpack</tt>).
93
+ #
94
+ # Use opts to register custom read handlers, associating each one
95
+ # with its tag.
96
+ #
97
+ # @example
98
+ #
99
+ # json_reader = Transit::Reader.new(:json, io)
100
+ # # ^^ reads both :json and :json_verbose formats ^^
101
+ # msgpack_writer = Transit::Reader.new(:msgpack, io)
102
+ # writer_with_custom_handlers = Transit::Reader.new(:json, io,
103
+ # :handlers => {"point" => PointReadHandler})
104
+ #
105
+ # @see Transit::ReadHandlers
106
+ def initialize(format, io, opts={})
107
+ @reader = case format
108
+ when :json, :json_verbose
109
+ require 'oj'
110
+ JsonUnmarshaler.new(io, opts)
111
+ else
112
+ require 'msgpack'
113
+ MessagePackUnmarshaler.new(io, opts)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,70 @@
1
+ # Copyright 2014 Cognitect. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS-IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Transit
16
+ # @api private
17
+ class RollingCache
18
+ extend Forwardable
19
+
20
+ def_delegators "@key_to_value", :has_key?, :size
21
+
22
+ FIRST_ORD = 48
23
+ LAST_ORD = 91
24
+ CACHE_CODE_DIGITS = 44;
25
+ CACHE_SIZE = CACHE_CODE_DIGITS * CACHE_CODE_DIGITS;
26
+ MIN_SIZE_CACHEABLE = 4
27
+
28
+ def initialize
29
+ clear
30
+ end
31
+
32
+ def read(key)
33
+ @key_to_value[key]
34
+ end
35
+
36
+ def write(val)
37
+ @value_to_key[val] || begin
38
+ clear if @key_to_value.size >= CACHE_SIZE
39
+ key = next_key(@key_to_value.size)
40
+ @value_to_key[val] = key
41
+ @key_to_value[key] = val
42
+ end
43
+ end
44
+
45
+ def cache_key?(str, _=false)
46
+ str[0] == SUB && str != MAP_AS_ARRAY
47
+ end
48
+
49
+ def cacheable?(str, as_map_key=false)
50
+ str.size >= MIN_SIZE_CACHEABLE && (as_map_key || str.start_with?("~#","~$","~:"))
51
+ end
52
+
53
+ private
54
+
55
+ def clear
56
+ @key_to_value = {}
57
+ @value_to_key = {}
58
+ end
59
+
60
+ def next_key(i)
61
+ hi = i / CACHE_CODE_DIGITS;
62
+ lo = i % CACHE_CODE_DIGITS;
63
+ if hi == 0
64
+ "^#{(lo+FIRST_ORD).chr}"
65
+ else
66
+ "^#{(hi+FIRST_ORD).chr}#{(lo+FIRST_ORD).chr}"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,251 @@
1
+ # Copyright 2014 Cognitect. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS-IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Transit
16
+ class Wrapper
17
+ extend Forwardable
18
+
19
+ def_delegators :@value, :hash, :to_sym, :to_s
20
+
21
+ attr_reader :value
22
+
23
+ def initialize(value)
24
+ @value = value
25
+ end
26
+
27
+ def ==(other)
28
+ other.is_a?(self.class) && @value == other.value
29
+ end
30
+ alias eql? ==
31
+
32
+ def inspect
33
+ "<#{self.class} \"#{to_s}\">"
34
+ end
35
+ end
36
+
37
+ # Represents a transit symbol extension type.
38
+ # @see https://github.com/cognitect/transit-format
39
+ class Symbol < Wrapper
40
+ def initialize(sym)
41
+ super sym.to_sym
42
+ end
43
+
44
+ def namespace
45
+ @namespace ||= parsed[-2]
46
+ end
47
+
48
+ def name
49
+ @name ||= parsed[-1] || "/"
50
+ end
51
+
52
+ private
53
+
54
+ def parsed
55
+ @parsed ||= @value.to_s.split("/")
56
+ end
57
+ end
58
+
59
+ # Represents a transit byte array extension type.
60
+ # @see https://github.com/cognitect/transit-format
61
+ class ByteArray < Wrapper
62
+ def self.from_base64(data)
63
+ new(Base64.decode64(data))
64
+ end
65
+
66
+ def to_base64
67
+ Base64.encode64(@value)
68
+ end
69
+
70
+ def to_s
71
+ @value
72
+ end
73
+ end
74
+
75
+ # Represents a transit UUID extension type.
76
+ # @see https://github.com/cognitect/transit-format
77
+ class UUID
78
+ def self.random
79
+ new
80
+ end
81
+
82
+ def initialize(uuid_or_most_significant_bits=nil,least_significant_bits=nil)
83
+ case uuid_or_most_significant_bits
84
+ when String
85
+ @string_rep = uuid_or_most_significant_bits
86
+ when Array
87
+ @numeric_rep = uuid_or_most_significant_bits.map {|n| twos_complement(n)}
88
+ when Numeric
89
+ @numeric_rep = [twos_complement(uuid_or_most_significant_bits), twos_complement(least_significant_bits)]
90
+ when nil
91
+ @string_rep = SecureRandom.uuid
92
+ else
93
+ raise "Can't build UUID from #{uuid_or_most_significant_bits.inspect}"
94
+ end
95
+ end
96
+
97
+ def to_s
98
+ @string_rep ||= numbers_to_string
99
+ end
100
+
101
+ def most_significant_bits
102
+ @most_significant_bits ||= numeric_rep[0]
103
+ end
104
+
105
+ def least_significant_bits
106
+ @least_significant_bits ||= numeric_rep[1]
107
+ end
108
+
109
+ def inspect
110
+ @inspect ||= "<#{self.class} \"#{to_s}\">"
111
+ end
112
+
113
+ def ==(other)
114
+ return false unless other.is_a?(self.class)
115
+ if @numeric_rep
116
+ other.most_significant_bits == most_significant_bits &&
117
+ other.least_significant_bits == least_significant_bits
118
+ else
119
+ other.to_s == @string_rep
120
+ end
121
+ end
122
+ alias eql? ==
123
+
124
+ def hash
125
+ most_significant_bits.hash + least_significant_bits.hash
126
+ end
127
+
128
+ private
129
+
130
+ def numeric_rep
131
+ @numeric_rep ||= string_to_numbers
132
+ end
133
+
134
+ def numbers_to_string
135
+ most_significant_bits = @numeric_rep[0]
136
+ least_significant_bits = @numeric_rep[1]
137
+ digits(most_significant_bits >> 32, 8) + "-" +
138
+ digits(most_significant_bits >> 16, 4) + "-" +
139
+ digits(most_significant_bits, 4) + "-" +
140
+ digits(least_significant_bits >> 48, 4) + "-" +
141
+ digits(least_significant_bits, 12)
142
+ end
143
+
144
+ def string_to_numbers
145
+ str = @string_rep.delete("-")
146
+ [twos_complement(str[ 0..15].hex), twos_complement(str[16..31].hex)]
147
+ end
148
+
149
+ def digits(val, digits)
150
+ hi = 1 << (digits*4)
151
+ (hi | (val & (hi - 1))).to_s(16)[1..-1]
152
+ end
153
+
154
+ def twos_complement(integer_value, num_of_bits=64)
155
+ max_signed = 2**(num_of_bits-1)
156
+ max_unsigned = 2**num_of_bits
157
+ (integer_value >= max_signed) ? integer_value - max_unsigned : integer_value
158
+ end
159
+ end
160
+
161
+ # Represents a transit hypermedia link extension type.
162
+ # @see https://github.com/cognitect/transit-format
163
+ # @see http://amundsen.com/media-types/collection/format/#arrays-links
164
+ class Link
165
+ KEYS = ["href", "rel", "name", "render", "prompt"]
166
+ RENDER_VALUES = ["link", "image"]
167
+
168
+ # @overload Link.new(hash)
169
+ # @param [Hash] hash
170
+ # Valid keys are:
171
+ # "href" required, String or Addressable::URI
172
+ # "rel" required, String
173
+ # "name" optional, String
174
+ # "render" optional, String (only "link" or "image")
175
+ # "prompt" optional, String
176
+ # @overload Link.new(href, rel, name, render, prompt)
177
+ # @param [String, Addressable::URI] href required
178
+ # @param [String] rel required
179
+ # @param [String] name optional
180
+ # @param [String] render optional (only "link" or "image")
181
+ # @param [String] prompt optional
182
+ def initialize(*args)
183
+ @values = if args[0].is_a?(Hash)
184
+ reconcile_values(args[0])
185
+ elsif args.length >= 2 && (args[0].is_a?(Addressable::URI) || args[0].is_a?(String))
186
+ reconcile_values(Hash[KEYS.zip(args)])
187
+ else
188
+ raise ArgumentError, "The first argument to Link.new can be a URI, String or a Hash. When the first argument is a URI or String, the second argument, rel, must present."
189
+ end
190
+ end
191
+
192
+ def href; @href ||= @values["href"] end
193
+ def rel; @rel ||= @values["rel"] end
194
+ def name; @name ||= @values["name"] end
195
+ def render; @render ||= @values["render"] end
196
+ def prompt; @prompt ||= @values["prompt"] end
197
+
198
+ def to_h
199
+ @values
200
+ end
201
+
202
+ def ==(other)
203
+ other.is_a?(Link) && other.to_h == to_h
204
+ end
205
+ alias eql? ==
206
+
207
+ def hash
208
+ @values.hash
209
+ end
210
+
211
+ private
212
+
213
+ def reconcile_values(map)
214
+ map.dup.tap do |m|
215
+ m["href"] = Addressable::URI.parse(m["href"]) if m["href"].is_a?(String)
216
+ if m["render"]
217
+ render = m["render"].downcase
218
+ if RENDER_VALUES.include?(render)
219
+ m["render"] = render
220
+ else
221
+ raise ArgumentError, "render must be either #{RENDER_VALUES[0]} or #{RENDER_VALUES[1]}"
222
+ end
223
+ end
224
+ end.freeze
225
+ end
226
+ end
227
+
228
+ # Represents a transit tag and value. Returned by default when a
229
+ # reader encounters a tag for which there is no registered
230
+ # handler. Can also be used in a custom write handler to force
231
+ # representation to use a transit ground type using a rep for which
232
+ # there is no registered handler (e.g., an iterable for the
233
+ # representation of an array).
234
+ # @see https://github.com/cognitect/transit-format
235
+ class TaggedValue
236
+ attr_reader :tag, :rep
237
+ def initialize(tag, rep)
238
+ @tag = tag
239
+ @rep = rep
240
+ end
241
+
242
+ def ==(other)
243
+ other.is_a?(self.class) && other.tag == @tag && other.rep == @rep
244
+ end
245
+ alias eql? ==
246
+
247
+ def hash
248
+ @tag.hash + @rep.hash
249
+ end
250
+ end
251
+ end