wonkavision 0.5.11 → 0.6.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.
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}}}]