lean-ruport 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.

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