ruport 0.2.9 → 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
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