lean-ruport 0.3.8

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of lean-ruport might be problematic. Click here for more details.

Files changed (50) hide show
  1. data/ACKNOWLEDGEMENTS +33 -0
  2. data/AUTHORS +19 -0
  3. data/CHANGELOG +206 -0
  4. data/COPYING +340 -0
  5. data/LICENSE +7 -0
  6. data/README +209 -0
  7. data/Rakefile +54 -0
  8. data/TODO +27 -0
  9. data/lib/ruport.rb +58 -0
  10. data/lib/ruport/config.rb +114 -0
  11. data/lib/ruport/data_row.rb +144 -0
  12. data/lib/ruport/data_set.rb +221 -0
  13. data/lib/ruport/format.rb +116 -0
  14. data/lib/ruport/format/builder.rb +89 -0
  15. data/lib/ruport/format/document.rb +77 -0
  16. data/lib/ruport/format/open_node.rb +36 -0
  17. data/lib/ruport/parser.rb +202 -0
  18. data/lib/ruport/query.rb +208 -0
  19. data/lib/ruport/query/sql_split.rb +33 -0
  20. data/lib/ruport/report.rb +116 -0
  21. data/lib/ruport/report/mailer.rb +48 -0
  22. data/test/samples/addressbook.csv +6 -0
  23. data/test/samples/car_ads.txt +505 -0
  24. data/test/samples/data.csv +3 -0
  25. data/test/samples/document.xml +22 -0
  26. data/test/samples/five_lines.txt +5 -0
  27. data/test/samples/five_paragraphs.txt +9 -0
  28. data/test/samples/ross_report.txt +58530 -0
  29. data/test/samples/ruport_test.sql +8 -0
  30. data/test/samples/stonecodeblog.sql +279 -0
  31. data/test/samples/test.sql +2 -0
  32. data/test/samples/test.yaml +3 -0
  33. data/test/tc_builder.rb +116 -0
  34. data/test/tc_config.rb +41 -0
  35. data/test/tc_data_row.rb +36 -0
  36. data/test/tc_data_set.rb +141 -0
  37. data/test/tc_database.rb +25 -0
  38. data/test/tc_document.rb +42 -0
  39. data/test/tc_element.rb +18 -0
  40. data/test/tc_page.rb +42 -0
  41. data/test/tc_query.rb +55 -0
  42. data/test/tc_reading.rb +60 -0
  43. data/test/tc_report.rb +31 -0
  44. data/test/tc_section.rb +45 -0
  45. data/test/tc_sql_split.rb +18 -0
  46. data/test/tc_state.rb +142 -0
  47. data/test/ts_all.rb +9 -0
  48. data/test/ts_format.rb +5 -0
  49. data/test/ts_parser.rb +10 -0
  50. metadata +102 -0
@@ -0,0 +1,89 @@
1
+ module Ruport
2
+ class Format
3
+ class Builder
4
+
5
+ def initialize( data_set )
6
+ @data = data_set
7
+ @original = data_set.dup
8
+ @format = nil
9
+ @range = nil
10
+ @header = nil
11
+ @footer = nil
12
+ @output_type = nil
13
+ end
14
+
15
+ attr_accessor :format, :range, :header, :footer, :output_type
16
+
17
+ def render
18
+ @data = @range ? ( @original[@range] ) : ( @original )
19
+ send("render_#{@format}")
20
+ end
21
+
22
+ def render_csv
23
+ csv_klass = defined?(FasterCSV) ? FasterCSV : CSV
24
+ fields = @original.fields
25
+ ( @header ? "#{@header}\n\n" : "" ) +
26
+ @data.inject(csv_klass.generate_line(fields).chomp + "\n" ) { |out,r|
27
+ out << csv_klass.generate_line(fields.map { |f| r[f] }).chomp + "\n"
28
+ } + ( @footer ? "\n#{@footer}\n" : "" )
29
+ end
30
+
31
+ def render_html
32
+ head_text = ( if @header
33
+ " <tr>\n <th colspan=#{@original.fields.length}>#{@header}" +
34
+ "</th>\n </tr>\n"
35
+ end )
36
+ out = "<table>\n#{head_text} <tr>\n <th>"+
37
+ "#{@original.fields.join('</th><th>')}" +
38
+ "</th>\n </tr>\n"
39
+ @data.inject(out) do |html,row|
40
+ html << row.inject(" <tr>\n "){ |row_html, field|
41
+ row_html << "<td>#{field}</td>"
42
+ } + "\n </tr>\n"
43
+ end
44
+ foot_text = ( if @footer
45
+ " <tr>\n <th colspan=#{@original.fields.length}>#{@footer}" +
46
+ "</th>\n </tr>\n"
47
+ end )
48
+
49
+ out << "#{foot_text}</table>"
50
+ return out unless @output_type.eql?(:complete)
51
+ "<html><head><title></title></head><body>\n#{out}</body></html>\n"
52
+ end
53
+
54
+ def render_text
55
+ header = @header ? "#{@header}\n" : ""
56
+ header << "fields: ( #{ @original.fields.join(', ') } )\n"
57
+ indices = (@range) ? @range.to_a : (0...@original.to_a.length).to_a
58
+ @data.inject(header) do |output,row|
59
+ output << "row#{indices.shift}: ( #{row.to_a.join(', ')} )\n"
60
+ end + (@footer ? "#{@footer}\n" : "" )
61
+
62
+ end
63
+ def render_pdf
64
+
65
+ return unless defined? PDF::Writer
66
+ pdf = PDF::Writer.new
67
+ pdf.margins_cm(0)
68
+ @data.each do |page|
69
+ unless page.eql?(@data.pages.first)
70
+ pdf.start_new_page
71
+ end
72
+ page.each do |section|
73
+ section.each do |element|
74
+ pdf.y = pdf.cm2pts(element.top)
75
+ pdf.text element.content,
76
+ :left => pdf.cm2pts(element.left),
77
+ :right => pdf.cm2pts(element.right),
78
+ :justification => element.align || :center
79
+
80
+ end
81
+ end
82
+ end
83
+ pdf.render
84
+
85
+ end
86
+ end
87
+ end
88
+ end
89
+
@@ -0,0 +1,77 @@
1
+ require "ostruct"
2
+ require "rexml/document"
3
+ module Ruport
4
+ class Format
5
+ class Document < OpenStruct
6
+ include Enumerable
7
+
8
+ def initialize(name,options={})
9
+ super(options)
10
+ self.name = name
11
+ self.pages ||= []
12
+ end
13
+
14
+ def each
15
+ self.pages.each { |p| yield(p) }
16
+ end
17
+
18
+ def add_page(name,options={})
19
+ options[:document] = self
20
+ self.pages << Format::Page.new(name,options)
21
+ end
22
+
23
+ def <<(page)
24
+ page.document = self
25
+ self.pages << page.dup
26
+ end
27
+
28
+ def [](page_name)
29
+ return self.pages[page_name] if page_name.kind_of? Integer
30
+ self.pages.find { |p| p.name.eql?(page_name) }
31
+ end
32
+
33
+ def clone
34
+ cloned = self.clone
35
+ cloned.pages = self.pages.clone
36
+ return cloned
37
+ end
38
+ end
39
+
40
+ class Page < Format::OpenNode
41
+
42
+ def initialize(name,options={})
43
+ super(:page,:document,:sections,name,options)
44
+ end
45
+
46
+ def add_section(name,options={})
47
+ add_child(Format::Section,name,options)
48
+ end
49
+
50
+ end
51
+
52
+ class Section < Format::OpenNode
53
+
54
+ def initialize(name, options={})
55
+ super(:section,:page,:elements,name,options)
56
+ end
57
+
58
+ def add_element(name,options={})
59
+ add_child(Format::Element,name,options)
60
+ end
61
+
62
+ end
63
+
64
+ class Element < OpenStruct
65
+
66
+ def initialize(name,options={})
67
+ super(options)
68
+ self.name = name
69
+ end
70
+
71
+ def to_s
72
+ self.content
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -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