dossier-segmenter 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,104 @@
1
+ require 'dossier'
2
+ require 'dossier/segmenter/engine'
3
+ require 'dossier/segmenter/version'
4
+
5
+ module Dossier
6
+ class Segmenter
7
+ attr_accessor :report
8
+
9
+ class << self
10
+ attr_accessor :report_class
11
+ end
12
+
13
+ def self.segments
14
+ @segment_chain ||= Segment::Chain.new
15
+ end
16
+
17
+ def self.segment(name, options = {}, &block)
18
+ segments << Segment::Definition.new(self, name, options)
19
+ instance_eval(&block) if block_given?
20
+ end
21
+
22
+ def self.skip_headers
23
+ segments.map(&:columns).flatten
24
+ end
25
+
26
+ delegate :skip_headers, to: "self.class"
27
+
28
+ def initialize(report)
29
+ self.report = report
30
+ extend(segment_chain.first.segment_module) if report.segmented?
31
+ end
32
+
33
+ def headers
34
+ @headers ||= report_results.headers.reject { |header| header.in?(skip_headers) }
35
+ end
36
+
37
+ def data
38
+ @data ||= report_results.rows.inject(Hash.new { [] }) { |acc, row|
39
+ acc.tap { |hash| hash[key_path_for(row)] += [row] }
40
+ }
41
+ end
42
+
43
+ def segment_chain
44
+ self.class.segments
45
+ end
46
+
47
+ def segmenter
48
+ self
49
+ end
50
+
51
+ def segment_options_for(segment)
52
+ position = segment.key_path.split('.').count
53
+ data.keys.map { |key|
54
+ key.split('.')
55
+ }.inject({}) { |acc, key|
56
+ acc.tap { |hash|
57
+ hash[key.first(position + 1)] ||= data[key.join('.')].first
58
+ }
59
+ }.select { |key, value|
60
+ key.first(position) == segment.key_path.split('.')
61
+ }.values.map { |row|
62
+ Hash[report_results.headers.zip(row)]
63
+ }
64
+ end
65
+
66
+ def key_path
67
+ String.new
68
+ end
69
+
70
+ def inspect
71
+ "#<#{self.class.name}>"
72
+ end
73
+
74
+ def header_index_map
75
+ @header_index_map ||= Hash[skip_headers.map { |h| [h, report_results.headers.index(h)] }]
76
+ end
77
+
78
+ private
79
+
80
+ def report_results
81
+ report.raw_results
82
+ end
83
+
84
+ def key_path_for(row)
85
+ group_by_indexes.map { |i| row.at(i) }.join('.')
86
+ end
87
+
88
+ def segment_options
89
+ data unless defined?(@data)
90
+ @segment_options
91
+ end
92
+
93
+ def group_by_indexes
94
+ @group_by_indexes ||= header_index_map.values_at(*segment_chain.map(&:group_by).map(&:to_s))
95
+ end
96
+ end
97
+ end
98
+
99
+ require "dossier/segment"
100
+ require "dossier/segment/chain"
101
+ require "dossier/segment/definition"
102
+ require "dossier/segment/rows"
103
+ require "dossier/segment/summary"
104
+ require "dossier/segmenter/report"
@@ -0,0 +1,6 @@
1
+ module Dossier
2
+ class Segmenter
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,34 @@
1
+ module Dossier
2
+ class Segmenter
3
+ module Report
4
+ extend ActiveSupport::Concern
5
+
6
+ def segment_parent
7
+ nil
8
+ end
9
+
10
+ def segmenter
11
+ @segmenter ||= self.class.segmenter_class.new(self)
12
+ end
13
+
14
+ def segmented?
15
+ self.class.segmenter_class.segments.any?
16
+ end
17
+
18
+ module ClassMethods
19
+ def segmenter_class
20
+ const_get(:Segmenter)
21
+ end
22
+
23
+ def inherited(base)
24
+ segmenter_class = Class.new(Dossier::Segmenter)
25
+ segmenter_class.report_class = base
26
+ base.const_set(:Segmenter, segmenter_class)
27
+ super
28
+ end
29
+ end
30
+
31
+ Dossier::Report.send :include, self
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ module Dossier
2
+ class Segmenter
3
+ VERSION = "0.9.0"
4
+ end
5
+ end
data/script/rails ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ APP_PATH = File.expand_path('../../spec/dummy/config/application', __FILE__)
4
+ require APP_PATH
5
+ require 'rails/commands'
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Segment::Chain do
4
+ let(:report_class) { Class.new(Dossier::Report) }
5
+ let(:segmenter_class) { report_class.segmenter_class }
6
+ let(:definition) { Dossier::Segment::Definition.new(segmenter_class, :foo) }
7
+ let(:next) { Dossier::Segment::Definition.new(segmenter_class, :bar) }
8
+ let(:chain) { described_class.new.tap { |c| c << definition } }
9
+
10
+ it "allows #at access of definitions" do
11
+ expect(chain.at 0).to eq definition
12
+ end
13
+
14
+ it "aliases [] to at" do
15
+ expect(chain[0]).to eq definition
16
+ end
17
+
18
+ it "conforms to enumerable" do
19
+ chain.each { |d| expect(d).to eq definition }
20
+ end
21
+
22
+ describe "appending to the chain" do
23
+ before :each do
24
+ chain << self.next
25
+ end
26
+
27
+ it "uses <<" do
28
+ expect(chain.at 1).to eq self.next
29
+ end
30
+
31
+ it "allows accessing the end" do
32
+ expect(chain.last).to eq self.next
33
+ end
34
+
35
+ it "assigns the previous segment the given segment as next" do
36
+ expect(chain.first.next).to eq self.next
37
+ end
38
+
39
+ it "assigns the next segment the previous segment as prev" do
40
+ expect(chain.last.prev).to eq chain.first
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Segment::Definition do
4
+ let(:report_class) { Class.new(Dossier::Report) }
5
+ let(:segmenter_class) { report_class.segmenter_class }
6
+ let(:definition) { described_class.new(segmenter_class, :foo) }
7
+
8
+ describe "attributes" do
9
+ it "has a segmenter_class" do
10
+ expect(definition.segmenter_class).to eq segmenter_class
11
+ end
12
+ it "has a name" do
13
+ expect(definition.name).to eq :foo
14
+ end
15
+
16
+ it "uses the name as the group by if one isn't provided" do
17
+ expect(definition.group_by).to eq :foo
18
+ end
19
+
20
+ it "uses the name as the display name if one isn't provided" do
21
+ expect(definition.display_name).to eq :foo
22
+ end
23
+
24
+ it "has a group_by" do
25
+ definition = described_class.new(segmenter_class, :foo, group_by: :bar)
26
+ expect(definition.group_by).to eq :bar
27
+ end
28
+
29
+ it "has a display_name" do
30
+ definition = described_class.new(segmenter_class, :foo, display_name: :baz)
31
+ expect(definition.display_name).to eq :baz
32
+ end
33
+
34
+ it "has a segment class name" do
35
+ expect(definition.segment_class_name).to eq 'Foo'
36
+ end
37
+
38
+ it "has a plural name" do
39
+ expect(definition.plural_name).to eq 'foos'
40
+ end
41
+
42
+ describe "for columns" do
43
+ it "knows to use the name" do
44
+ definition = described_class.new(segmenter_class, :foo)
45
+ expect(definition.columns).to eq %w[foo]
46
+ end
47
+
48
+ it "knows to use group_by if provided" do
49
+ definition = described_class.new(segmenter_class, :foo, group_by: :bar)
50
+ expect(definition.columns).to eq %w[bar foo]
51
+ end
52
+
53
+ it "knows to use display_name if not a proc" do
54
+ definition = described_class.new(segmenter_class, :foo, display_name: :baz)
55
+ expect(definition.columns).to eq %w[foo baz]
56
+ end
57
+
58
+ it "knows not to use display_name if not a proc" do
59
+ definition = described_class.new(segmenter_class, :foo, display_name: ->(row) {})
60
+ expect(definition.columns).to eq %w[foo]
61
+ end
62
+
63
+ it "knows not to use the name if group_by and display_name are set" do
64
+ definition = described_class.new(segmenter_class, :foo, group_by: :bar, display_name: :baz)
65
+ expect(definition.columns).to eq %w[bar baz]
66
+ end
67
+
68
+ it "knows not to use the display_name if group_by is present and display_name is a proc" do
69
+ definition = described_class.new(segmenter_class, :foo, group_by: :foo_id, display_name: ->(row) {})
70
+ expect(definition.columns).to eq %w[foo_id]
71
+ end
72
+ end
73
+ end
74
+
75
+ describe "segment subclass" do
76
+ it "is defined in the report class namespace as a subclass of Dossier::Segment" do
77
+ expect(definition.segment_subclass.superclass).to eq Dossier::Segment
78
+ end
79
+
80
+ it "sets up the segment subclasses name constant properly" do
81
+ expect(CuteAnimalsReport::Family).to be_a Class
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Segment::Rows do
4
+
5
+ it "can determine the key path"
6
+
7
+ it "has the rows relevant to its parent segment"
8
+
9
+ end
10
+
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Segment::Summary do
4
+
5
+ let(:summary) { described_class.new(headers) }
6
+ let(:headers) { %w[animal weight cuteness fanciness] }
7
+ let(:rows) { [
8
+ ['Cats', 15.0, 97, true],
9
+ ['Dogs', 88.2, 45, false] # dogs aren't very fancy animals...
10
+ ] }
11
+
12
+ before(:each) {
13
+ rows.each { |row| summary << row }
14
+ }
15
+
16
+ it "counts all the objects it has summarized" do
17
+ expect(summary.count).to eq 2
18
+ end
19
+
20
+ it "allows appending multiple rows" do
21
+ summary << rows[0] << rows[1]
22
+ expect(summary.count).to eq 4
23
+ end
24
+
25
+ describe "summing" do
26
+ it "handles whole numbers" do
27
+ expect(summary.sum :cuteness).to eq 142
28
+ end
29
+
30
+ it "handles decimal places" do
31
+ expect(summary.sum :weight).to eq 103.2
32
+ end
33
+ end
34
+
35
+ describe "averaging" do
36
+ it "handles whole numbers" do
37
+ expect(summary.average :cuteness).to eq 71
38
+ end
39
+
40
+ it "handles decimal places" do
41
+ expect(summary.average :weight).to eq 51.6
42
+ end
43
+ end
44
+
45
+ describe "operating on a header that doesn't exist" do
46
+ it "raises an helpful error" do
47
+ expect { summary.sum :fluffikins }.to raise_error(Dossier::Segment::Summary::HeaderError, /no such header 'fluffikins'/i)
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,121 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Segment do
4
+ let(:report_class) { Class.new(Dossier::Report) }
5
+ let!(:segmenter_class) {
6
+ report_class.segmenter_class.tap { |sc|
7
+ sc.segment :foo
8
+ }
9
+ }
10
+ let(:definition) { segmenter_class.segments.first }
11
+ let(:report) { report_class.new }
12
+ let(:segmenter) { report.segmenter }
13
+ let(:segment) { described_class.new(segmenter, definition) }
14
+
15
+ describe "attributes" do
16
+ it "takes the segment instance" do
17
+ expect(segment.segmenter).to eq segmenter
18
+ end
19
+
20
+ it "takes its definition" do
21
+ expect(segment.definition).to eq definition
22
+ end
23
+
24
+ it "has access to the report" do
25
+ expect(segment.report).to eq report
26
+ end
27
+
28
+ it "delegates headers to its segmenter" do
29
+ segment.segmenter.should_receive(:headers)
30
+ segment.headers
31
+ end
32
+ end
33
+
34
+
35
+ describe CuteAnimalsReport do
36
+ describe "key paths" do
37
+ let(:report) { CuteAnimalsReport.new }
38
+ let!(:segmenter) { report.segmenter }
39
+ let(:families) { segmenter.families }
40
+ let(:domestics) { families.map(&:domestics).flatten }
41
+ let(:groups) { domestics.map(&:groups).flatten }
42
+ let(:rows) { groups.map(&:rows).map(&:to_a) }
43
+ let(:rows) { groups.inject([]) { |a,g| a += g.rows.to_a } }
44
+
45
+ describe "select the correct amount of segments" do
46
+
47
+ it "has the right amount of families" do
48
+ expect(families.count).to eq 2
49
+ end
50
+
51
+ it "has the right amount of domestics" do
52
+ expect(domestics.count).to eq 4
53
+ end
54
+
55
+ it "has the right amount of groups" do
56
+ expect(groups.count).to eq 6
57
+ end
58
+
59
+ it "has the right amount of rows" do
60
+ expect(rows.count).to eq CuteAnimalsReport::ROWS.call.count
61
+ end
62
+ end
63
+
64
+ describe "summarizing" do
65
+ before(:each) { rows }
66
+
67
+ it "properly sums all rows under family" do
68
+ expect(families.first.summary.sum(:gifs).to_s).to eq '4154.0'
69
+ end
70
+
71
+ it "properly sums all rows under domestic" do
72
+ expect(domestics.first.summary.sum(:gifs).to_s).to eq '2755.0'
73
+ end
74
+
75
+ it "property sums all the rows under group" do
76
+ expect(groups.first.summary.sum(:gifs).to_s).to eq '1364.0'
77
+ end
78
+
79
+ describe "segment chains" do
80
+ let(:family_definition) { segmenter.segment_chain[0] }
81
+ let(:domestic_definition) { segmenter.segment_chain[1] }
82
+ let(:group_definition) { segmenter.segment_chain[2] }
83
+ let(:family) { described_class.new(segmenter, family_definition, {family: 'feline'}) }
84
+ let(:domestic) { described_class.new(segmenter, domestic_definition, {domestic: true} ).tap { |s| s.parent = family } }
85
+ let(:group) { described_class.new(segmenter, group_definition, {group_id: 25} ).tap { |s| s.parent = domestic } }
86
+
87
+ it "has all three elements in the chain from the bottom" do
88
+ expect(group.chain).to eq [group, domestic, family]
89
+ end
90
+
91
+ it "has two elements in the chain from the middle" do
92
+ expect(domestic.chain).to eq [domestic, family]
93
+ end
94
+
95
+ it "has one element in the chain from the top" do
96
+ expect(family.chain).to eq [family]
97
+ end
98
+
99
+ it "does not replicate elements in the chain when accessed again" do
100
+ family.chain
101
+ expect(family.chain.length).to eq 1
102
+ end
103
+
104
+ describe "key paths" do
105
+ it "works from the bottom" do
106
+ expect(group.key_path).to eq 'feline.true.25'
107
+ end
108
+
109
+ it "works from the middle" do
110
+ expect(domestic.key_path).to eq 'feline.true'
111
+ end
112
+
113
+ it "works from the top" do
114
+ expect(family.key_path).to eq 'feline'
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end