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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +7 -0
- data/app/views/dossier/reports/cute_animals.html.haml +57 -0
- data/dossier-segmenter.gemspec +30 -0
- data/lib/dossier/segment.rb +54 -0
- data/lib/dossier/segment/chain.rb +31 -0
- data/lib/dossier/segment/definition.rb +73 -0
- data/lib/dossier/segment/rows.rb +39 -0
- data/lib/dossier/segment/summary.rb +50 -0
- data/lib/dossier/segmenter.rb +104 -0
- data/lib/dossier/segmenter/engine.rb +6 -0
- data/lib/dossier/segmenter/report.rb +34 -0
- data/lib/dossier/segmenter/version.rb +5 -0
- data/script/rails +5 -0
- data/spec/dossier/segment/chain_spec.rb +43 -0
- data/spec/dossier/segment/definition_spec.rb +84 -0
- data/spec/dossier/segment/rows_spec.rb +10 -0
- data/spec/dossier/segment/summary_spec.rb +51 -0
- data/spec/dossier/segment_spec.rb +121 -0
- data/spec/dossier/segmenter/report_spec.rb +23 -0
- data/spec/dossier/segmenter_spec.rb +171 -0
- data/spec/dummy/app/reports/cute_animals_report.rb +41 -0
- data/spec/dummy/config.ru +2 -0
- data/spec/dummy/config/application.rb +27 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/features/segmented_report_spec.rb +28 -0
- data/spec/spec_helper.rb +20 -0
- metadata +201 -0
@@ -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,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
|
data/script/rails
ADDED
@@ -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,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
|