seeing_is_believing 3.0.0.beta.4 → 3.0.0.beta.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -8
  3. data/Rakefile +1 -1
  4. data/Readme.md +65 -25
  5. data/bin/seeing_is_believing +1 -0
  6. data/docs/sib-streaming.gif +0 -0
  7. data/features/deprecated-flags.feature +62 -2
  8. data/features/errors.feature +12 -7
  9. data/features/examples.feature +143 -4
  10. data/features/flags.feature +89 -29
  11. data/features/regression.feature +58 -14
  12. data/features/support/env.rb +4 -0
  13. data/features/xmpfilter-style.feature +181 -36
  14. data/lib/seeing_is_believing.rb +44 -33
  15. data/lib/seeing_is_believing/binary.rb +31 -88
  16. data/lib/seeing_is_believing/binary/align_chunk.rb +30 -11
  17. data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +10 -16
  18. data/lib/seeing_is_believing/binary/annotate_every_line.rb +5 -25
  19. data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +136 -0
  20. data/lib/seeing_is_believing/binary/comment_lines.rb +8 -10
  21. data/lib/seeing_is_believing/binary/commentable_lines.rb +20 -26
  22. data/lib/seeing_is_believing/binary/config.rb +392 -0
  23. data/lib/seeing_is_believing/binary/data_structures.rb +57 -0
  24. data/lib/seeing_is_believing/binary/engine.rb +104 -0
  25. data/lib/seeing_is_believing/binary/{comment_formatter.rb → format_comment.rb} +6 -6
  26. data/lib/seeing_is_believing/binary/remove_annotations.rb +29 -28
  27. data/lib/seeing_is_believing/binary/rewrite_comments.rb +42 -43
  28. data/lib/seeing_is_believing/code.rb +105 -49
  29. data/lib/seeing_is_believing/debugger.rb +6 -5
  30. data/lib/seeing_is_believing/error.rb +6 -17
  31. data/lib/seeing_is_believing/evaluate_by_moving_files.rb +78 -129
  32. data/lib/seeing_is_believing/event_stream/consumer.rb +114 -64
  33. data/lib/seeing_is_believing/event_stream/events.rb +169 -11
  34. data/lib/seeing_is_believing/event_stream/handlers/debug.rb +57 -0
  35. data/lib/seeing_is_believing/event_stream/handlers/record_exitstatus.rb +18 -0
  36. data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +45 -0
  37. data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +39 -0
  38. data/lib/seeing_is_believing/event_stream/producer.rb +25 -24
  39. data/lib/seeing_is_believing/hash_struct.rb +206 -0
  40. data/lib/seeing_is_believing/result.rb +20 -3
  41. data/lib/seeing_is_believing/the_matrix.rb +20 -12
  42. data/lib/seeing_is_believing/version.rb +1 -1
  43. data/lib/seeing_is_believing/wrap_expressions.rb +55 -115
  44. data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +14 -0
  45. data/seeing_is_believing.gemspec +1 -1
  46. data/spec/binary/alignment_specs.rb +27 -0
  47. data/spec/binary/comment_lines_spec.rb +3 -2
  48. data/spec/binary/config_spec.rb +657 -0
  49. data/spec/binary/engine_spec.rb +97 -0
  50. data/spec/binary/{comment_formatter_spec.rb → format_comment_spec.rb} +2 -2
  51. data/spec/binary/marker_spec.rb +71 -0
  52. data/spec/binary/options_spec.rb +0 -0
  53. data/spec/binary/remove_annotations_spec.rb +31 -18
  54. data/spec/binary/rewrite_comments_spec.rb +26 -11
  55. data/spec/code_spec.rb +190 -6
  56. data/spec/debugger_spec.rb +4 -0
  57. data/spec/evaluate_by_moving_files_spec.rb +38 -20
  58. data/spec/event_stream_spec.rb +265 -116
  59. data/spec/hash_struct_spec.rb +514 -0
  60. data/spec/seeing_is_believing_spec.rb +108 -46
  61. data/spec/spec_helper.rb +9 -0
  62. data/spec/wrap_expressions_spec.rb +207 -172
  63. metadata +30 -18
  64. data/docs/for-presentations +0 -33
  65. data/lib/seeing_is_believing/binary/annotate_xmpfilter_style.rb +0 -128
  66. data/lib/seeing_is_believing/binary/interpret_flags.rb +0 -156
  67. data/lib/seeing_is_believing/binary/parse_args.rb +0 -263
  68. data/lib/seeing_is_believing/event_stream/update_result.rb +0 -24
  69. data/lib/seeing_is_believing/inspect_expressions.rb +0 -21
  70. data/lib/seeing_is_believing/parser_helpers.rb +0 -82
  71. data/spec/binary/interpret_flags_spec.rb +0 -332
  72. data/spec/binary/parse_args_spec.rb +0 -415
@@ -1,17 +1,175 @@
1
+ require 'seeing_is_believing/hash_struct'
2
+
1
3
  class SeeingIsBelieving
2
4
  module EventStream
5
+ Event = HashStruct.anon do # one superclass to rule them all!
6
+ def self.event_name
7
+ raise NotImplementedError, "Subclass should have defined this!"
8
+ end
9
+
10
+ def event_name
11
+ self.class.event_name
12
+ end
13
+
14
+ def as_json
15
+ [event_name, to_h]
16
+ end
17
+ end
18
+
3
19
  module Events
4
- LineResult = Struct.new(:type, :line_number, :inspected)
5
- UnrecordedResult = Struct.new(:type, :line_number)
6
- Stdout = Struct.new(:value)
7
- Stderr = Struct.new(:value)
8
- MaxLineCaptures = Struct.new(:value)
9
- Filename = Struct.new(:value)
10
- NumLines = Struct.new(:value)
11
- SiBVersion = Struct.new(:value)
12
- RubyVersion = Struct.new(:value)
13
- Exitstatus = Struct.new(:value)
14
- Exception = Struct.new(:line_number, :class_name, :message, :backtrace)
20
+ # A line was printed to stdout.
21
+ class Stdout < Event
22
+ def self.event_name
23
+ :stdout
24
+ end
25
+ attributes :value
26
+ end
27
+
28
+ # A line was printed to stderr.
29
+ class Stderr < Event
30
+ def self.event_name
31
+ :stderr
32
+ end
33
+ attributes :value
34
+ end
35
+
36
+ # The program will not record more results than this for a line.
37
+ # Note that if this is hit, it will emit an unrecorded_result.
38
+ class MaxLineCaptures < Event
39
+ def self.event_name
40
+ :max_line_captures
41
+ end
42
+ def as_json
43
+ value, is_infinity = if self.value == Float::INFINITY
44
+ [-1, true]
45
+ else
46
+ [self.value, false]
47
+ end
48
+ [event_name, {value: value, is_infinity: is_infinity}]
49
+ end
50
+ attribute :value
51
+ end
52
+
53
+ # Name of the file being evaluated.
54
+ class Filename < Event
55
+ def self.event_name
56
+ :filename
57
+ end
58
+ attributes :value
59
+ end
60
+
61
+ # Number of lines in the program.
62
+ class NumLines < Event
63
+ def self.event_name
64
+ :num_lines
65
+ end
66
+ attributes :value
67
+ end
68
+
69
+ # Version of SeeingIsBelieving used to evaluate the code.
70
+ # Equivalent to `SeeingIsBelieving::VERSION`, and `seeing_is_believing --version`
71
+ class SiBVersion < Event
72
+ def self.event_name
73
+ :sib_version
74
+ end
75
+ attributes :value
76
+ end
77
+
78
+ # Version of Ruby being used to evaluate the code.
79
+ # Equivalent to `RUBY_VERSION`
80
+ class RubyVersion < Event
81
+ def self.event_name
82
+ :ruby_version
83
+ end
84
+ attributes :value
85
+ end
86
+
87
+ # The process' exitstatus.
88
+ class Exitstatus < Event
89
+ def self.event_name
90
+ :exitstatus
91
+ end
92
+ attributes :value
93
+ end
94
+
95
+ # Emitted when the process invokes exec.
96
+ # Note that this could be a child process,
97
+ # so it does not necessarily mean there won't be any more line results
98
+ class Exec < Event
99
+ def self.event_name
100
+ :exec
101
+ end
102
+ attributes :args
103
+ end
104
+
105
+ # A line was executed, and its result recorded.
106
+ # Currently, type will either be :inspect, or :pp
107
+ # :pp is used by AnnotateMarkedLines to facilitate xmpfilter style.
108
+ # If you're consuming the event stream, it's safe to assume type will always be :inspect
109
+ # If you're using the library, it's whatever you've recorded it as (if you haven't changed this, it's :inspect)
110
+ class LineResult < Event
111
+ def self.event_name
112
+ :line_result
113
+ end
114
+ attributes :type, :line_number, :inspected
115
+ end
116
+
117
+ # There were more results than we are emitting for this line / type of recording
118
+ # See LineResult for explanation of types
119
+ # This would occur because the line was executed more times than the max.
120
+ class ResultsTruncated < Event
121
+ def self.event_name
122
+ :results_truncated
123
+ end
124
+ attributes :type, :line_number
125
+ end
126
+
127
+ # The program raised an exception and did not catch it.
128
+ # Note that currently `ExitStatus` exceptions are not emitted.
129
+ # That could change at some point as it seems like the stream consumer
130
+ # should decide whether they care about that rather than the producer.
131
+ class Exception < Event
132
+ def self.event_name
133
+ :exception
134
+ end
135
+ attributes :line_number, :class_name, :message, :backtrace
136
+ end
137
+
138
+ # The process's stdout stream was closed, there will be no more Stdout events.
139
+ # "side" will either be :producer or :consumer
140
+ class StdoutClosed < Event
141
+ def self.event_name
142
+ :stdout_closed
143
+ end
144
+ attributes :side
145
+ end
146
+
147
+ # The process's stderr stream was closed, there will be no more Stderr events.
148
+ # "side" will either be :producer or :consumer
149
+ class StderrClosed < Event
150
+ def self.event_name
151
+ :stderr_closed
152
+ end
153
+ attributes :side
154
+ end
155
+
156
+ # The process's event stream was closed, there will be no more events that come via the stream.
157
+ # Currently, that's all events except Stdout, StdoutClosed, Stderr, StdoutClosed, ExitStatus, and Finished
158
+ class EventStreamClosed < Event
159
+ def self.event_name
160
+ :event_stream_closed
161
+ end
162
+ attributes :side
163
+ end
164
+
165
+ # All streams are closed and the exit status is known.
166
+ # There will be no more events.
167
+ class Finished < Event
168
+ def self.event_name
169
+ :finished
170
+ end
171
+ attributes []
172
+ end
15
173
  end
16
174
  end
17
175
  end
@@ -0,0 +1,57 @@
1
+ class SeeingIsBelieving
2
+ module EventStream
3
+ module Handlers
4
+ class Debug
5
+ def initialize(debugger, handler)
6
+ @debugger = debugger
7
+ @handler = handler
8
+ @seen = ""
9
+ @line_width = 150 # debugger is basically for me, so giving it a nice wide width
10
+ @name_width = 20
11
+ @attr_width = @line_width - @name_width
12
+ end
13
+
14
+ def call(event)
15
+ observe event
16
+ finish if event.kind_of? Events::Finished
17
+ @handler.call event
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :debugger, :handler
23
+
24
+ def finish
25
+ @debugger.context("EVENTS:") { @seen }
26
+ end
27
+
28
+ def observe(event)
29
+ name = event.class.name.split("::").last
30
+ lines = event.to_h
31
+ .map { |attribute, value|
32
+ case attribute
33
+ when :side then "#{attribute}: #{value}"
34
+ when :value then value.to_s.chomp
35
+ when :backtrace then indented = value.map { |v| "- #{v}" }
36
+ ["backtrace:", *indented]
37
+ else "#{attribute}: #{value.inspect}"
38
+ end
39
+ }
40
+ .flatten
41
+ joined = lines.join ", "
42
+ if joined.size < @attr_width
43
+ @seen << sprintf("%-#{@name_width}s%s\n", name, joined)
44
+ elsif lines.size == 1
45
+ @seen << sprintf("%-#{@name_width}s%s...\n", name, lines.first[0...@attr_width-3])
46
+ else
47
+ @seen << "#{name}\n"
48
+ lines.each { |line|
49
+ line = line[0...@line_width-5] << "..." if @line_width < line.length + 2
50
+ @seen << sprintf("| %s\n", line)
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,18 @@
1
+ class SeeingIsBelieving
2
+ module EventStream
3
+ module Handlers
4
+ class RecordExitStatus
5
+ attr_reader :exitstatus
6
+
7
+ def initialize(next_observer)
8
+ @next_observer = next_observer
9
+ end
10
+
11
+ def call(event)
12
+ @exitstatus = event.value if event.event_name == :exitstatus
13
+ @next_observer.call(event)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,45 @@
1
+ require 'json'
2
+ class SeeingIsBelieving
3
+ module EventStream
4
+ module Handlers
5
+ class StreamJsonEvents
6
+ attr_reader :stream
7
+
8
+ def initialize(stream)
9
+ @flush = true if stream.respond_to? :flush
10
+ @stream = stream
11
+ @has_exception = false
12
+ @exitstatus = :not_yet_seen
13
+ end
14
+
15
+ def call(event)
16
+ write_event event
17
+ record_outcome event
18
+ end
19
+
20
+ def has_exception?
21
+ true
22
+ end
23
+
24
+ def exitstatus
25
+ @exitstatus
26
+ end
27
+
28
+ private
29
+
30
+ def write_event(event)
31
+ @stream << JSON.dump(event.as_json)
32
+ @stream << "\n"
33
+ @stream.flush if @flush
34
+ end
35
+
36
+ def record_outcome(event)
37
+ case event
38
+ when Events::Exception then @has_exception = true
39
+ when Events::Exitstatus then @exitstatus = event.value
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,39 @@
1
+ require 'seeing_is_believing/event_stream/events'
2
+ class SeeingIsBelieving
3
+ module EventStream
4
+ module Handlers
5
+ class UpdateResult
6
+ include EventStream::Events
7
+
8
+ attr_reader :result
9
+
10
+ def initialize(result)
11
+ @result = result
12
+ end
13
+
14
+ def call(event)
15
+ case event
16
+ when LineResult then result.record_result(event.type, event.line_number, event.inspected)
17
+ when ResultsTruncated then result.record_result(event.type, event.line_number, '...') # <-- is this really what I want?
18
+ when Exception then result.record_exception event.line_number, event.class_name, event.message, event.backtrace
19
+ when Stdout then result.stdout << event.value
20
+ when Stderr then result.stderr << event.value
21
+ when MaxLineCaptures then result.max_line_captures = event.value
22
+ when Exitstatus then result.exitstatus = event.value
23
+ when NumLines then result.num_lines = event.value
24
+ when SiBVersion then result.sib_version = event.value
25
+ when RubyVersion then result.ruby_version = event.value
26
+ when Filename then result.filename = event.value
27
+ when Exec,
28
+ Finished,
29
+ StdoutClosed,
30
+ StderrClosed,
31
+ EventStreamClosed
32
+ # no op
33
+ else raise "Unknown event: #{event.inspect}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -11,13 +11,11 @@ class SeeingIsBelieving
11
11
  def shift() end
12
12
  end
13
13
 
14
- attr_accessor :exitstatus, :max_line_captures, :num_lines, :filename
14
+ attr_accessor :max_line_captures, :filename
15
15
 
16
16
  def initialize(resultstream)
17
17
  self.filename = nil
18
- self.exitstatus = 0
19
18
  self.max_line_captures = Float::INFINITY
20
- self.num_lines = 0
21
19
  self.recorded_results = []
22
20
  self.queue = Queue.new
23
21
  self.producer_thread = Thread.new do
@@ -31,9 +29,9 @@ class SeeingIsBelieving
31
29
  rescue IOError, Errno::EPIPE
32
30
  queue.clear
33
31
  ensure
32
+ self.queue = NullQueue
34
33
  resultstream.flush rescue nil
35
34
  end
36
- self.queue = NullQueue
37
35
  end
38
36
  end
39
37
 
@@ -56,7 +54,6 @@ class SeeingIsBelieving
56
54
  StackErrors = [SystemStackError]
57
55
  StackErrors << Java::JavaLang::StackOverflowError if defined?(RUBY_PLATFORM) && RUBY_PLATFORM == 'java'
58
56
  def record_result(type, line_number, value)
59
- self.num_lines = line_number if num_lines < line_number
60
57
  counts = recorded_results[line_number] ||= Hash.new(0)
61
58
  count = counts[type]
62
59
  recorded_results[line_number][type] = count.next
@@ -83,25 +80,24 @@ class SeeingIsBelieving
83
80
  value
84
81
  end
85
82
 
83
+ # records the exception, returns the exitstatus for that exception
86
84
  def record_exception(line_number, exception)
87
- self.exitstatus = (exception.kind_of?(SystemExit) ? exception.status : 1)
88
- if line_number
89
- self.num_lines = line_number if num_lines < line_number
90
- elsif filename
91
- begin
92
- line_number = exception.backtrace.grep(/#{filename}/).first[/:\d+/][1..-1].to_i
93
- rescue Exception
85
+ return exception.status if exception.kind_of? SystemExit
86
+ if !line_number && filename
87
+ begin line_number = exception.backtrace.grep(/#{filename}/).first[/:\d+/][1..-1].to_i
88
+ rescue NoMethodError
94
89
  end
95
90
  end
96
91
  line_number ||= -1
97
- queue << "exception"
98
- queue << " line_number #{line_number}"
99
- queue << " class_name #{to_string_token exception.class.name}"
100
- queue << " message #{to_string_token exception.message}"
101
- exception.backtrace.each { |line|
102
- queue << " backtrace #{to_string_token line}"
103
- }
104
- queue << "end"
92
+ queue << [
93
+ "exception",
94
+ line_number,
95
+ to_string_token(exception.class.name),
96
+ to_string_token(exception.message),
97
+ exception.backtrace.size,
98
+ *exception.backtrace.map { |line| to_string_token line }
99
+ ].join(" ")
100
+ 1 # exit status
105
101
  end
106
102
 
107
103
  def record_filename(filename)
@@ -109,11 +105,16 @@ class SeeingIsBelieving
109
105
  queue << "filename #{to_string_token filename}"
110
106
  end
111
107
 
112
- # note that producer will continue reading until stream is closed
113
- def finish!
108
+ def record_exec(args)
109
+ queue << "exec #{to_string_token args.inspect}"
110
+ end
111
+
112
+ def record_num_lines(num_lines)
114
113
  queue << "num_lines #{num_lines}"
115
- queue << "exitstatus #{exitstatus}"
116
- queue << :break
114
+ end
115
+
116
+ def finish!
117
+ queue << :break # note that consumer will continue reading until stream is closed
117
118
  producer_thread.join
118
119
  end
119
120
 
@@ -0,0 +1,206 @@
1
+ class SeeingIsBelieving
2
+ HashStruct = Class.new
3
+
4
+ class << HashStruct
5
+ NoDefault = Module.new
6
+
7
+ def init_blocks
8
+ @init_blocks ||= {}
9
+ end
10
+
11
+ def attribute(name, value=NoDefault, &init_block)
12
+ init_blocks.key?(name) && raise(ArgumentError, "#{name} was already defined")
13
+ name.kind_of?(Symbol) || raise(ArgumentError, "#{name.inspect} should have been a symbol")
14
+
15
+ init_block ||= lambda do |hash_struct|
16
+ if value == NoDefault
17
+ raise ArgumentError, "Must provide a value for #{name.inspect}"
18
+ else
19
+ value
20
+ end
21
+ end
22
+ init_blocks[name] = init_block
23
+ define_method(name) { self[name] }
24
+ define_method(:"#{name}=") { |val| self[name] = val }
25
+
26
+ self
27
+ end
28
+
29
+ def attributes(*names_or_pairs)
30
+ names_or_pairs.each do |norp|
31
+ case norp
32
+ when Symbol then attribute(norp)
33
+ else norp.each { |name, default| attribute name, default }
34
+ end
35
+ end
36
+ self
37
+ end
38
+
39
+ def predicate(name, *rest, &b)
40
+ attribute name, *rest, &b
41
+ define_method(:"#{name}?") { !!self[name] }
42
+ self
43
+ end
44
+
45
+ def predicates(*names_or_pairs)
46
+ names_or_pairs.each do |name_or_pairs|
47
+ name = pairs = name_or_pairs
48
+ name_or_pairs.kind_of?(Symbol) ?
49
+ predicate(name) :
50
+ pairs.each { |name, default| predicate name, default }
51
+ end
52
+ self
53
+ end
54
+
55
+ def anon(&block)
56
+ Class.new self, &block
57
+ end
58
+
59
+ def for(*attributes_args, &block)
60
+ anon(&block).attributes(*attributes_args)
61
+ end
62
+
63
+ def for?(*predicate_args, &block)
64
+ anon(&block).predicates(*predicate_args)
65
+ end
66
+ end
67
+
68
+ class HashStruct
69
+ def self.inspect
70
+ name || "HashStruct.anon"
71
+ end
72
+
73
+ # This could support dynamic attributes very easily
74
+ # ie they are calculated, but appear as a value (e.g. in to_hash)
75
+ # not sure how to deal with the fact that they could be assigned, though
76
+ class Attr
77
+ def initialize(instance, value=nil, &block)
78
+ @instance = instance
79
+ @block = block if block
80
+ @value = value unless block
81
+ end
82
+ def value
83
+ return @value if defined? @value
84
+ @value = @block.call(@instance)
85
+ end
86
+ end
87
+
88
+ # The aggressivenes of this is kind of annoying when you're trying to build up a large hash of values
89
+ # maybe new vs new! one validates arg presence,
90
+ # maybe a separate #validate! method for that?
91
+ def initialize(initial_values={}, &initializer)
92
+ initial_values.respond_to?(:each) ||
93
+ raise(ArgumentError, "#{self.class.inspect} expects to be initialized with a hash-like object, but got #{initial_values.inspect}")
94
+ @attributes = self
95
+ .class
96
+ .ancestors
97
+ .take_while { |ancestor| ancestor != HashStruct }
98
+ .map(&:init_blocks)
99
+ .reverse
100
+ .inject({}, :merge)
101
+ .each_with_object({}) { |(name, block), attrs| attrs[name] = Attr.new(self, &block) }
102
+ initial_values.each { |key, value| self[key] = value }
103
+ initializer.call self if initializer
104
+ each { } # access each key to see if it blows up
105
+ end
106
+
107
+ include Enumerable
108
+ def each(&block)
109
+ return to_enum :each unless block
110
+ @attributes.keys.each do |name|
111
+ block.call(name, self[name])
112
+ end
113
+ end
114
+
115
+ def [](key)
116
+ @attributes[internalize! key].value
117
+ end
118
+
119
+ def []=(key, value)
120
+ @attributes[internalize! key] = Attr.new(self, value)
121
+ end
122
+
123
+ def fetch(key, ignored=nil)
124
+ self[key]
125
+ end
126
+
127
+ def to_hash
128
+ Hash[to_a]
129
+ end
130
+ alias to_h to_hash
131
+
132
+ def merge(overrides)
133
+ self.class.new(to_h.merge overrides)
134
+ end
135
+
136
+ def keys
137
+ to_a.map(&:first)
138
+ end
139
+
140
+ def values
141
+ to_a.map(&:last)
142
+ end
143
+
144
+ def inspect
145
+ classname = self.class.name ? "HashStruct #{self.class.name}" : self.class.inspect
146
+ inspected_attrs = map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
147
+ "#<#{classname}: {#{inspected_attrs}}>"
148
+ end
149
+
150
+ def pretty_print(pp)
151
+ pp.text self.class.name || 'HashStruct.anon { ... }'
152
+ pp.text '.new('
153
+ pp.group 2 do
154
+ pp.breakable '' # place inside so that if we break, we are indented
155
+ last_key = keys.last
156
+ each do |key, value|
157
+ # text-space-value, or text-neline-indent-value
158
+ pp.text "#{key}:"
159
+ pp.group 2 do
160
+ pp.breakable " "
161
+ pp.pp value
162
+ end
163
+ # all lines end in a comma, and can have a newline, except the last
164
+ pp.comma_breakable unless key == last_key
165
+ end
166
+ end
167
+ pp.breakable ''
168
+ pp.text ')'
169
+ end
170
+
171
+ def key?(key)
172
+ key.respond_to?(:to_sym) && @attributes.key?(key.to_sym)
173
+ end
174
+ alias has_key? key?
175
+ alias include? key? # b/c Hash does this
176
+ alias member? key? # b/c Hash does this
177
+
178
+ def ==(other)
179
+ if equal? other
180
+ true
181
+ elsif other.kind_of? Hash
182
+ to_h == other
183
+ elsif other.respond_to?(:to_h)
184
+ to_h == other.to_h
185
+ else
186
+ false
187
+ end
188
+ end
189
+ alias eql? ==
190
+
191
+ # this might be pretty expensive
192
+ def hash
193
+ to_h.hash
194
+ end
195
+
196
+ private
197
+
198
+ def internalize!(key)
199
+ internal = key.to_sym
200
+ @attributes.key?(internal) || raise(KeyError)
201
+ internal
202
+ rescue NoMethodError, KeyError
203
+ raise KeyError, "#{key.inspect} is not an attribute, should be in #{@attributes.keys.inspect}"
204
+ end
205
+ end
206
+ end