seeing_is_believing 3.0.0.beta.4 → 3.0.0.beta.5
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.
- checksums.yaml +4 -4
- data/.travis.yml +0 -8
- data/Rakefile +1 -1
- data/Readme.md +65 -25
- data/bin/seeing_is_believing +1 -0
- data/docs/sib-streaming.gif +0 -0
- data/features/deprecated-flags.feature +62 -2
- data/features/errors.feature +12 -7
- data/features/examples.feature +143 -4
- data/features/flags.feature +89 -29
- data/features/regression.feature +58 -14
- data/features/support/env.rb +4 -0
- data/features/xmpfilter-style.feature +181 -36
- data/lib/seeing_is_believing.rb +44 -33
- data/lib/seeing_is_believing/binary.rb +31 -88
- data/lib/seeing_is_believing/binary/align_chunk.rb +30 -11
- data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +10 -16
- data/lib/seeing_is_believing/binary/annotate_every_line.rb +5 -25
- data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +136 -0
- data/lib/seeing_is_believing/binary/comment_lines.rb +8 -10
- data/lib/seeing_is_believing/binary/commentable_lines.rb +20 -26
- data/lib/seeing_is_believing/binary/config.rb +392 -0
- data/lib/seeing_is_believing/binary/data_structures.rb +57 -0
- data/lib/seeing_is_believing/binary/engine.rb +104 -0
- data/lib/seeing_is_believing/binary/{comment_formatter.rb → format_comment.rb} +6 -6
- data/lib/seeing_is_believing/binary/remove_annotations.rb +29 -28
- data/lib/seeing_is_believing/binary/rewrite_comments.rb +42 -43
- data/lib/seeing_is_believing/code.rb +105 -49
- data/lib/seeing_is_believing/debugger.rb +6 -5
- data/lib/seeing_is_believing/error.rb +6 -17
- data/lib/seeing_is_believing/evaluate_by_moving_files.rb +78 -129
- data/lib/seeing_is_believing/event_stream/consumer.rb +114 -64
- data/lib/seeing_is_believing/event_stream/events.rb +169 -11
- data/lib/seeing_is_believing/event_stream/handlers/debug.rb +57 -0
- data/lib/seeing_is_believing/event_stream/handlers/record_exitstatus.rb +18 -0
- data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +45 -0
- data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +39 -0
- data/lib/seeing_is_believing/event_stream/producer.rb +25 -24
- data/lib/seeing_is_believing/hash_struct.rb +206 -0
- data/lib/seeing_is_believing/result.rb +20 -3
- data/lib/seeing_is_believing/the_matrix.rb +20 -12
- data/lib/seeing_is_believing/version.rb +1 -1
- data/lib/seeing_is_believing/wrap_expressions.rb +55 -115
- data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +14 -0
- data/seeing_is_believing.gemspec +1 -1
- data/spec/binary/alignment_specs.rb +27 -0
- data/spec/binary/comment_lines_spec.rb +3 -2
- data/spec/binary/config_spec.rb +657 -0
- data/spec/binary/engine_spec.rb +97 -0
- data/spec/binary/{comment_formatter_spec.rb → format_comment_spec.rb} +2 -2
- data/spec/binary/marker_spec.rb +71 -0
- data/spec/binary/options_spec.rb +0 -0
- data/spec/binary/remove_annotations_spec.rb +31 -18
- data/spec/binary/rewrite_comments_spec.rb +26 -11
- data/spec/code_spec.rb +190 -6
- data/spec/debugger_spec.rb +4 -0
- data/spec/evaluate_by_moving_files_spec.rb +38 -20
- data/spec/event_stream_spec.rb +265 -116
- data/spec/hash_struct_spec.rb +514 -0
- data/spec/seeing_is_believing_spec.rb +108 -46
- data/spec/spec_helper.rb +9 -0
- data/spec/wrap_expressions_spec.rb +207 -172
- metadata +30 -18
- data/docs/for-presentations +0 -33
- data/lib/seeing_is_believing/binary/annotate_xmpfilter_style.rb +0 -128
- data/lib/seeing_is_believing/binary/interpret_flags.rb +0 -156
- data/lib/seeing_is_believing/binary/parse_args.rb +0 -263
- data/lib/seeing_is_believing/event_stream/update_result.rb +0 -24
- data/lib/seeing_is_believing/inspect_expressions.rb +0 -21
- data/lib/seeing_is_believing/parser_helpers.rb +0 -82
- data/spec/binary/interpret_flags_spec.rb +0 -332
- 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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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 :
|
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
|
-
|
88
|
-
if line_number
|
89
|
-
|
90
|
-
|
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 <<
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
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
|