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
@@ -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
|
data/lib/ruport/query.rb
ADDED
@@ -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
|