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