dossier-segmenter 0.9.0

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