rose 0.0.5

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.
@@ -0,0 +1,56 @@
1
+ require 'rose'
2
+ require 'active_record'
3
+
4
+ module Rose
5
+ # This class is provides ActiveRecord models the ability to run reports
6
+ class ActiveRecordAdapter < ObjectAdapter
7
+ # @see Rose::ObjectAdapter#sprout
8
+ def self.sprout(seedling, options={})
9
+ table = nil
10
+ options[:class].transaction do
11
+ table = super(seedling, options)
12
+ raise ActiveRecord::Rollback
13
+ end
14
+ table
15
+ end
16
+ end
17
+
18
+ module ActiveRecordExtensions
19
+ def self.included(base)
20
+ base.extend(ClassMethods)
21
+ end
22
+
23
+ module ClassMethods
24
+ def rose(name, options={}, &blk)
25
+ instance = Rose::Seedling.new(Rose::ActiveRecordAdapter, options.merge(:class => self))
26
+ instance.instance_eval(&blk)
27
+ register_seedling(name, instance)
28
+ end
29
+
30
+ def rose_for(name, *args)
31
+ seedlings(name).bloom(self.find(:all, *args))
32
+ end
33
+
34
+ def root_for(name, options={}, *args)
35
+ seedlings(name).photosynthesize(self.find(:all, *args), options)
36
+ end
37
+
38
+ def seedlings(name)
39
+ @seedlings ||= {}
40
+ @seedlings[name]
41
+ end
42
+
43
+ private
44
+
45
+ def register_seedling(name, instance)
46
+ @seedlings ||= {}
47
+ @seedlings[name] = Shell.new(instance)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ # This extends ActiveRecord::Base to include Rose reporting
54
+ class ActiveRecord::Base
55
+ include Rose::ActiveRecordExtensions
56
+ end
@@ -0,0 +1,93 @@
1
+ module Rose
2
+ # An Attribute is value object used to operate on a table
3
+ class Attribute
4
+ attr_reader :method_name
5
+ attr_reader :column_name
6
+
7
+ def initialize(method_name, column_name)
8
+ @method_name = method_name
9
+ @column_name = column_name
10
+ end
11
+
12
+ def column_name
13
+ @column_name || @method_name
14
+ end
15
+
16
+ # This class defines an Attribute whose value is defined via a block
17
+ class Indirect < Attribute
18
+ attr_reader :value_block
19
+
20
+ def initialize(method_name, column_name, value_block)
21
+ super(method_name, column_name)
22
+ @value_block = value_block
23
+ end
24
+
25
+ def evaluate(item)
26
+ item.instance_eval(&@value_block)
27
+ end
28
+ end
29
+
30
+ # This class defines an Attribute whose value is defined via a method
31
+ class Direct < Attribute
32
+ def evaluate(item)
33
+ if item.respond_to?(@method_name.to_sym)
34
+ item.__send__(@method_name)
35
+ end
36
+ end
37
+ end
38
+
39
+ # This is a value object for pivot parameters
40
+ class Pivot < Indirect
41
+ alias_method :group_column, :method_name
42
+ alias_method :pivot_column, :column_name
43
+
44
+ def on(table)
45
+ table.pivot(pivot_column, :group_by => group_column, :values => value_block)
46
+ end
47
+ end
48
+
49
+ # Defines a collection of attributes
50
+ class Collection < Array
51
+ def row
52
+ inject({}) do |row, attribute|
53
+ row[attribute.column_name] = yield(attribute) if block_given?
54
+ row
55
+ end
56
+ end
57
+
58
+ def column_names
59
+ map(&:column_name)
60
+ end
61
+ end
62
+
63
+ # This is a value object for sort parameters
64
+ class Sort
65
+ attr_reader :column_name, :order, :sort_block
66
+
67
+ def initialize(column_name, order, &sort_block)
68
+ @column_name = column_name
69
+ @order = order
70
+ @sort_block = sort_block
71
+ end
72
+
73
+ def on(table)
74
+ if @sort_block
75
+ table.sort_rows_by!(nil, :order => @order) do |row|
76
+ @sort_block.call(row[@column_name])
77
+ end
78
+ else
79
+ table.sort_rows_by!(@column_name, :order => @order)
80
+ end
81
+ table
82
+ end
83
+ end
84
+
85
+ # This class defines an Indirect Attribute for rejecting table rows
86
+ class Filter < Indirect
87
+ def on(table)
88
+ table.data.reject! { |record| !@value_block.call(record) }
89
+ table
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,27 @@
1
+ module Rose
2
+ module CoreExtensions
3
+ # Validates that the given hash includes the required keys.
4
+ # If any keys are missing, ArgumentError will be raised.
5
+ #
6
+ # @param [Hash] hash the hash to validate
7
+ # @param [Array<Symbol>] required_keys the list of keys
8
+ # @raise [ArgumentError]
9
+ def require_keys(hash, *required_keys)
10
+ missing_keys = required_keys - hash.keys
11
+ raise ArgumentError, "Missing required key(s): #{missing_keys.join(', ')}" unless missing_keys.empty?
12
+ end
13
+
14
+ # Returns the values of required keys in the given hash
15
+ #
16
+ # @param [Hash] hash the hash to validate
17
+ # @param [Array<Symbol>] required_keys the list of keys
18
+ # @raise [ArgumentError] (see Rose::CoreExtensions#require_keys)
19
+ def required_values(hash, *required_keys)
20
+ require_keys(hash, *required_keys)
21
+ values = required_keys.inject([]) do |values, key|
22
+ values << hash[key]
23
+ end
24
+ required_keys.length == 1 ? values.first : values
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,130 @@
1
+ require 'rose'
2
+ require 'rose/ruport'
3
+
4
+ module Rose
5
+ # This class is provides Objects the ability to run reports
6
+ class ObjectAdapter
7
+ class << self
8
+ include CoreExtensions
9
+ end
10
+
11
+ # @param [Rose::Seedling] seedling the seedling to sprout
12
+ # @param [Hash] options the options to run with
13
+ # @option options [Array] :items (required) the items to report on
14
+ # @option options [Rose::Attribute::Collection] :attributes (nil) a row of attributes
15
+ # @option options [String] :group_column (nil) @deprecated
16
+ # @option options [String] :pivot_column (nil) @deprecated
17
+ # @option options [String] :value_block (nil) @deprecated
18
+ # @option options [String] :summary (nil) @deprecated
19
+ # @option options [String] :summary_on (nil) @deprecated
20
+ # @option options [String] :sort_by (nil) @deprecated
21
+ # @option options [String] :sort_order (nil) @deprecated
22
+ # @return [Ruport::Data::Table] the resulting table
23
+ def self.sprout(seedling, options={})
24
+ items, attributes = required_values(options, :items, :attributes)
25
+ table = Ruport::Data::RoseTable.new(:column_names => attributes.column_names)
26
+
27
+ self.rows(table, options)
28
+
29
+ if (alterations = options[:alterations])
30
+ alterations.each do |alteration|
31
+ table = alteration.on(table)
32
+ end
33
+ end
34
+
35
+ table
36
+ end
37
+
38
+ # @param [Rose::Seedling] seedling the seedling to update
39
+ # @param [Hash] options the options to run with
40
+ # @option options [Array] :items (required) an Array of items
41
+ # @option options [Hash,String] :with (required) (see Rose::Shell#photosynthesize)
42
+ def self.osmosis(seedling, options={})
43
+ hash_or_csv, items = required_values(options, :with, :items)
44
+
45
+ root, row = seedling.root, seedling.row
46
+ idy_attr = row.identity_attribute
47
+
48
+ items = case hash_or_csv
49
+ when String # CSV File
50
+ self.osmosis_from_csv(root, options.merge(
51
+ :idy_attr => idy_attr,
52
+ :csv_file => hash_or_csv,
53
+ :items => items
54
+ ))
55
+ when Hash
56
+ self.osmosis_from_hash(root, options.merge(
57
+ :idy_attr => idy_attr,
58
+ :updates => hash_or_csv,
59
+ :items => items
60
+ ))
61
+ end
62
+
63
+ self.sprout(seedling, options.merge(
64
+ :attributes => row.attributes,
65
+ :items => items
66
+ ))
67
+ end
68
+
69
+ protected
70
+
71
+ def self.rows(table, options={})
72
+ items, attributes = required_values(options, :items, :attributes)
73
+ items.each do |item|
74
+ options[:class].tap { |enforce_class| enforce_item_type(item, enforce_class) if enforce_class }
75
+ table << attributes.row { |attr| attr.evaluate(item).to_s }
76
+ end
77
+ end
78
+
79
+ def self.enforce_item_type(item, klass)
80
+ raise TypeError.new("Expected #{klass}, got #{item.class}") unless item.kind_of?(klass)
81
+ end
82
+
83
+ # @return [Array] items (see Rose::ObjectAdapter#osmosis_from_hash)
84
+ def self.osmosis_from_csv(root, options={})
85
+ idy_attr, csv_file, items = required_values(options, :idy_attr, :csv_file, :items)
86
+ updates = data_from_csv(csv_file).inject({}) do |updates, data|
87
+ updates[data.delete(idy_attr.column_name)] = data
88
+ updates
89
+ end
90
+ self.osmosis_from_hash(root, options.merge(:updates => updates))
91
+ end
92
+
93
+ # @return [Array] items the updated items
94
+ def self.osmosis_from_hash(root, options={})
95
+ idy_attr, updates, items = required_values(options, :idy_attr, :updates, :items)
96
+ finder = root.finder || auto_finder(idy_attr)
97
+ new_items = []
98
+ updates.each do |idy, update|
99
+ record = use_finder(finder, items, idy)
100
+ if record
101
+ root.updater(options[:preview]).call(record, update)
102
+ elsif creator = root.creator(options[:preview])
103
+ new_items << creator.call(idy, update)
104
+ else
105
+ next
106
+ end
107
+ end
108
+ items | new_items
109
+ end
110
+
111
+ def self.data_from_csv(csv_file)
112
+ Ruport::Data::RoseTable.load(csv_file).data.map(&:to_hash)
113
+ end
114
+
115
+ # @param [Rose::Attribute] idy_attr the attribute to find items with
116
+ # @return [Proc] a Proc that will find items with attribute evaluating to given id
117
+ def self.auto_finder(idy_attr)
118
+ lambda { |items, idy|
119
+ items.find do |item|
120
+ idy_attr.evaluate(item).to_s == idy
121
+ end
122
+ }
123
+ end
124
+
125
+ # In case subclasses want to call finders differently
126
+ def self.use_finder(finder, items, idy)
127
+ finder.call(items, idy)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,114 @@
1
+ require 'rose/attribute'
2
+
3
+ module Rose
4
+ module Proxy
5
+ # This class is used by the DSL to collect a row of attributes
6
+ class Row
7
+ # Each attribute defines how values are generated for that column
8
+ # as well how that column is named
9
+ # @return [Rose::Attribute::Collection] A collection of attributes
10
+ attr_reader :attributes
11
+
12
+ # @return [Rose::Attribute] attribute used to determine which column
13
+ # is the id column
14
+ attr_reader :identity_attribute
15
+
16
+ def initialize
17
+ @attributes = Attribute::Collection.new
18
+ end
19
+
20
+ def column(name, &blk)
21
+ @attributes << attribute(name, &blk)
22
+ end
23
+
24
+ def identity(name, &blk)
25
+ column(name, &blk)
26
+ @identity_attribute = @attributes.last
27
+ end
28
+
29
+ def self.name_and_title(name)
30
+ case name
31
+ when Hash
32
+ name.to_a.first
33
+ else
34
+ [name, nil]
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def attribute(name, &blk)
41
+ name, title = self.class.name_and_title(name)
42
+
43
+ if block_given?
44
+ attribute = Attribute::Indirect.new(name, title, blk)
45
+ else
46
+ attribute = Attribute::Direct.new(name, title)
47
+ end
48
+ end
49
+ end
50
+
51
+ # This class is used by the DSL to collect summary attributes
52
+ class Summary < Row
53
+ attr_reader :column_name
54
+
55
+ def initialize(column_name)
56
+ super()
57
+ @column_name = column_name
58
+ end
59
+
60
+ def on(table)
61
+ rows = table.column(@column_name).uniq.inject([]) do |rows, group|
62
+ rows << @attributes.row { |attr|
63
+ gr = table.grouped_rows({@column_name => group}, attr.column_name)
64
+ attr.evaluate(gr)
65
+ }.merge(@column_name => group)
66
+ end
67
+ Ruport::Data::RoseTable.new(:column_names => [@column_name] | @attributes.column_names).tap do |table|
68
+ rows.each { |row| table << row }
69
+ end
70
+ end
71
+ end
72
+
73
+ # This class is used by the DSL to collect update attributes.
74
+ # Just like a root is the foundation of transporting water into
75
+ # a tree, a Root provides what's required to import data into a Rose
76
+ class Root
77
+ attr_reader :finder
78
+ attr_reader :updater
79
+ attr_reader :update_previewer
80
+ attr_reader :creator
81
+ attr_reader :create_previewer
82
+
83
+ def find(&blk)
84
+ @finder = blk
85
+ end
86
+
87
+ def update(&blk)
88
+ @updater = blk
89
+ end
90
+
91
+ def preview_update(&blk)
92
+ @update_previewer = blk
93
+ end
94
+
95
+ def create(&blk)
96
+ @creator = blk
97
+ end
98
+
99
+ def preview_create(&blk)
100
+ @create_previewer = blk
101
+ end
102
+
103
+ # @param [true, false] preview whether to use the update_previewer or not
104
+ def updater(preview = false)
105
+ preview ? @update_previewer : @updater
106
+ end
107
+
108
+ # @param [true, false] preview whether to use the create_previewer or not
109
+ def creator(preview = false)
110
+ preview ? @create_previewer : @creator
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,45 @@
1
+ # Enables pivoting with value columns
2
+ class Ruport::Data::RoseTable < Ruport::Data::Table
3
+ def pivot(pivot_column, options = {})
4
+ group_column = options[:group_by] ||
5
+ raise(ArgumentError, ":group_by option required")
6
+ value_column = options[:values] ||
7
+ raise(ArgumentError, ":values option required")
8
+ RosePivot.new(
9
+ self, group_column, pivot_column, nil, options
10
+ ).to_table(&value_column)
11
+ end
12
+
13
+ def grouped_rows(with, column_name)
14
+ rows_with(with).map { |row|
15
+ begin
16
+ row.send(column_name.to_sym)
17
+ rescue NoMethodError => no_method_error
18
+ nil
19
+ end
20
+ }
21
+ end
22
+ end
23
+
24
+ # Enables using value blocks for the value column
25
+ class Ruport::Data::Table::RosePivot < Ruport::Data::Table::Pivot
26
+ def to_table
27
+ result = Ruport::Data::RoseTable.new()
28
+ result.add_column(@group_column)
29
+ pivoted_columns = columns_from_pivot
30
+ pivoted_columns.each { |name| result.add_column(name) }
31
+ outer_grouping = Grouping(@table, :by => @group_column)
32
+ group_column_entries.each {|outer_group_name|
33
+ outer_group = outer_grouping[outer_group_name]
34
+ pivot_values = pivoted_columns.inject({}) do |hsh, pc|
35
+ matching_rows = outer_group.rows_with(@pivot_column => pc)
36
+ hsh[pc] = yield(matching_rows)
37
+ hsh
38
+ end
39
+ result << [outer_group_name] + pivoted_columns.map {|pc|
40
+ pivot_values[pc]
41
+ }
42
+ }
43
+ result
44
+ end
45
+ end