ruport 0.2.9 → 0.3.8
Sign up to get free protection for your applications and to get access to all the features.
- data/ACKNOWLEDGEMENTS +33 -0
- data/AUTHORS +13 -1
- data/CHANGELOG +76 -1
- data/README +208 -89
- data/Rakefile +12 -8
- data/TODO +14 -122
- data/lib/ruport.rb +58 -0
- data/lib/ruport/config.rb +114 -0
- data/lib/ruport/data_row.rb +144 -0
- data/lib/ruport/data_set.rb +221 -0
- data/lib/ruport/format.rb +116 -0
- data/lib/ruport/format/builder.rb +29 -5
- data/lib/ruport/format/document.rb +77 -0
- data/lib/ruport/format/open_node.rb +36 -0
- data/lib/ruport/parser.rb +202 -0
- data/lib/ruport/query.rb +208 -0
- data/lib/ruport/query/sql_split.rb +33 -0
- data/lib/ruport/report.rb +116 -0
- data/lib/ruport/report/mailer.rb +17 -15
- data/test/{addressbook.csv → samples/addressbook.csv} +0 -0
- data/test/samples/car_ads.txt +505 -0
- data/test/{data.csv → samples/data.csv} +0 -0
- data/test/samples/document.xml +22 -0
- data/test/samples/five_lines.txt +5 -0
- data/test/samples/five_paragraphs.txt +9 -0
- data/test/samples/ross_report.txt +58530 -0
- data/test/samples/ruport_test.sql +8 -0
- data/test/samples/stonecodeblog.sql +279 -0
- data/test/{test.sql → samples/test.sql} +2 -1
- data/test/{test.yaml → samples/test.yaml} +0 -0
- data/test/tc_builder.rb +7 -4
- data/test/tc_config.rb +41 -0
- data/test/tc_data_row.rb +16 -26
- data/test/tc_data_set.rb +60 -41
- data/test/tc_database.rb +25 -0
- data/test/tc_document.rb +42 -0
- data/test/tc_element.rb +18 -0
- data/test/tc_page.rb +42 -0
- data/test/tc_query.rb +55 -0
- data/test/tc_reading.rb +60 -0
- data/test/tc_report.rb +31 -0
- data/test/tc_section.rb +45 -0
- data/test/tc_sql_split.rb +18 -0
- data/test/tc_state.rb +142 -0
- data/test/ts_all.rb +6 -3
- data/test/ts_format.rb +5 -0
- data/test/ts_parser.rb +10 -0
- metadata +102 -60
- data/bin/ruport +0 -104
- data/lib/ruport/format/chart.rb +0 -1
- data/lib/ruport/report/data_row.rb +0 -79
- data/lib/ruport/report/data_set.rb +0 -153
- data/lib/ruport/report/engine.rb +0 -201
- data/lib/ruport/report/fake_db.rb +0 -54
- data/lib/ruport/report/fake_engine.rb +0 -26
- data/lib/ruport/report/fake_mailer.rb +0 -23
- data/lib/ruport/report/sql.rb +0 -95
- data/lib/ruportlib.rb +0 -11
- data/test/tc_engine.rb +0 -102
- data/test/tc_mailer.rb +0 -21
data/TODO
CHANGED
@@ -1,135 +1,27 @@
|
|
1
|
-
TODO:
|
1
|
+
TODO: (Wiped clean for a fresh start as of 2006.02.20)
|
2
2
|
|
3
|
-
|
4
|
-
- Fix the manual that is in the example package so it it reads FakeDB instead
|
5
|
-
of MockDB. Update the manual to cover new features in 0.3.0
|
3
|
+
For Ruport 0.4.0
|
6
4
|
|
7
|
-
|
8
|
-
creations. Gotta work out the kinks here.
|
9
|
-
|
10
|
-
Improvement:
|
5
|
+
- Integrate Ruport::Parser into Report#parse and Format#parser
|
11
6
|
|
12
|
-
-
|
7
|
+
- Document the inner classes of Format
|
13
8
|
|
14
|
-
-
|
9
|
+
- Get unit tests up to 100% coverage
|
15
10
|
|
16
|
-
-
|
17
|
-
Make attachments doable
|
11
|
+
- make the Fetchable module to abstract data acquisition
|
18
12
|
|
19
|
-
|
13
|
+
High Priority Goals:
|
20
14
|
|
21
|
-
|
22
|
-
Make line editing work right
|
23
|
-
|
24
|
-
- Queries Table System
|
15
|
+
- Implement some aspects of XST into DataSet
|
25
16
|
|
26
|
-
|
27
|
-
Hook up unit tests.
|
17
|
+
- Offer charting support in Format
|
28
18
|
|
29
|
-
|
19
|
+
Community Requests:
|
30
20
|
|
31
|
-
|
32
|
-
Form better more complete unit tests.
|
21
|
+
- Integration with Rails (ActiveRecord)
|
33
22
|
|
34
|
-
|
23
|
+
Other (Unordered) Goals:
|
35
24
|
|
36
|
-
|
25
|
+
- Make mailer more robust via MailFactory
|
37
26
|
|
38
|
-
-
|
39
|
-
|
40
|
-
* Implement RSS generation for Format::Builder
|
41
|
-
|
42
|
-
- Charts:
|
43
|
-
|
44
|
-
Build something that'll take a data set and build a chart. (SVG?),
|
45
|
-
then dump it into PDF::Writer.
|
46
|
-
|
47
|
-
- Multiple query reports:
|
48
|
-
|
49
|
-
Allow the user to make arbitrary SQL queries,
|
50
|
-
combining the rows, and then iterating through them row by row in the
|
51
|
-
order specified.
|
52
|
-
|
53
|
-
This could be done with select taking an array of queries and possibly
|
54
|
-
some ordering instructions
|
55
|
-
|
56
|
-
- Cross database import from file:
|
57
|
-
|
58
|
-
Make an import(file,table,delimiter=",") command that will work
|
59
|
-
regardless of database choice and import the data stored in a file into
|
60
|
-
a specified table.
|
61
|
-
|
62
|
-
- Parse/Input tight integration:
|
63
|
-
|
64
|
-
Write wrapper functions over the Parse/Input library to give Ruport the
|
65
|
-
power to feed in any data source such as a CSV, A website, or a log file
|
66
|
-
and do formatting and use the other features of Ruport.
|
67
|
-
|
68
|
-
- PDF::Writer and CSV convenience methods:
|
69
|
-
|
70
|
-
Create functions that will allow basic and common reports to be built
|
71
|
-
using PDF and CSV format without having to write the functionality over
|
72
|
-
and over again. This would be especially useful for just dumping a
|
73
|
-
table's values in a certain order with some fields removed.
|
74
|
-
|
75
|
-
[UPDATE: to_csv is in place but can use some additional frosting]
|
76
|
-
|
77
|
-
- Logger / Exception handler:
|
78
|
-
|
79
|
-
Create a system to handle and log errors as well as provide log messages
|
80
|
-
regarding what Ruport does when a template is run.
|
81
|
-
|
82
|
-
[UPDATE: Logger is in place but is not covering many functions]
|
83
|
-
|
84
|
-
- DataSets should implement tagging, allowing tag conditions to be passed and
|
85
|
-
then applied to rows. Formulas should be implemented, allowing column
|
86
|
-
generation by Proc. More about this later. (These are COOL and POWERFUL
|
87
|
-
features... thanks to Greg Gibson for the ideas.
|
88
|
-
|
89
|
-
|
90
|
-
Design considerations:
|
91
|
-
|
92
|
-
Ruport will eventually need to be cleanly seperating between formatting
|
93
|
-
tools and data feeders. This will likely happen in an early release, if not
|
94
|
-
the next than the one after.
|
95
|
-
|
96
|
-
[ UPDATE: The folders have been seperated but need reworking. db/mailer ??
|
97
|
-
Ruport needs to be made into a module ]
|
98
|
-
|
99
|
-
Ruport trys to use a lot of 'intelligent' defaults but it might try to be
|
100
|
-
too clever in some places making it not clever at all. The community
|
101
|
-
reaction will determine what places may need opening up more or need more
|
102
|
-
consideration for the defaults.
|
103
|
-
|
104
|
-
[ UPDATE: See changelog for a list of dropped features. Complain about any
|
105
|
-
that you still think need to be dropped ]
|
106
|
-
|
107
|
-
Other:
|
108
|
-
|
109
|
-
A quick reference page for templates and/or tutorial would be A Good Thing
|
110
|
-
|
111
|
-
The RDOC sucks!
|
112
|
-
|
113
|
-
The unit tests need to be wired so that they can actually run without a ton
|
114
|
-
of additional steps, they need to be expanded to cover the whole system, and
|
115
|
-
they need to work in the Gem package as well as the source package.
|
116
|
-
|
117
|
-
[UPDATE: Mailer is the only class that needs to be unit tested still. The
|
118
|
-
units work out of the box via MockDB now. Functional tests with
|
119
|
-
scaffolding are needed, though ]
|
120
|
-
|
121
|
-
Ruport needs to be modified to support as many databases as possibly
|
122
|
-
internally. The system was written using MySQL on Gentoo Linux
|
123
|
-
and Mac OS X.3 / OS X.4, Windows 2000 / XP and Microsoft SQL Server
|
124
|
-
on Windows 2000 (via ODBC),
|
125
|
-
|
126
|
-
so the support for these platforms are best. An effort will be given to
|
127
|
-
make Ruport less hostile in other environments.
|
128
|
-
|
129
|
-
|
130
|
-
This is only the tip of the iceburg. Please feel free to continue to fill my
|
131
|
-
plate by sending any suggestions to gregory.t.brown@gmail.com
|
132
|
-
|
133
|
-
JEG2 Code Review 2005.11.14: (email me if you find any of this interesting)
|
134
|
-
- use the many levels of logger
|
135
|
-
- RQL (Ruport Query Language)
|
27
|
+
- Support KirbyBase
|
data/lib/ruport.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
# ruport.rb : Ruby Reports toplevel module
|
3
|
+
#
|
4
|
+
# Author: Gregory T. Brown (gregory.t.brown at gmail dot com)
|
5
|
+
#
|
6
|
+
# Copyright (c) 2006, All Rights Reserved.
|
7
|
+
#
|
8
|
+
# This is free software. You may modify and redistribute this freely under
|
9
|
+
# your choice of the GNU General Public License or the Ruby License.
|
10
|
+
#
|
11
|
+
# See LICENSE and COPYING for details
|
12
|
+
#
|
13
|
+
|
14
|
+
module Ruport
|
15
|
+
VERSION = "Ruport Version 0.3.8 (Developmental)"
|
16
|
+
|
17
|
+
# Ruports logging and error interface.
|
18
|
+
# Can generate warnings or raise fatal errors
|
19
|
+
#
|
20
|
+
# Takes a message to display and a set of options.
|
21
|
+
# Will log to the file defined by Config::log_file
|
22
|
+
#
|
23
|
+
# Options:
|
24
|
+
# <tt>:status</tt>:: sets the severity level. defaults to <tt>:warn</tt>
|
25
|
+
# <tt>:output</tt>:: optional secondary output, defaults to <tt>$stderr</tt>
|
26
|
+
# <tt>:level</tt>:: set to <tt>:log_only</tt> to disable secondary output
|
27
|
+
# <tt>:exception</tt>:: exception to throw on fail. Defaults to RunTimeError
|
28
|
+
#
|
29
|
+
# The status <tt>:warn</tt> will invoke Logger#warn. A status of
|
30
|
+
# <tt>:fatal</tt> will invoke Logger#fatal and raise an exception
|
31
|
+
#
|
32
|
+
# By default, complain will also print warnings to $stderr
|
33
|
+
# You can redirect this to any I/O object via <tt>:output</tt>
|
34
|
+
#
|
35
|
+
# You can prevent messages from appearing on the secondary output by setting
|
36
|
+
# <tt>:level</tt> to <tt>:log_only</tt>
|
37
|
+
#
|
38
|
+
# If you want to recover these messages to secondary output for debugging, you
|
39
|
+
# can use Config::enable_paranoia
|
40
|
+
def Ruport.complain(message,options={})
|
41
|
+
options[:status] ||= :warn
|
42
|
+
options[:output] ||= $stderr
|
43
|
+
case(options[:status])
|
44
|
+
when :warn
|
45
|
+
Ruport::Config::logger.warn(message) if Ruport::Config::logger
|
46
|
+
when :fatal
|
47
|
+
Ruport::Config::logger.fatal(message) if Ruport::Config::logger
|
48
|
+
raise options[:exception] || RuntimeError, message
|
49
|
+
end
|
50
|
+
options[:output].puts "[!!] #{message}" unless
|
51
|
+
options[:level].eql?(:log_only) and not Ruport::Config.paranoid?
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
%w[ config report format query data_row data_set].each { |lib|
|
57
|
+
require "ruport/#{lib}"
|
58
|
+
}
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# ruport/config.rb : Ruby Reports configuration system
|
2
|
+
#
|
3
|
+
# Author: Gregory T. Brown (gregory.t.brown at gmail dot com)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2006, All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This is free software. You may modify and redistribute this freely under
|
8
|
+
# your choice of the GNU General Public License or the Ruby License.
|
9
|
+
#
|
10
|
+
# See LICENSE and COPYING for details
|
11
|
+
#
|
12
|
+
require "singleton"
|
13
|
+
require "ostruct"
|
14
|
+
module Ruport
|
15
|
+
# This class serves as the configuration system for Ruport.
|
16
|
+
# It's functionality is implemented through Config::method_missing
|
17
|
+
#
|
18
|
+
# source :default and mailer :default will become the fallback values if one
|
19
|
+
# is not specified in Report::Mailer or Query, but you may define as many
|
20
|
+
# sources as you like and switch between them later.
|
21
|
+
#
|
22
|
+
# An example config file is shown below:
|
23
|
+
#
|
24
|
+
# # password is optional, dsn may omit hostname for localhost
|
25
|
+
# Ruport::Config.source :default,
|
26
|
+
# :dsn => "dbi:mysql:somedb:db.blixy.org", :user => "root", :password => "chunky_bacon"
|
27
|
+
#
|
28
|
+
# # :password, :port, and :auth_type are optional. :port defaults to 25 and
|
29
|
+
# # :auth_type defaults to :plain. For more information, see the source
|
30
|
+
# # of Report::Mailer#select_mailer
|
31
|
+
# Ruport::Config.mailer :default,
|
32
|
+
# :host => "mail.chunkybacon.org", :address => "chunky@bacon.net",
|
33
|
+
# :user => "cartoon", :password => "fox", :port => 25, :auth_type => :login
|
34
|
+
#
|
35
|
+
# # optional, if specifed, Ruport#complain will report to it
|
36
|
+
# Ruport::Config.log_file 'foo.log'
|
37
|
+
#
|
38
|
+
# # optional, if enabled, will force :log_only complaint calls to
|
39
|
+
# # print to secondary output ($sterr by default).
|
40
|
+
# # call Ruport::Config.disable_paranoia to disable
|
41
|
+
# Ruport::Config.enable_paranoia
|
42
|
+
#
|
43
|
+
# Alternatively, this configuration could be done by opening the class:
|
44
|
+
# class Ruport::Config
|
45
|
+
#
|
46
|
+
# source :default, :dsn => "dbi:mysql:some_db", :user => "root"
|
47
|
+
#
|
48
|
+
# mailer :default, :host => "mail.iheartwhy.com",
|
49
|
+
# :address => "sandal@ruby-harmonix.net", :user => "sandal",
|
50
|
+
# :password => "abc123"
|
51
|
+
#
|
52
|
+
# logfile 'foo.log'
|
53
|
+
#
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# Saving this config information into a file and then requiring it can allow
|
57
|
+
# you share configurations between Ruport applications.
|
58
|
+
#
|
59
|
+
class Config
|
60
|
+
include Singleton
|
61
|
+
|
62
|
+
def Config.method_missing(method_id,*args)
|
63
|
+
case(method_id)
|
64
|
+
when :source
|
65
|
+
return @@sources[args.first] if args.length == 1
|
66
|
+
@@sources[args.first] = OpenStruct.new(*args[1..-1])
|
67
|
+
unless @@sources[args.first].send(:dsn)
|
68
|
+
Ruport.complain("Bad or missing DSN for source #{args.first}!")
|
69
|
+
end
|
70
|
+
when :mailer
|
71
|
+
@@mailers[args.first] = OpenStruct.new(*args[1..-1])
|
72
|
+
when :log_file
|
73
|
+
@@logger = Logger.new(args.first)
|
74
|
+
when :default_source
|
75
|
+
@@sources[:default]
|
76
|
+
when :default_mailer
|
77
|
+
@@mailers[:default]
|
78
|
+
when :sources
|
79
|
+
@@sources
|
80
|
+
when :mailers
|
81
|
+
@@mailers
|
82
|
+
when :logger
|
83
|
+
@@logger
|
84
|
+
when :enable_paranoia
|
85
|
+
@@paranoid = true
|
86
|
+
when :disable_paranoia
|
87
|
+
@@paranoid = false
|
88
|
+
when :paranoid?
|
89
|
+
@@paranoid
|
90
|
+
else
|
91
|
+
super
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def Config.init!
|
98
|
+
@@sources = { :default =>
|
99
|
+
OpenStruct.new( :dsn => "ruport",
|
100
|
+
:user => "",
|
101
|
+
:password => ""
|
102
|
+
)
|
103
|
+
}
|
104
|
+
@@mailers = { :default => nil }
|
105
|
+
@@logger ||= nil
|
106
|
+
@@paranoid ||= false
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
|
111
|
+
init!
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# --
|
2
|
+
# data_row.rb : Ruby Reports row abstraction
|
3
|
+
#
|
4
|
+
# Author: Gregory T. Brown (gregory.t.brown at gmail dot com)
|
5
|
+
#
|
6
|
+
# Copyright (c) 2006, All Rights Reserved.
|
7
|
+
#
|
8
|
+
# This is free software. You may modify and redistribute this freely under
|
9
|
+
# your choice of the GNU General Public License or the Ruby License.
|
10
|
+
#
|
11
|
+
# See LICENSE and COPYING for details
|
12
|
+
# ++
|
13
|
+
module Ruport
|
14
|
+
|
15
|
+
# DataRows are Enumerable lists which can be accessed by field name or ordinal
|
16
|
+
# position.
|
17
|
+
#
|
18
|
+
# They feature a tagging system, allowing them to be easily
|
19
|
+
# compared or recalled.
|
20
|
+
#
|
21
|
+
# DataRows form the elements of DataSets
|
22
|
+
#
|
23
|
+
class DataRow
|
24
|
+
|
25
|
+
include Enumerable
|
26
|
+
|
27
|
+
# Takes data and field names as well as some optional parameters and
|
28
|
+
# constructs a DataRow.
|
29
|
+
#
|
30
|
+
#
|
31
|
+
# <tt>data</tt> can be specified in Hash, Array, or DataRow form
|
32
|
+
#
|
33
|
+
# Options:
|
34
|
+
# <tt>:filler</tt>:: this will be used as a default value for empty
|
35
|
+
# <tt>:tags</tt>:: an initial set of tags for the row
|
36
|
+
#
|
37
|
+
#
|
38
|
+
# Examples:
|
39
|
+
# >> Ruport::DataRow.new [1,2,3,4,5], [:a,:b,:c,:d,:e],
|
40
|
+
# :tags => %w[cat dog]
|
41
|
+
# => #<Ruport::DataRow:0xb77e4b04 @fields=[:a, :b, :c, :d, :e],
|
42
|
+
# @data=[1, 2, 3, 4, 5], @tags=["cat", "dog"]>
|
43
|
+
#
|
44
|
+
# >> Ruport::DataRow.new({ :a => 'moo', :c => 'caw'} , [:a,:b,:c,:d,:e],
|
45
|
+
# :tags => %w[cat dog])
|
46
|
+
# => #<Ruport::DataRow:0xb77c298c @fields=[:a, :b, :c, :d, :e],
|
47
|
+
# @data=["moo", nil, "caw", nil, nil], @tags=["cat", "dog"]>
|
48
|
+
#
|
49
|
+
# >> Ruport::DataRow.new [1,2,3], [:a,:b,:c,:d,:e], :tags => %w[cat dog],
|
50
|
+
# :filler => 0
|
51
|
+
# => #<Ruport::DataRow:0xb77bb4d4 @fields=[:a, :b, :c, :d, :e],
|
52
|
+
# @data=[1, 2, 3, 0, 0], @tags=["cat", "dog"]>
|
53
|
+
#
|
54
|
+
def initialize( data, fields, options={} )
|
55
|
+
@fields = fields
|
56
|
+
@tags = options[:tags] || {}
|
57
|
+
@data = []
|
58
|
+
nr_action =
|
59
|
+
if data.kind_of?(Array)
|
60
|
+
lambda { |key, index| @data[index] = data.shift || options[:filler] }
|
61
|
+
elsif data.kind_of?(DataRow)
|
62
|
+
lambda { |key, index| @data = data.to_a }
|
63
|
+
else
|
64
|
+
lambda { |key, index| @data[index] = data[key] || options[:filler] }
|
65
|
+
end
|
66
|
+
@fields.each_with_index { |key, index| nr_action.call(key,index) }
|
67
|
+
end
|
68
|
+
|
69
|
+
attr_accessor :fields, :tags
|
70
|
+
|
71
|
+
# Returns an array of values. Should probably return a DataRow.
|
72
|
+
# Loses field information.
|
73
|
+
def +(other)
|
74
|
+
self.to_a + other.to_a
|
75
|
+
end
|
76
|
+
|
77
|
+
# Lets you access individual fields
|
78
|
+
#
|
79
|
+
# i.e. row["phone"] or row[4]
|
80
|
+
def [](key)
|
81
|
+
key.kind_of?(Fixnum) ? @data[key] : @data[@fields.index(key)]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Lets you set field values
|
85
|
+
#
|
86
|
+
# i.e. row["phone"] = '2038291203', row[7] = "allen"
|
87
|
+
def []=(key,value)
|
88
|
+
if key.kind_of?(Fixnum)
|
89
|
+
@data[key] = value
|
90
|
+
else
|
91
|
+
@data[@fields.index(key)] = value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Converts the DataRow to a plain old Array
|
96
|
+
def to_a
|
97
|
+
@data
|
98
|
+
end
|
99
|
+
|
100
|
+
# Converts the DataRow to a string representation
|
101
|
+
# for outputting to screen.
|
102
|
+
def to_s
|
103
|
+
"[" + @data.join(",") + "]"
|
104
|
+
end
|
105
|
+
|
106
|
+
# Checks to see row includes the tag given.
|
107
|
+
#
|
108
|
+
# Example:
|
109
|
+
#
|
110
|
+
# >> row.has_tag? :running_balance
|
111
|
+
# => true
|
112
|
+
#
|
113
|
+
def has_tag?(tag)
|
114
|
+
@tags.include?(tag)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Iterates through DataRow elements. Accepts a block.
|
118
|
+
def each(&action)
|
119
|
+
@data.each(&action)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Allows you to add a tag to a row.
|
123
|
+
#
|
124
|
+
# Examples:
|
125
|
+
#
|
126
|
+
# row.tag_as(:jay_cross) if row["product"].eql?("im_courier")
|
127
|
+
# row.tag_as(:running_balance) if row.fields.include?("RB")
|
128
|
+
#
|
129
|
+
def tag_as(something)
|
130
|
+
@tags[something] = true
|
131
|
+
end
|
132
|
+
|
133
|
+
# Compares two DataRow objects. If values and fields are the same
|
134
|
+
# (and in the correct order) returns true. Otherwise returns false.
|
135
|
+
def ==(other)
|
136
|
+
self.to_a.eql?(other.to_a) && @fields.eql?(other.fields)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Synonym for DataRow#==
|
140
|
+
def eql?
|
141
|
+
self == other
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|