sleek 0.0.1 → 0.0.2

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 (38) hide show
  1. data/README.md +95 -20
  2. data/lib/sleek.rb +4 -2
  3. data/lib/sleek/core_ext/range.rb +6 -7
  4. data/lib/sleek/event.rb +4 -1
  5. data/lib/sleek/filter.rb +11 -0
  6. data/lib/sleek/group_by_criteria.rb +144 -0
  7. data/lib/sleek/interval.rb +21 -20
  8. data/lib/sleek/{base.rb → namespace.rb} +13 -8
  9. data/lib/sleek/queries.rb +0 -1
  10. data/lib/sleek/queries/average.rb +1 -1
  11. data/lib/sleek/queries/count_unique.rb +1 -1
  12. data/lib/sleek/queries/maximum.rb +1 -1
  13. data/lib/sleek/queries/minimum.rb +1 -1
  14. data/lib/sleek/queries/query.rb +70 -37
  15. data/lib/sleek/queries/sum.rb +1 -1
  16. data/lib/sleek/query_collection.rb +3 -3
  17. data/lib/sleek/query_command.rb +71 -0
  18. data/lib/sleek/timeframe.rb +45 -52
  19. data/lib/sleek/version.rb +1 -1
  20. data/spec/lib/sleek/event_spec.rb +3 -3
  21. data/spec/lib/sleek/filter_spec.rb +3 -3
  22. data/spec/lib/sleek/group_by_criteria_spec.rb +139 -0
  23. data/spec/lib/sleek/interval_spec.rb +6 -5
  24. data/spec/lib/sleek/{base_spec.rb → namespace_spec.rb} +15 -8
  25. data/spec/lib/sleek/queries/average_spec.rb +1 -1
  26. data/spec/lib/sleek/queries/count_spec.rb +1 -1
  27. data/spec/lib/sleek/queries/count_unique_spec.rb +1 -1
  28. data/spec/lib/sleek/queries/maximum_spec.rb +1 -1
  29. data/spec/lib/sleek/queries/minimum_spec.rb +1 -1
  30. data/spec/lib/sleek/queries/query_spec.rb +58 -84
  31. data/spec/lib/sleek/queries/sum_spec.rb +1 -1
  32. data/spec/lib/sleek/query_collection_spec.rb +11 -9
  33. data/spec/lib/sleek/query_command_spec.rb +171 -0
  34. data/spec/lib/sleek/timeframe_spec.rb +70 -36
  35. data/spec/lib/sleek_spec.rb +2 -2
  36. metadata +11 -8
  37. data/lib/sleek/queries/targetable.rb +0 -13
  38. data/spec/lib/sleek/queries/targetable_spec.rb +0 -29
@@ -1,12 +1,12 @@
1
1
  module Sleek
2
- class Base
3
- attr_reader :namespace
2
+ class Namespace
3
+ attr_reader :name
4
4
 
5
5
  # Internal: Initialize Sleek with namespace.
6
6
  #
7
7
  # namespace - the Symbol namespace name.
8
- def initialize(namespace)
9
- @namespace = namespace
8
+ def initialize(name)
9
+ @name = name
10
10
  end
11
11
 
12
12
  # Public: Record an event.
@@ -14,12 +14,17 @@ module Sleek
14
14
  # bucket - the String name of bucket.
15
15
  # payload - the Hash of event data.
16
16
  def record(bucket, payload)
17
- Event.create_with_namespace(namespace, bucket, payload)
17
+ Event.create_with_namespace(name, bucket, payload)
18
18
  end
19
19
 
20
20
  # Public: Get `QueriesCollection` for the namespace.
21
21
  def queries
22
- @queries ||= QueryCollection.new(namespace)
22
+ @queries ||= QueryCollection.new(self)
23
+ end
24
+
25
+ # Public: Delete the namespace.
26
+ def delete!
27
+ events.delete_all
23
28
  end
24
29
 
25
30
  # Public: Delete event bucket.
@@ -40,13 +45,13 @@ module Sleek
40
45
  # Internal: Get events associated with current namespace and,
41
46
  # optionally, specified bucket.
42
47
  def events(bucket = nil)
43
- evts = Event.where(namespace: namespace)
48
+ evts = Event.where(namespace: name)
44
49
  evts = evts.where(bucket: bucket) if bucket.present?
45
50
  evts
46
51
  end
47
52
 
48
53
  def inspect
49
- "#<Sleek::Base ns=#{namespace}>"
54
+ "#<Sleek::Namespace #{name}>"
50
55
  end
51
56
  end
52
57
  end
@@ -1,5 +1,4 @@
1
1
  require 'sleek/queries/query'
2
- require 'sleek/queries/targetable'
3
2
  require 'sleek/queries/count'
4
3
  require 'sleek/queries/count_unique'
5
4
  require 'sleek/queries/minimum'
@@ -11,7 +11,7 @@ module Sleek
11
11
  # sleek.queries.average(:purchases, target_property: "total")
12
12
  # # => 49_35
13
13
  class Average < Query
14
- include Targetable
14
+ require_target_property!
15
15
 
16
16
  def perform(events)
17
17
  events.avg target_property
@@ -11,7 +11,7 @@ module Sleek
11
11
  # sleek.queries.count_unique(:purchases, target_property: "customer.email")
12
12
  # # => 4
13
13
  class CountUnique < Query
14
- include Targetable
14
+ require_target_property!
15
15
 
16
16
  def perform(events)
17
17
  events.distinct(target_property).count
@@ -11,7 +11,7 @@ module Sleek
11
11
  # sleek.queries.maximum(:purchases, target_property: "total")
12
12
  # # => 199_99
13
13
  class Maximum < Query
14
- include Targetable
14
+ require_target_property!
15
15
 
16
16
  def perform(events)
17
17
  events.max target_property
@@ -11,7 +11,7 @@ module Sleek
11
11
  # sleek.queries.minimum(:purchases, target_property: "total")
12
12
  # # => 19_99
13
13
  class Minimum < Query
14
- include Targetable
14
+ require_target_property!
15
15
 
16
16
  def perform(events)
17
17
  events.min target_property
@@ -1,11 +1,30 @@
1
1
  module Sleek
2
2
  module Queries
3
+ # Public: The query.
4
+ #
5
+ # Queries are performed on a set of events and usually return
6
+ # numeric values. You shouldn't be using Sleek::Queries::Query
7
+ # directly, instead, you should subclass it and define `#perform` on
8
+ # it, which takes an events criteria and does its job.
9
+ #
10
+ # Sleek::Queries::Query would take care of processing options,
11
+ # filtering events, handling series and groups.
12
+ #
13
+ # Examples
14
+ #
15
+ # class SomeQuery < Query
16
+ # def perform(events)
17
+ # ...
18
+ # end
19
+ # end
3
20
  class Query
4
- attr_reader :namespace, :bucket, :options
21
+ attr_reader :namespace, :bucket, :options, :timeframe
22
+
23
+ delegate :require_target_property?, to: 'self.class'
5
24
 
6
25
  # Internal: Initialize the query.
7
26
  #
8
- # namespace - the Symbol namespace name.
27
+ # namespace - the Sleek::Namespace object.
9
28
  # bucket - the String bucket name.
10
29
  # options - the optional Hash of options.
11
30
  # :timeframe - the optional timeframe description.
@@ -16,29 +35,35 @@ module Sleek
16
35
  @namespace = namespace
17
36
  @bucket = bucket
18
37
  @options = options
38
+ @timeframe = options[:timeframe]
19
39
 
20
- raise ArgumentError, "options are invalid" unless valid_options?
40
+ raise ArgumentError, 'options are invalid' unless valid_options?
21
41
  end
22
42
 
23
43
  # Internal: Get Mongoid::Criteria for events to perform the query.
24
44
  #
25
45
  # time_range - the optional range of Time objects.
26
- def events(time_range = time_range)
27
- evts = Event.where(namespace: namespace, bucket: bucket)
28
- evts = evts.between("s.t" => time_range) if time_range
46
+ def events
47
+ evts = namespace.events(bucket)
48
+ evts = evts.between('s.t' => timeframe) if timeframe?
29
49
  evts = apply_filters(evts) if filter?
50
+
51
+ if group_by.present?
52
+ evts = Sleek::GroupByCriteria.new(evts, "d.#{group_by}")
53
+ end
54
+
30
55
  evts
31
56
  end
32
57
 
33
58
  # Internal: Apply all the filters to the criteria.
34
59
  def apply_filters(criteria)
35
- filters.inject(criteria) { |crit, filter| filter.apply(crit) }
60
+ filters.reduce(criteria) { |crit, filter| filter.apply(crit) }
36
61
  end
37
62
 
38
63
  # Internal: Get filters.
39
64
  def filters
40
65
  filters = options[:filter]
41
-
66
+
42
67
  if filters.is_a?(Array) && filters.size == 3 && filters.none? { |f| f.is_a?(Array) }
43
68
  filters = [filters]
44
69
  elsif !filters.is_a?(Array) || !filters.all? { |f| f.is_a?(Array) && f.size == 3 }
@@ -48,22 +73,6 @@ module Sleek
48
73
  filters.map { |f| Sleek::Filter.new(*f) }
49
74
  end
50
75
 
51
- # Internal: Get timeframe for the query.
52
- #
53
- # Returns nil if no timeframe was passed to initializer.
54
- def timeframe
55
- Sleek::Timeframe.new(options[:timeframe]) if timeframe?
56
- end
57
-
58
- # Internal: Get timeframe range.
59
- def time_range
60
- timeframe.try(:to_time_range)
61
- end
62
-
63
- def series
64
- Sleek::Interval.new(options[:interval], timeframe).timeframes if series? && timeframe?
65
- end
66
-
67
76
  # Internal: Check if options include filter.
68
77
  def filter?
69
78
  options[:filter].present?
@@ -71,23 +80,24 @@ module Sleek
71
80
 
72
81
  # Internal: Check if options include timeframe.
73
82
  def timeframe?
74
- options[:timeframe].present?
83
+ timeframe.present?
75
84
  end
76
85
 
77
- # Internal: Check if options include interval.
78
- def series?
79
- options[:interval].present?
86
+ # Internal: Get group_by property.
87
+ def group_by
88
+ options[:group_by]
89
+ end
90
+
91
+ # Internal: Get the target property.
92
+ def target_property
93
+ if options[:target_property].present?
94
+ "d.#{options[:target_property]}"
95
+ end
80
96
  end
81
97
 
82
98
  # Internal: Run the query.
83
99
  def run
84
- if series?
85
- series.map do |tf|
86
- { timeframe: tf, value: perform(events(tf.to_time_range)) }
87
- end
88
- else
89
- perform(events)
90
- end
100
+ perform(events)
91
101
  end
92
102
 
93
103
  # Internal: Perform the query on a set of events.
@@ -97,8 +107,31 @@ module Sleek
97
107
 
98
108
  # Internal: Validate the options.
99
109
  def valid_options?
100
- options.is_a?(Hash) && (filter? ? options[:filter].is_a?(Array) : true) &&
101
- (series? ? timeframe? && series : true)
110
+ options.is_a?(Hash) &&
111
+ (filter? ? options[:filter].is_a?(Array) : true) &&
112
+ (require_target_property? ? options[:target_property].present? : true)
113
+ end
114
+
115
+ class << self
116
+ # Public: Indicate that the query requires target property.
117
+ #
118
+ # Examples
119
+ #
120
+ # class SomeQuery < Query
121
+ # require_target_property!
122
+ #
123
+ # def perform(events)
124
+ # ...
125
+ # end
126
+ # end
127
+ def require_target_property!
128
+ @require_target_property = true
129
+ end
130
+
131
+ # Public: Check if the query requires target property.
132
+ def require_target_property?
133
+ !!@require_target_property
134
+ end
102
135
  end
103
136
  end
104
137
  end
@@ -11,7 +11,7 @@ module Sleek
11
11
  # sleek.queries.sum(:purchases, target_property: "total")
12
12
  # # => 2_072_70
13
13
  class Sum < Query
14
- include Targetable
14
+ require_target_property!
15
15
 
16
16
  def perform(events)
17
17
  events.sum target_property
@@ -4,7 +4,7 @@ module Sleek
4
4
 
5
5
  # Inernal: Initialize query collection.
6
6
  #
7
- # namespace - the Symbol namespace name.
7
+ # namespace - the Sleek::Namespace object.
8
8
  def initialize(namespace)
9
9
  @namespace = namespace
10
10
  end
@@ -14,12 +14,12 @@ module Sleek
14
14
  klass = "Sleek::Queries::#{name.to_s.camelize}".constantize
15
15
 
16
16
  define_method(name) do |bucket, options = {}|
17
- klass.new(namespace, bucket, options).run
17
+ QueryCommand.new(klass, namespace, bucket, options).run
18
18
  end
19
19
  end
20
20
 
21
21
  def inspect
22
- "#<Sleek::QueryCollection ns=#{namespace}>"
22
+ "#<Sleek::QueryCollection ns=#{namespace.name}>"
23
23
  end
24
24
  end
25
25
  end
@@ -0,0 +1,71 @@
1
+ module Sleek
2
+ # Internal: A query command. It's primarily responsible for breaking a
3
+ # timeframe into intervals (if applicable), running the query on each
4
+ # sub-timeframe, and wrapping up a result.
5
+ class QueryCommand
6
+ attr_reader :klass, :namespace, :bucket, :options
7
+
8
+ # Internal: Initialize the query command.
9
+ #
10
+ # klass - the Sleek::Queries::Query subclass.
11
+ # namespace - the Sleek::Namespace object.
12
+ # bucket - the String bucket name.
13
+ # options - the optional Hash of options. Everything but
14
+ # :timeframe and :interval will be passed on to the
15
+ # query class.
16
+ # :timeframe - the optional timeframe description.
17
+ # :timezone - the optional TZ identifier.
18
+ # :interval - the optional interval description. If
19
+ # passed, requires that :timeframe is passed
20
+ # as well.
21
+ #
22
+ # Raises ArgumentError if :interval is passed but :timeframe is not.
23
+ def initialize(klass, namespace, bucket, options = {})
24
+ @klass = klass
25
+ @namespace = namespace
26
+ @bucket = bucket
27
+ @timeframe = options.delete(:timeframe)
28
+ @timezone = options.delete(:timezone)
29
+ @interval = options.delete(:interval)
30
+ @options = options
31
+
32
+ if @interval.present? && @timeframe.blank?
33
+ raise ArgumentError, 'interval requires timeframe'
34
+ end
35
+ end
36
+
37
+ # Internal: Check if options include interval.
38
+ def series?
39
+ @interval.present?
40
+ end
41
+
42
+ # Internal: Parse a time range from the timeframe description.
43
+ # description.
44
+ def timeframe
45
+ Sleek::Timeframe.to_range(@timeframe, @timezone) if @timeframe
46
+ end
47
+
48
+ # Internal: Split timeframe into sub-timeframes of interval.
49
+ def series
50
+ Sleek::Interval.new(@interval, timeframe).timeframes
51
+ end
52
+
53
+ # Internal: Instantiate a query object.
54
+ #
55
+ # timeframe - the time range.
56
+ def new_query(timeframe)
57
+ klass.new(namespace, bucket, options.merge(timeframe: timeframe))
58
+ end
59
+
60
+ # Internal: Run the query on each timeframe.
61
+ def run
62
+ if series?
63
+ series.map do |tf|
64
+ { timeframe: tf, value: new_query(tf).run }
65
+ end
66
+ else
67
+ new_query(timeframe).run
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,40 +1,6 @@
1
1
  module Sleek
2
2
  class Timeframe
3
- # Internal: Initialize timeframe.
4
- #
5
- # timeframe - either a range of Time objects, an array of two Time
6
- # objects, or a special string.
7
- #
8
- # Possible special string format:
9
- # (this|previous)_((\d+)_)?(minute|hour|day|week|month)s?
10
- #
11
- # Examples
12
- #
13
- # Timeframe.new "this_2_days"
14
- #
15
- # Timeframe.new "previous_hour"
16
- def initialize(timeframe)
17
- @timeframe = timeframe
18
- end
19
-
20
- # Public: Get timeframe start.
21
- def start
22
- to_time_range.begin
23
- end
24
-
25
- # Public: Get timeframe end.
26
- def end
27
- to_time_range.end
28
- end
29
-
30
- # Internal: Convert Timeframe instance to a range of Time objects.
31
- def to_time_range
32
- @range ||= self.class.to_range(@timeframe)
33
- end
34
-
35
- def inspect
36
- "#<Sleek::Timeframe #{to_time_range}>"
37
- end
3
+ REGEXP = /(this|previous)_((\d+)_)?(minute|hour|day|week|month)s?/
38
4
 
39
5
  class << self
40
6
  # Internal: Transform the object passed to Timeframe initializer
@@ -42,44 +8,71 @@ module Sleek
42
8
  #
43
9
  # timeframe - either a Range of Time objects, a two-element array
44
10
  # of Time Objects, or a special string.
11
+ # timezone - the optional String TZ identifier. See
12
+ # `ActiveSupport::TimeZone`.
13
+ #
14
+ # Examples
15
+ #
16
+ # Timeframe.to_range :this_2_days
17
+ #
18
+ # Timeframe.to_range :previous_hour
19
+ #
20
+ # Timeframe.to_range :this_day, timezone: 'US/Pacific'
45
21
  #
46
22
  # Raises ArgumentError if passed object can't be processed.
47
- def to_range(timeframe)
23
+ def to_range(timeframe, timezone = nil)
48
24
  case timeframe
49
- when Range
50
- t = timeframe if timeframe.time_range?
51
- when Array
52
- t = timeframe.first..timeframe.last if timeframe.size == 2 && timeframe.count { |tf| tf.is_a?(Time) } == 2
25
+ when proc { |tf| tf.is_a?(Range) && tf.time_range? }
26
+ t = timeframe
27
+ when proc { |tf| tf.is_a?(Array) && tf.size == 2 && tf.count { |_tf| _tf.is_a?(Time) } == 2 }
28
+ t = timeframe.first..timeframe.last
53
29
  when String, Symbol
54
- t = parse(timeframe.to_s)
30
+ t = parse(timeframe.to_s, timezone)
31
+ else
32
+ raise ArgumentError, "wrong timeframe - #{timeframe}"
55
33
  end
56
-
57
- raise ArgumentError, "wrong timeframe" unless t
58
- t
59
34
  end
60
35
 
61
36
  # Internal: Process timeframe string to make up a range.
62
37
  #
63
38
  # timeframe - the String matching
64
39
  # (this|previous)_((\d+)_)?(minute|hour|day|week|month)s?
65
- def parse(timeframe)
66
- regexp = /(this|previous)_((\d+)_)?(minute|hour|day|week|month)s?/
67
- _, category, _, number, interval = *timeframe.match(regexp)
40
+ # timezone - the optional String TZ identifier. See
41
+ # `ActiveSupport::TimeZone`.
42
+ def parse(timeframe, timezone = nil)
43
+ _, category, _, number, interval = *timeframe.match(REGEXP)
68
44
 
69
45
  unless category && interval
70
- raise ArgumentError, "special timeframe string is malformed"
46
+ raise ArgumentError, 'special timeframe string is malformed'
71
47
  end
72
48
 
73
49
  number ||= 1
74
50
  number = number.to_i
75
51
 
76
- end_point = Time.now.send("end_of_#{interval}").round
77
- start_point = end_point - number.send(interval)
78
-
79
- range = start_point..end_point
52
+ range = range_from_interval(interval, number, timezone)
80
53
  range = range - 1.send(interval) if category == 'previous'
81
54
  range
82
55
  end
56
+
57
+ # Internal: Create a time range from interval type & number of
58
+ # intervals.
59
+ #
60
+ # interval - the String interval type name. Valid values are
61
+ # minute, hour, day, week, and month.
62
+ # number - the Integer number of periods.
63
+ # timezone - the optional String TZ identifier. See
64
+ # `ActiveSupport::TimeZone`.
65
+ #
66
+ # Returns the range of TimeWithZone objects.
67
+ def range_from_interval(interval, number = 1, timezone = nil)
68
+ timezone ||= 'UTC'
69
+ now = ActiveSupport::TimeZone.new(timezone).now
70
+
71
+ end_point = now.send("end_of_#{interval}").round
72
+ start_point = end_point - number.send(interval)
73
+
74
+ start_point..end_point
75
+ end
83
76
  end
84
77
  end
85
78
  end