wonkavision 0.5.11 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/CHANGELOG.rdoc +3 -0
  2. data/lib/wonkavision.rb +28 -1
  3. data/lib/wonkavision/aggregation.rb +21 -0
  4. data/lib/wonkavision/event_coordinator.rb +19 -7
  5. data/lib/wonkavision/extensions/symbol.rb +55 -0
  6. data/lib/wonkavision/facts.rb +27 -0
  7. data/lib/wonkavision/local_job_queue.rb +28 -0
  8. data/lib/wonkavision/message_mapper.rb +2 -2
  9. data/lib/wonkavision/message_mapper/map.rb +60 -8
  10. data/lib/wonkavision/persistence/mongo.rb +95 -0
  11. data/lib/wonkavision/plugins.rb +2 -1
  12. data/lib/wonkavision/plugins/analytics/aggregation.rb +139 -0
  13. data/lib/wonkavision/plugins/analytics/aggregation/aggregation_spec.rb +53 -0
  14. data/lib/wonkavision/plugins/analytics/aggregation/attribute.rb +22 -0
  15. data/lib/wonkavision/plugins/analytics/aggregation/dimension.rb +64 -0
  16. data/lib/wonkavision/plugins/analytics/aggregation/measure.rb +240 -0
  17. data/lib/wonkavision/plugins/analytics/cellset.rb +171 -0
  18. data/lib/wonkavision/plugins/analytics/facts.rb +106 -0
  19. data/lib/wonkavision/plugins/analytics/handlers/apply_aggregation.rb +35 -0
  20. data/lib/wonkavision/plugins/analytics/handlers/split_by_aggregation.rb +60 -0
  21. data/lib/wonkavision/plugins/analytics/member_filter.rb +106 -0
  22. data/lib/wonkavision/plugins/analytics/mongo.rb +6 -0
  23. data/lib/wonkavision/plugins/analytics/persistence/hash_store.rb +59 -0
  24. data/lib/wonkavision/plugins/analytics/persistence/mongo_store.rb +85 -0
  25. data/lib/wonkavision/plugins/analytics/persistence/store.rb +105 -0
  26. data/lib/wonkavision/plugins/analytics/query.rb +76 -0
  27. data/lib/wonkavision/plugins/event_handling.rb +15 -3
  28. data/lib/wonkavision/version.rb +1 -1
  29. data/test/aggregation_spec_test.rb +99 -0
  30. data/test/aggregation_test.rb +170 -0
  31. data/test/analytics/test_aggregation.rb +78 -0
  32. data/test/apply_aggregation_test.rb +92 -0
  33. data/test/attribute_test.rb +26 -0
  34. data/test/cellset_test.rb +200 -0
  35. data/test/dimension_test.rb +186 -0
  36. data/test/facts_test.rb +146 -0
  37. data/test/hash_store_test.rb +112 -0
  38. data/test/log/test.log +96844 -0
  39. data/test/map_test.rb +48 -1
  40. data/test/measure_test.rb +146 -0
  41. data/test/member_filter_test.rb +143 -0
  42. data/test/mongo_store_test.rb +115 -0
  43. data/test/query_test.rb +106 -0
  44. data/test/split_by_aggregation_test.rb +114 -0
  45. data/test/store_test.rb +71 -0
  46. data/test/symbol_test.rb +62 -0
  47. data/test/test_activity_models.rb +1 -1
  48. data/test/test_aggregation.rb +42 -0
  49. data/test/test_data.tuples +100 -0
  50. data/test/test_helper.rb +7 -0
  51. metadata +57 -5
@@ -0,0 +1,106 @@
1
+ require "test_helper"
2
+
3
+ class QueryTest < ActiveSupport::TestCase
4
+ Query = Wonkavision::Analytics::Query
5
+
6
+ context "Query" do
7
+ setup do
8
+ @query = Query.new
9
+ end
10
+
11
+ context "Class methods" do
12
+ context "#axis_ordinal" do
13
+ should "convert nil or empty string to axis zero" do
14
+ assert_equal 0, Query.axis_ordinal(nil)
15
+ assert_equal 0, Query.axis_ordinal("")
16
+ end
17
+ should "convert string integers into real integers" do
18
+ assert_equal 3, Query.axis_ordinal("3")
19
+ end
20
+ should "correctly interpret named axes" do
21
+ ["Columns", :rows, :PAGES, "chapters", "SECTIONS"].each_with_index do |item,idx|
22
+ assert_equal idx, Query.axis_ordinal(item)
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+
29
+ context "#select" do
30
+ should "associate dimensions with the default axis (columns)" do
31
+ @query.select :hi, :there
32
+ assert_equal [:hi,:there], @query.axes[0]
33
+ end
34
+ should "associate dimensions with the specified axis" do
35
+ @query.select :hi, :there, :on => :rows
36
+ assert_equal [:hi, :there], @query.axes[1]
37
+ end
38
+ end
39
+
40
+ context "#selected_dimensions" do
41
+ should "collect dimensions from each axis" do
42
+ @query.select :c, :d; @query.select :b, :a, :on => :rows
43
+ assert_equal [:c,:d,:b,:a], @query.selected_dimensions
44
+ end
45
+ end
46
+
47
+ context "#referenced_dimensions" do
48
+ should "match selected dimensions with no dimension filters" do
49
+ @query.select :c, :d
50
+ assert_equal @query.selected_dimensions, @query.referenced_dimensions
51
+ end
52
+ should "include filter dimensions in the presence of a dimension filter"do
53
+ @query.select(:c,:d).where(:e=>:f)
54
+ assert_equal 3, @query.referenced_dimensions.length
55
+ assert_equal [], @query.referenced_dimensions - [:c, :d, :e]
56
+ end
57
+ end
58
+
59
+ context "#matches_filter?" do
60
+ should "return true if all filters are applied" do
61
+ @query.expects(:all_filters_applied?).returns(true)
62
+ assert @query.matches_filter?(nil,nil)
63
+ end
64
+ end
65
+
66
+ context "#where" do
67
+ should "convert a symbol to a MemberFilter" do
68
+ @query.where :a=>:b
69
+ assert @query.filters[0].kind_of?(Wonkavision::Analytics::MemberFilter)
70
+ end
71
+
72
+ should "append filters to the filters array" do
73
+ @query.where :a=>:b, :c=>:d
74
+ assert_equal 2, @query.filters.length
75
+ end
76
+
77
+ should "set the member filters value from the hash" do
78
+ @query.where :a=>:b
79
+ assert_equal :b, @query.filters[0].value
80
+ end
81
+
82
+ should "add dimension names to the slicer" do
83
+ @query.where :dimensions.a => :b
84
+ assert @query.slicer.include?(:a)
85
+ end
86
+
87
+ should "not add measure names to the slicer" do
88
+ @query.where :measures.a => :b
89
+ assert @query.slicer.include?(:a) == false
90
+ end
91
+
92
+ end
93
+
94
+ context "#slicer" do
95
+ setup do
96
+ @query.select :a, :b
97
+ @query.where :dimensions.b=>:b, :dimensions.c=>:c
98
+ end
99
+ should "include only dimensions not on another axis" do
100
+ assert_equal [:c], @query.slicer
101
+ end
102
+
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,114 @@
1
+ require "test_helper"
2
+
3
+ class SplitByAggregationTest < ActiveSupport::TestCase
4
+ context "SplitByAggregation" do
5
+ setup do
6
+ @agg = Class.new
7
+ @agg.class_eval do
8
+ def self.name; "MyAggregation" end
9
+ include Wonkavision::Aggregation
10
+ dimension :a, :b, :c
11
+ measure :d, :e
12
+ aggregate_by :a, :b
13
+ aggregate_by :a, :b, :c
14
+ store :hash_store
15
+ end
16
+ @handler = Wonkavision::Analytics::SplitByAggregation.new
17
+ end
18
+
19
+ should "initialize with the appropriate namespace" do
20
+ assert_equal Wonkavision.join("wv", "analytics"), @handler.class.event_namespace
21
+ end
22
+
23
+ context "#aggregation_for" do
24
+ should "look up an aggregation for the provided name" do
25
+ assert_equal @agg, @handler.aggregation_for(@agg.name)
26
+ end
27
+ end
28
+
29
+ context "#split_dimensions_by_aggregation" do
30
+ setup do
31
+ @entity = {"a" => :a, "b" => :b, "c" => :c, "d" => :d, "e" => :e}
32
+ @split = @handler.split_dimensions_by_aggregation(@agg,@entity)
33
+ end
34
+ should "create one entry per aggregate_by" do
35
+ assert_equal 2, @split.length
36
+ end
37
+ should "create a hash of key values for each aggregation" do
38
+ assert_equal( { "a" => { "a" => :a}, "b" => { "b"=>:b} }, @split[0] )
39
+ assert_equal( { "a" => { "a" => :a}, "b" => { "b"=>:b} , "c"=>{ "c"=>:c} }, @split[1] )
40
+ end
41
+ end
42
+
43
+ context "#process_aggregations" do
44
+ should "call process on each message in the batch" do
45
+ path = Wonkavision.join("wv", "analytics", "aggregation", "updated")
46
+ @handler.expects(:submit).with(path, { :hi => "there"})
47
+ @handler.process_aggregations [{ :hi => "there"}]
48
+ end
49
+ end
50
+
51
+ context "#process_event" do
52
+ should "return false unless all appropriate metadata is present and valid" do
53
+ assert_equal false, @handler.process_event({"aggregation"=>"ack",
54
+ "action"=>"add","data"=>{}})
55
+
56
+ assert_equal false, @handler.process_event( { "aggregation"=>@agg.name,
57
+ "action"=>"add"})
58
+
59
+ assert_equal false, @handler.process_event( { "aggregation"=>@agg.name,
60
+ "data"=>{}})
61
+ end
62
+ context "with a valid message" do
63
+ setup do
64
+ @message = {
65
+ "aggregation" => @agg.name,
66
+ "action" => "add",
67
+ "data" => {
68
+ "a" => :a, "b" => :b, "c" => :c,
69
+ "d" => 1.0, "e" => 2.0
70
+ }
71
+ }
72
+ end
73
+
74
+ should "prepare a message for each aggregation" do
75
+ assert_equal 2, @handler.process_event(@message).length
76
+ end
77
+
78
+ should "submit each message for processing" do
79
+ @handler.expects(:submit).times(2)
80
+ @handler.process_event(@message)
81
+ end
82
+
83
+ should "not submit messages if the filter doesn't match" do
84
+ @agg.filter { |m|m["a"] != :a}
85
+ assert_equal 0, @handler.process_event(@message).length
86
+ end
87
+
88
+ should "copy the measures once for each aggregation" do
89
+ results = @handler.process_event(@message)
90
+ results.each do |result|
91
+ assert_equal "add", result["action"]
92
+ assert_equal @agg.name, result["aggregation"]
93
+ assert_equal( { "d" => 1.0, "e" => 2.0} , result["measures"] )
94
+ end
95
+ end
96
+
97
+ should "key each message with a unique aggregation" do
98
+ results = @handler.process_event(@message)
99
+ results[0][:dimensions] = { "a" => :a, "b" => :b}
100
+ results[1][:dimensions] = { "a" => :a, "b" => :b, "c" => :c}
101
+ end
102
+
103
+ end
104
+ context "will listen for entity updated messages" do
105
+ should "respond to entity updated messages" do
106
+ Wonkavision::Analytics::SplitByAggregation.any_instance.expects(:process_event)
107
+ Wonkavision.event_coordinator.receive_event("wv/analytics/facts/updated",{ :a=>:b})
108
+ end
109
+ end
110
+
111
+ end
112
+ end
113
+
114
+ end
@@ -0,0 +1,71 @@
1
+ require "test_helper"
2
+
3
+ class StoreTest < ActiveSupport::TestCase
4
+ Store = Wonkavision::Analytics::Persistence::Store
5
+
6
+ context "Store" do
7
+ setup do
8
+ @facts = Class.new
9
+ @facts.class_eval do
10
+ include Wonkavision::Facts
11
+ record_id :tada
12
+ end
13
+ @store = Store.new(@facts)
14
+ end
15
+
16
+ should "provide access to the underlying owner" do
17
+ assert_equal @facts, @store.owner
18
+ end
19
+
20
+ should "be able to extract a record_id from a message" do
21
+ assert_equal 123, @store.send(:assert_record_id,{ "tada" => 123 })
22
+ end
23
+
24
+ should "raise an exception if a record_id is requested but not found" do
25
+ assert_raise(RuntimeError) { @store.send(:assert_record_id,{ "haha" => 123})}
26
+ end
27
+
28
+ context "Public api" do
29
+ context "#update_facts" do
30
+ should "extract a record_id and delegate to update_facts_record" do
31
+ @store.expects(:update_facts_record).with(123,{ "tada"=>123})
32
+ @store.update_facts("tada"=>123)
33
+ end
34
+ end
35
+ context "#add_facts" do
36
+ should "extract a record_id and delegate to insert_facts_record" do
37
+ @store.expects(:insert_facts_record).with(123,{ "tada" => 123})
38
+ @store.add_facts("tada"=>123)
39
+ end
40
+ end
41
+ context "#remove_facts" do
42
+ should "extract a record_id and delegate to delete_facts_record" do
43
+ @store.expects(:delete_facts_record).with(123,{ "tada" => 123} )
44
+ @store.remove_facts("tada"=>123)
45
+ end
46
+ end
47
+ context "#execute_query" do
48
+ setup do
49
+ @query = Wonkavision::Analytics::Query.new
50
+ @query.select :a, :b, :on => :columns
51
+ @query.select :c, :on => :rows
52
+ @query.where :d=>:e
53
+ end
54
+ should "delegate to fetch tuples, passing the selected dimensions" do
55
+ @store.expects(:fetch_tuples).with([:a,:b,:c, :d],@query.filters)
56
+ @store.execute_query(@query)
57
+ end
58
+ should "pass an empty array of dimensions when nothing is selected" do
59
+ @store.expects(:fetch_tuples).with([],[])
60
+ @store.execute_query(Wonkavision::Analytics::Query.new)
61
+ end
62
+ end
63
+ context "Deriving from Store" do
64
+ should "register the derived class with the superclass" do
65
+ class NewStore < Store; end
66
+ assert_equal NewStore, Store[:new_store]
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,62 @@
1
+ require "test_helper"
2
+
3
+ class SymbolTest < ActiveSupport::TestCase
4
+ context "Symbol extensions" do
5
+ setup do
6
+ @symbol = :member
7
+ end
8
+ context "#key, #caption and #sort" do
9
+ should "produce MemberFilters of type dimension" do
10
+ [:key,:caption,:sort].each do |method|
11
+ filter = @symbol.send(method)
12
+ assert_equal @symbol, filter.name
13
+ assert_equal :dimension, filter.member_type
14
+ assert_equal method, filter.attribute_name
15
+ end
16
+ end
17
+ end
18
+ context "#sum, #sum2, #count" do
19
+ should "produce MemberFilters of type measure" do
20
+ [:sum,:sum2,:count].each do |method|
21
+ filter = @symbol.send(method)
22
+ assert_equal @symbol, filter.name
23
+ assert_equal :measure, filter.member_type
24
+ assert_equal method, filter.attribute_name
25
+ end
26
+ end
27
+ end
28
+
29
+ context "#[]" do
30
+ should "produce a MemberFilter with the attribute name specified by the indexer" do
31
+ filter = @symbol[:an_attribute]
32
+ assert_equal @symbol, filter.name
33
+ assert_equal :an_attribute, filter.attribute_name
34
+ end
35
+ end
36
+ context "method_missing" do
37
+ should "produce a MemberFilter with an attribute name of the method called" do
38
+ filter = @symbol.an_attribute
39
+ assert_equal @symbol, filter.name
40
+ assert_equal :an_attribute, filter.attribute_name
41
+ end
42
+ end
43
+ context "when the symbol is ':dimensions'" do
44
+ should "produce a MemberFilter with a dimension name as specified and a default attribute name" do
45
+ filter = :dimensions.a_dimension
46
+ assert_equal :a_dimension, filter.name
47
+ assert_equal :key, filter.attribute_name
48
+ assert_equal :dimension, filter.member_type
49
+ end
50
+ end
51
+ context "when the symbol is ':measures'" do
52
+ should "produce a MemberFilter with a measure name as specified and a default attribute name" do
53
+ filter = :measures.a_measure
54
+ assert_equal :a_measure, filter.name
55
+ assert_equal :count, filter.attribute_name
56
+ assert_equal :measure, filter.member_type
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -66,7 +66,7 @@ unless defined?(::TestEventHandler)
66
66
 
67
67
  Wonkavision::MessageMapper.register("evt4_test_map") do
68
68
  string 'test_id'
69
- date 'event_time'
69
+ time 'event_time'
70
70
  end
71
71
 
72
72
  end
@@ -0,0 +1,42 @@
1
+ class TestAggregation
2
+ include Wonkavision::Aggregation
3
+
4
+ store :hash_store
5
+
6
+ dimension :color, :size, :shape
7
+ measure :weight, :cost
8
+
9
+ aggregate_by :color
10
+ aggregate_by :size
11
+ aggregate_by :shape
12
+ aggregate_by :color, :size
13
+ aggregate_by :color, :shape
14
+ aggregate_by :size, :shape
15
+ aggregate_by :size, :color, :shape
16
+
17
+ @i = 0
18
+ def self.send_messages
19
+ colors = %w(red green red green yellow black black white red yellow)
20
+ sizes = %w(large large small medium medium small large small medium small)
21
+ shapes = %w(square circle rectangle rectangle circle square circle rectangle square square)
22
+ weights = [1.0, 2.0, 1.1, 2.1, 1.2, 2.2, 1.3, 2.3,4.5,6.5]
23
+ costs = [5, 10, 15, 20, 15, 20, 5, 8, 9, 20]
24
+
25
+ (0..9).each do |idx|
26
+ Wonkavision.event_coordinator.submit_job "wv/analytics/entity/updated", {
27
+ "aggregation" => "TestAggregation",
28
+ "action" => "add",
29
+ "entity" => {
30
+ "color" => colors[idx],
31
+ "size" => sizes[idx],
32
+ "shape" => shapes[idx],
33
+ "weight" => weights[idx],
34
+ "cost" => costs[idx]
35
+ }
36
+ }
37
+ @i+=1
38
+ print @i % 100 == 0 ? @i : "."
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,100 @@
1
+ [{"_id"=>BSON::ObjectId('4d470fb0ca248815e73bd89a'),
2
+ "dimension_keys"=>["red", "square", "large"],
3
+ "dimension_names"=>["color", "shape", "size"],
4
+ "dimensions"=>
5
+ {"size"=>{"size"=>"large"},
6
+ "shape"=>{"shape"=>"square"},
7
+ "color"=>{"color"=>"red"}},
8
+ "measures"=>
9
+ {"cost"=>{"count"=>10, "sum"=>50, "sum2"=>250},
10
+ "weight"=>{"count"=>10, "sum"=>10.0, "sum2"=>10.0}}},
11
+ {"_id"=>BSON::ObjectId('4d470fb0ca248815e73bd8a0'),
12
+ "dimension_keys"=>["green", "circle", "large"],
13
+ "dimension_names"=>["color", "shape", "size"],
14
+ "dimensions"=>
15
+ {"size"=>{"size"=>"large"},
16
+ "shape"=>{"shape"=>"circle"},
17
+ "color"=>{"color"=>"green"}},
18
+ "measures"=>
19
+ {"cost"=>{"count"=>10, "sum"=>100, "sum2"=>1000},
20
+ "weight"=>{"count"=>10, "sum"=>20.0, "sum2"=>40.0}}},
21
+ {"_id"=>BSON::ObjectId('4d470fb0ca248815e73bd8a6'),
22
+ "dimension_keys"=>["red", "rectangle", "small"],
23
+ "dimension_names"=>["color", "shape", "size"],
24
+ "dimensions"=>
25
+ {"size"=>{"size"=>"small"},
26
+ "shape"=>{"shape"=>"rectangle"},
27
+ "color"=>{"color"=>"red"}},
28
+ "measures"=>
29
+ {"cost"=>{"count"=>10, "sum"=>150, "sum2"=>2250},
30
+ "weight"=>{"count"=>10, "sum"=>11.0, "sum2"=>12.1}}},
31
+ {"_id"=>BSON::ObjectId('4d470fb0ca248815e73bd8ab'),
32
+ "dimension_keys"=>["green", "rectangle", "medium"],
33
+ "dimension_names"=>["color", "shape", "size"],
34
+ "dimensions"=>
35
+ {"size"=>{"size"=>"medium"},
36
+ "shape"=>{"shape"=>"rectangle"},
37
+ "color"=>{"color"=>"green"}},
38
+ "measures"=>
39
+ {"cost"=>{"count"=>10, "sum"=>200, "sum2"=>4000},
40
+ "weight"=>{"count"=>10, "sum"=>21.0, "sum2"=>44.1}}},
41
+ {"_id"=>BSON::ObjectId('4d470fb0ca248815e73bd8b0'),
42
+ "dimension_keys"=>["yellow", "circle", "medium"],
43
+ "dimension_names"=>["color", "shape", "size"],
44
+ "dimensions"=>
45
+ {"size"=>{"size"=>"medium"},
46
+ "shape"=>{"shape"=>"circle"},
47
+ "color"=>{"color"=>"yellow"}},
48
+ "measures"=>
49
+ {"cost"=>{"count"=>10, "sum"=>150, "sum2"=>2250},
50
+ "weight"=>{"count"=>10, "sum"=>12.0, "sum2"=>14.4}}},
51
+ {"_id"=>BSON::ObjectId('4d470fb0ca248815e73bd8b5'),
52
+ "dimension_keys"=>["black", "square", "small"],
53
+ "dimension_names"=>["color", "shape", "size"],
54
+ "dimensions"=>
55
+ {"size"=>{"size"=>"small"},
56
+ "shape"=>{"shape"=>"square"},
57
+ "color"=>{"color"=>"black"}},
58
+ "measures"=>
59
+ {"cost"=>{"count"=>10, "sum"=>200, "sum2"=>4000},
60
+ "weight"=>{"count"=>10, "sum"=>22.0, "sum2"=>48.4}}},
61
+ {"_id"=>BSON::ObjectId('4d470fb0ca248815e73bd8b8'),
62
+ "dimension_keys"=>["black", "circle", "large"],
63
+ "dimension_names"=>["color", "shape", "size"],
64
+ "dimensions"=>
65
+ {"size"=>{"size"=>"large"},
66
+ "shape"=>{"shape"=>"circle"},
67
+ "color"=>{"color"=>"black"}},
68
+ "measures"=>
69
+ {"cost"=>{"count"=>10, "sum"=>50, "sum2"=>250},
70
+ "weight"=>{"count"=>10, "sum"=>13.0, "sum2"=>16.9}}},
71
+ {"_id"=>BSON::ObjectId('4d470fb0ca248815e73bd8bc'),
72
+ "dimension_keys"=>["white", "rectangle", "small"],
73
+ "dimension_names"=>["color", "shape", "size"],
74
+ "dimensions"=>
75
+ {"size"=>{"size"=>"small"},
76
+ "shape"=>{"shape"=>"rectangle"},
77
+ "color"=>{"color"=>"white"}},
78
+ "measures"=>
79
+ {"cost"=>{"count"=>10, "sum"=>80, "sum2"=>640},
80
+ "weight"=>{"count"=>10, "sum"=>23.0, "sum2"=>52.9}}},
81
+ {"_id"=>BSON::ObjectId('4d470fb0ca248815e73bd8bf'),
82
+ "dimension_keys"=>["red", "square", "medium"],
83
+ "dimension_names"=>["color", "shape", "size"],
84
+ "dimensions"=>
85
+ {"size"=>{"size"=>"medium"},
86
+ "shape"=>{"shape"=>"square"},
87
+ "color"=>{"color"=>"red"}},
88
+ "measures"=>
89
+ {"cost"=>{"count"=>10, "sum"=>90, "sum2"=>810},
90
+ "weight"=>{"count"=>10, "sum"=>45.0, "sum2"=>202.5}}},
91
+ {"_id"=>BSON::ObjectId('4d470fb0ca248815e73bd8c2'),
92
+ "dimension_keys"=>["yellow", "square", "small"],
93
+ "dimension_names"=>["color", "shape", "size"],
94
+ "dimensions"=>
95
+ {"size"=>{"size"=>"small"},
96
+ "shape"=>{"shape"=>"square"},
97
+ "color"=>{"color"=>"yellow"}},
98
+ "measures"=>
99
+ {"cost"=>{"count"=>10, "sum"=>200, "sum2"=>4000},
100
+ "weight"=>{"count"=>10, "sum"=>65.0, "sum2"=>422.5}}}]