ruport 0.2.9 → 0.3.8

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.
Files changed (60) hide show
  1. data/ACKNOWLEDGEMENTS +33 -0
  2. data/AUTHORS +13 -1
  3. data/CHANGELOG +76 -1
  4. data/README +208 -89
  5. data/Rakefile +12 -8
  6. data/TODO +14 -122
  7. data/lib/ruport.rb +58 -0
  8. data/lib/ruport/config.rb +114 -0
  9. data/lib/ruport/data_row.rb +144 -0
  10. data/lib/ruport/data_set.rb +221 -0
  11. data/lib/ruport/format.rb +116 -0
  12. data/lib/ruport/format/builder.rb +29 -5
  13. data/lib/ruport/format/document.rb +77 -0
  14. data/lib/ruport/format/open_node.rb +36 -0
  15. data/lib/ruport/parser.rb +202 -0
  16. data/lib/ruport/query.rb +208 -0
  17. data/lib/ruport/query/sql_split.rb +33 -0
  18. data/lib/ruport/report.rb +116 -0
  19. data/lib/ruport/report/mailer.rb +17 -15
  20. data/test/{addressbook.csv → samples/addressbook.csv} +0 -0
  21. data/test/samples/car_ads.txt +505 -0
  22. data/test/{data.csv → samples/data.csv} +0 -0
  23. data/test/samples/document.xml +22 -0
  24. data/test/samples/five_lines.txt +5 -0
  25. data/test/samples/five_paragraphs.txt +9 -0
  26. data/test/samples/ross_report.txt +58530 -0
  27. data/test/samples/ruport_test.sql +8 -0
  28. data/test/samples/stonecodeblog.sql +279 -0
  29. data/test/{test.sql → samples/test.sql} +2 -1
  30. data/test/{test.yaml → samples/test.yaml} +0 -0
  31. data/test/tc_builder.rb +7 -4
  32. data/test/tc_config.rb +41 -0
  33. data/test/tc_data_row.rb +16 -26
  34. data/test/tc_data_set.rb +60 -41
  35. data/test/tc_database.rb +25 -0
  36. data/test/tc_document.rb +42 -0
  37. data/test/tc_element.rb +18 -0
  38. data/test/tc_page.rb +42 -0
  39. data/test/tc_query.rb +55 -0
  40. data/test/tc_reading.rb +60 -0
  41. data/test/tc_report.rb +31 -0
  42. data/test/tc_section.rb +45 -0
  43. data/test/tc_sql_split.rb +18 -0
  44. data/test/tc_state.rb +142 -0
  45. data/test/ts_all.rb +6 -3
  46. data/test/ts_format.rb +5 -0
  47. data/test/ts_parser.rb +10 -0
  48. metadata +102 -60
  49. data/bin/ruport +0 -104
  50. data/lib/ruport/format/chart.rb +0 -1
  51. data/lib/ruport/report/data_row.rb +0 -79
  52. data/lib/ruport/report/data_set.rb +0 -153
  53. data/lib/ruport/report/engine.rb +0 -201
  54. data/lib/ruport/report/fake_db.rb +0 -54
  55. data/lib/ruport/report/fake_engine.rb +0 -26
  56. data/lib/ruport/report/fake_mailer.rb +0 -23
  57. data/lib/ruport/report/sql.rb +0 -95
  58. data/lib/ruportlib.rb +0 -11
  59. data/test/tc_engine.rb +0 -102
  60. data/test/tc_mailer.rb +0 -21
@@ -0,0 +1,36 @@
1
+ require "ostruct"
2
+ module Ruport
3
+ class Format
4
+ class OpenNode < OpenStruct
5
+ include Enumerable
6
+ def initialize(my_name, parent_name, children_name, name, options={})
7
+ @my_children_name = children_name
8
+ @my_parent_name = parent_name
9
+ @my_name = my_name
10
+ super(options)
11
+ self.name = name
12
+ self.send(@my_children_name) ||
13
+ self.send("#{@my_children_name}=".to_sym,{})
14
+ end
15
+
16
+ def each &p
17
+ self.send(@my_children_name).values.each(&p)
18
+ end
19
+
20
+ def add_child(klass,name,options={})
21
+ options[@my_name] = self
22
+ self << klass.new(name, options)
23
+ end
24
+
25
+ def <<(child)
26
+ child.send("#{@my_name}=".to_sym, self)
27
+ self.send(@my_children_name)[child.name] = child.dup
28
+ end
29
+
30
+ def [](child_name)
31
+ self.send(@my_children_name)[child_name]
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,202 @@
1
+ #!/usr/local/bin/ruby -w
2
+ # parser.rb, A tool for parsing arbitrary input
3
+ # ______________________ _ _
4
+ # (Here there be dragons!) o / \
5
+ # ---------------------- o | O O |
6
+ # | |
7
+ # .----. \/\/\/\/
8
+ # | 0 0 |
9
+ # | | o _____________________________
10
+ # |/\/\/\| o ( Dude. That phrase is sooo '89)
11
+ # ------------------------------
12
+ #
13
+ # [ but seriously. be careful in here. ]
14
+ #
15
+ # This is Free Software, you may freely modify and/or distribute it by the
16
+ # terms of the GNU General Public License or the Ruby license.
17
+ #
18
+ # See LICENSE and COPYING for details.
19
+ #
20
+ # Based on Parse::Input (http://rubyforge.org/projects/input)
21
+ # Modified version created on 2006.02.20 by Gregory Brown
22
+ # Copyight (C) 2006, all rights reserved.
23
+ #
24
+ # Modifications from Parse::Input include:
25
+ # - Ruport::Parser can handle string input
26
+ # - Ruport::Parser does not define a top level input() method
27
+ # - Any and all documentation has been added post-fork
28
+ # - Ruby Style indentation used
29
+ #
30
+ # Original application:
31
+ # Created by James Edward Gray II on 2005-08-14.
32
+ # Copyright 2005 Gray Productions. All rights reserved.
33
+
34
+ require "stringio"
35
+
36
+ module Ruport
37
+
38
+ # If you're reporting on data, there is a good chance it's going to need some
39
+ # munging. A 100% compatible modified version of JEG2's Parse::Input, Ruport::Parser
40
+ # can help you do that.
41
+ #
42
+ # This class provides a system that lets you specify regular expressions to
43
+ # identify patterns and extract the data you need from your input.
44
+ #
45
+ # It implements a somewhat straightforward domain specific language to help
46
+ # you scrape and tear up your gnarly input and get something managable.
47
+ #
48
+ # It is a bit of an expert tool, but is indispensible when messy data needs to be
49
+ # parsed. For now, you'll probably want to read the source for this particular
50
+ # class. There may be dragons.
51
+ #
52
+ # Sample usage:
53
+ # path = "somefile"
54
+ # data = Ruport::Parser.new(path, "") do
55
+ # @state = :skip
56
+ # stop_skipping_at("Save Ad")
57
+ # skip(/\A\s*\Z/)
58
+ #
59
+ # pre { @price = @miles = nil }
60
+ # read(/\$([\d,]+\d)/) { |price| @price = price.delete(",").to_i }
61
+ # read(/([\d,]*\d)\s*m/) { |miles| @miles = miles.delete(",").to_i }
62
+ #
63
+ # read do |ad|
64
+ # if @price and @price < 20_000 and @miles and @miles < 40_000
65
+ # (@ads ||= Array.new) << ad.strip
66
+ # end
67
+ # end
68
+ # end
69
+ class Parser
70
+
71
+ def initialize( path, separator = $/, &init )
72
+ @path = path
73
+ @separator = separator
74
+ @state = :read
75
+
76
+ @_pre_readers = Array.new
77
+ @_readers = Array.new
78
+ @_post_readers = Array.new
79
+ @_skip_starters = Array.new
80
+ @_skip_stoppers = Array.new
81
+ @_skips = Array.new
82
+ @_skip_searches = Array.new
83
+ @_stops = Array.new
84
+ @_origin = :file
85
+ instance_eval(&init) unless init.nil?
86
+
87
+ parse
88
+ end
89
+
90
+ def []( name )
91
+ variable_name = "@#{name}"
92
+ if instance_variables.include? variable_name
93
+ instance_variable_get(variable_name)
94
+ else
95
+ nil
96
+ end
97
+ end
98
+
99
+ def method_missing( method, *args, &block )
100
+ variable_name = "@#{method}"
101
+ if instance_variables.include? variable_name
102
+ instance_variable_get(variable_name)
103
+ else
104
+ super
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def parse
111
+ io_klass = (@origin.eql? :string) ? StringIO : File
112
+ io_klass.open(@path) do |io|
113
+ while @read = io.gets(@separator)
114
+ if @_stops.any? { |stop| @read.index(stop) }
115
+ @state = :stop
116
+ break
117
+ end
118
+
119
+ case @state
120
+ when :skip
121
+ search(@_skip_searches)
122
+
123
+ if @_skip_stoppers.any? { |stop| @read.index(stop) }
124
+ @state = :read
125
+ end
126
+ when :read
127
+ if @_skip_starters.any? { |start| @read.index(start) }
128
+ @state = :skip
129
+ search(@_skip_searches)
130
+ next
131
+ end
132
+
133
+ if @_skips.any? { |skip| @read.index(skip) }
134
+ search(@_skip_searches)
135
+ next
136
+ end
137
+
138
+ @_pre_readers.each { |pre| instance_eval(&pre) }
139
+ search(@_readers)
140
+ @_post_readers.each { |post| instance_eval(&post) }
141
+ end
142
+
143
+ break if @state == :stop
144
+ end
145
+ end
146
+ end
147
+
148
+ def pre( &pre_readers_handler )
149
+ @_pre_readers << pre_readers_handler
150
+ end
151
+
152
+ def read( pattern = nil, &handler )
153
+ @_readers << Array[pattern, handler]
154
+ end
155
+
156
+ def post( &post_readers_handler )
157
+ @_post_readers << post_readers_handler
158
+ end
159
+
160
+ def search( searches )
161
+ searches.each do |pattern, handler|
162
+ if pattern.nil?
163
+ handler[@read]
164
+ elsif pattern.is_a? Regexp
165
+ next unless md = @read.match(pattern)
166
+ captures = md.captures
167
+ if captures.empty?
168
+ handler[@read]
169
+ else
170
+ handler[*captures]
171
+ end
172
+ elsif @read.index(pattern)
173
+ handler[@read]
174
+ end
175
+ end
176
+ end
177
+
178
+ def find_in_skipped( pattern = nil, &handler )
179
+ @_skip_searches << Array[pattern, handler]
180
+ end
181
+
182
+ def skip( pattern )
183
+ @_skips << pattern
184
+ end
185
+
186
+ def start_skipping_at( pattern )
187
+ @_skip_starters << pattern
188
+ end
189
+
190
+ def stop_skipping_at( pattern )
191
+ @_skip_stoppers << pattern
192
+ end
193
+
194
+ def stop_at( pattern )
195
+ @_stops << pattern
196
+ end
197
+
198
+ def origin(source=:string)
199
+ @_origin = :file
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,208 @@
1
+ require "generator"
2
+ require "ruport/query/sql_split"
3
+
4
+ #this hack looks pointless right now but leaves room to just add more libs
5
+ %w[dbi].each { |lib| begin; require lib;
6
+ rescue LoadError
7
+ Ruport.complain "Couldn't find #{lib}!", :level => :log_only
8
+ end
9
+ }
10
+
11
+ module Ruport
12
+
13
+ # Query offers a way to interact with databases via DBI. It supports
14
+ # returning result sets in either Ruport's native DataSets, or in their raw
15
+ # form as DBI::Rows.
16
+ #
17
+ # It offers basic caching support, the ability to instantiate a generator for
18
+ # a result set, and the ability to quickly and easily swap between data
19
+ # sources.
20
+ class Query
21
+ include Enumerable
22
+
23
+ # Queries are initialized with some SQL and a number of options that effect
24
+ # their operation. They are NOT executed at initialization.
25
+ #
26
+ # This is important to note as they will not query the database until either
27
+ # Query#result, Query#execute, Query#generator, or an enumerable method is
28
+ # called on them.
29
+ #
30
+ # This kind of laziness is supposed to be A Good Thing, and
31
+ # as long as you keep it in mind, it should not cause any problems.
32
+ #
33
+ # The SQL can be single or multistatement, but the resulting DataSet will
34
+ # consist only of the result of the last statement which returns something.
35
+ #
36
+ # Options:
37
+ #
38
+ # <tt>:source</tt>
39
+ # A source specified in Ruport::Config.sources, defaults to :default
40
+ # <tt>:origin</tt>
41
+ # query origin, default to :string, but can be
42
+ # set to :file, loading the path specified by the sql parameter
43
+ # <tt>:dsn</tt>
44
+ # If specifed, the query object will manually override Ruport::Config
45
+ # <tt>:user</tt>
46
+ # If a DSN is specified, a user can be set by this option
47
+ # <tt>:password</tt>
48
+ # If a DSN is specified, a password can be set by this option
49
+ # <tt>:raw_data</tt>
50
+ # When set to true, DBI::Rows will be returned
51
+ # <tt>:cache_enabled</tt>
52
+ # When set to true, Query will download results only once, and then return
53
+ # cached values until cache has been cleared.
54
+ # Examples:
55
+ #
56
+ # # uses Ruport::Config's default source
57
+ # Ruport::Query.new("select * from fo")
58
+ #
59
+ # # uses the Ruport::Config's source labeled :my_source
60
+ # Ruport::Query.new("select * from fo", :source => :my_source)
61
+ #
62
+ # # uses a manually entered source
63
+ # Ruport::Query.new("select * from fo", :dsn => "dbi:mysql:my_db",
64
+ # :user => "greg", :password => "chunky_bacon" )
65
+ #
66
+ # # uses a SQL file stored on disk
67
+ # Ruport::Query.new("my_query.sql",:origin => :file)
68
+ def initialize(sql, options={})
69
+ options[:source] ||= :default
70
+ options[:origin] ||= :string
71
+ @sql = sql
72
+ @statements = SqlSplit.new(get_query(options[:origin],sql))
73
+
74
+ if options[:dsn]
75
+ Ruport::Config.source :temp, :dsn => options[:dsn],
76
+ :user => options[:user],
77
+ :password => options[:password]
78
+ options[:source] = :temp
79
+ end
80
+
81
+ select_source(options[:source])
82
+
83
+ @raw_data = options[:raw_data]
84
+ @cache_enabled = options[:cache_enabled]
85
+ @cached_data = nil
86
+ end
87
+
88
+ # set to true to get DBI:Rows, false to get Ruport constructs
89
+ attr_accessor :raw_data
90
+
91
+ # modifying this might be useful for testing, this is the data stored by
92
+ # ruport when caching
93
+ attr_accessor :cached_data
94
+
95
+ # this is the original SQL for the Query object
96
+ attr_reader :sql
97
+
98
+ # This will set the dsn, username, and password to one specified by a label
99
+ # that corresponds to a source in Ruport::Config
100
+ def select_source(label)
101
+ @dsn = Ruport::Config.sources[label].dsn
102
+ @user = Ruport::Config.sources[label].user
103
+ @password = Ruport::Config.sources[label].password
104
+ end
105
+
106
+ # Standard each iterator, iterates through result set row by row.
107
+ def each(&action)
108
+ Ruport::complain(
109
+ "no block given!", :status => :fatal, :exception => LocalJumpError
110
+ ) unless action
111
+ fetch &action
112
+ end
113
+
114
+ # Grabs the result set as a DataSet or if in raw_data mode, an array of
115
+ # DBI:Row objects
116
+ def result; fetch; end
117
+
118
+ # Runs the query without returning it's results.
119
+ def execute; fetch; nil; end
120
+
121
+ # clears the contents of the cache
122
+ def clear_cache
123
+ @cached_data = nil
124
+ end
125
+
126
+ # clears the contents of the cache and then runs the query, filling the
127
+ # cache with the new result
128
+ def update_cache
129
+ clear_cache
130
+ caching_flag,@cache_enabled = @cache_enabled, true
131
+ fetch; @cache_enabled = caching_flag
132
+ end
133
+
134
+ # Turns on caching. New data will not be loaded until cache is clear or
135
+ # caching is disabled.
136
+ def enable_caching
137
+ @cache_enabled = true
138
+ end
139
+
140
+ # Turns off caching and flushes the cached data
141
+ def disable_caching
142
+ @cached_data = nil
143
+ @cache_enabled = false
144
+ end
145
+
146
+ # Returns a DataSet, even if in raw_data mode
147
+ def to_dataset
148
+ data_flag, @raw_data = @raw_data, false
149
+ data = fetch; @raw_data = data_flag; return data
150
+ end
151
+
152
+ # Returns a csv dump of the query
153
+ def to_csv
154
+ to_dataset.to_csv
155
+ end
156
+
157
+ # Returns a Generator object of the result set
158
+ def generator
159
+ Generator.new(fetch)
160
+ end
161
+
162
+ private
163
+
164
+ def query_data( query_text )
165
+ data = @raw_data ? [] : DataSet.new
166
+ DBI.connect(@dsn, @user, @password) do |dbh|
167
+ dbh.execute(query_text) do |sth|
168
+ return unless sth.fetchable?
169
+ results = sth.fetch_all
170
+ data.fields = sth.column_names unless @raw_data
171
+ results.each { |row| data << row }
172
+ end
173
+ end
174
+ data
175
+ rescue
176
+ nil
177
+ end
178
+
179
+ def get_query(type,query)
180
+ case (type)
181
+ when :string
182
+ query
183
+ when :file
184
+ load_file( query )
185
+ end
186
+ end
187
+
188
+ def load_file( query_file )
189
+ begin; File.read( query_file ).strip ; rescue
190
+ Ruport::complain "Could not open #{query_file}",
191
+ :status => :fatal, :exception => LoadError
192
+ end
193
+ end
194
+
195
+ def fetch(&action)
196
+ data = nil
197
+ if @cache_enabled and @cached_data
198
+ data = @cached_data
199
+ else
200
+ @statements.each { |query_text| data = query_data( query_text ) }
201
+ end
202
+ data.each { |r| action.call(r) } if block_given? ; data
203
+ @cached_data = data if @cache_enabled
204
+ return data
205
+ end
206
+
207
+ end
208
+ end
@@ -0,0 +1,33 @@
1
+ #--
2
+ # sqlsplit.rb : A tool to properly split SQL input
3
+ #
4
+ # This is Free Software. You may freely redistribute or modify under the terms
5
+ # of the GNU General Public License or the Ruby License. See LICENSE and
6
+ # COPYING for details.
7
+ #
8
+ # Created by Francis Hwang, 2005.12.31
9
+ # Copyright (c) 2005, All Rights Reserved.
10
+ #++
11
+ module Ruport
12
+ class Query
13
+ # This class properly splits up multi-statement SQL input for use with
14
+ # Ruby/DBI
15
+ class SqlSplit < Array
16
+ def initialize( sql )
17
+ super()
18
+ next_sql = ''
19
+ sql.each do |line|
20
+ unless line =~ /^--/ or line =~ %r{^/\*.*\*/;} or line =~ /^\s*$/
21
+ next_sql << line
22
+ if line =~ /;$/
23
+ next_sql.gsub!( /;\s$/, '' )
24
+ self << next_sql
25
+ next_sql = ''
26
+ end
27
+ end
28
+ end
29
+ self << next_sql if next_sql != ''
30
+ end
31
+ end
32
+ end
33
+ end