sleek 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +22 -0
  6. data/README.md +256 -0
  7. data/Rakefile +6 -0
  8. data/lib/sleek/base.rb +52 -0
  9. data/lib/sleek/core_ext/range.rb +44 -0
  10. data/lib/sleek/core_ext/time.rb +2 -0
  11. data/lib/sleek/event.rb +37 -0
  12. data/lib/sleek/filter.rb +24 -0
  13. data/lib/sleek/interval.rb +41 -0
  14. data/lib/sleek/queries/average.rb +21 -0
  15. data/lib/sleek/queries/count.rb +17 -0
  16. data/lib/sleek/queries/count_unique.rb +21 -0
  17. data/lib/sleek/queries/maximum.rb +21 -0
  18. data/lib/sleek/queries/minimum.rb +21 -0
  19. data/lib/sleek/queries/query.rb +105 -0
  20. data/lib/sleek/queries/sum.rb +21 -0
  21. data/lib/sleek/queries/targetable.rb +13 -0
  22. data/lib/sleek/queries.rb +8 -0
  23. data/lib/sleek/query_collection.rb +25 -0
  24. data/lib/sleek/timeframe.rb +85 -0
  25. data/lib/sleek/version.rb +3 -0
  26. data/lib/sleek.rb +23 -0
  27. data/sleek.gemspec +28 -0
  28. data/sleek.png +0 -0
  29. data/spec/lib/sleek/base_spec.rb +48 -0
  30. data/spec/lib/sleek/event_spec.rb +21 -0
  31. data/spec/lib/sleek/filter_spec.rb +26 -0
  32. data/spec/lib/sleek/interval_spec.rb +24 -0
  33. data/spec/lib/sleek/queries/average_spec.rb +13 -0
  34. data/spec/lib/sleek/queries/count_spec.rb +13 -0
  35. data/spec/lib/sleek/queries/count_unique_spec.rb +15 -0
  36. data/spec/lib/sleek/queries/maximum_spec.rb +13 -0
  37. data/spec/lib/sleek/queries/minimum_spec.rb +13 -0
  38. data/spec/lib/sleek/queries/query_spec.rb +226 -0
  39. data/spec/lib/sleek/queries/sum_spec.rb +13 -0
  40. data/spec/lib/sleek/queries/targetable_spec.rb +29 -0
  41. data/spec/lib/sleek/query_collection_spec.rb +27 -0
  42. data/spec/lib/sleek/timeframe_spec.rb +86 -0
  43. data/spec/lib/sleek_spec.rb +10 -0
  44. data/spec/spec_helper.rb +17 -0
  45. metadata +203 -0
@@ -0,0 +1,105 @@
1
+ module Sleek
2
+ module Queries
3
+ class Query
4
+ attr_reader :namespace, :bucket, :options
5
+
6
+ # Internal: Initialize the query.
7
+ #
8
+ # namespace - the Symbol namespace name.
9
+ # bucket - the String bucket name.
10
+ # options - the optional Hash of options.
11
+ # :timeframe - the optional timeframe description.
12
+ # :interval - the optional interval description.
13
+ #
14
+ # Raises ArgumentError if passed options are invalid.
15
+ def initialize(namespace, bucket, options = {})
16
+ @namespace = namespace
17
+ @bucket = bucket
18
+ @options = options
19
+
20
+ raise ArgumentError, "options are invalid" unless valid_options?
21
+ end
22
+
23
+ # Internal: Get Mongoid::Criteria for events to perform the query.
24
+ #
25
+ # 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
29
+ evts = apply_filters(evts) if filter?
30
+ evts
31
+ end
32
+
33
+ # Internal: Apply all the filters to the criteria.
34
+ def apply_filters(criteria)
35
+ filters.inject(criteria) { |crit, filter| filter.apply(crit) }
36
+ end
37
+
38
+ # Internal: Get filters.
39
+ def filters
40
+ filters = options[:filter]
41
+
42
+ if filters.is_a?(Array) && filters.size == 3 && filters.none? { |f| f.is_a?(Array) }
43
+ filters = [filters]
44
+ elsif !filters.is_a?(Array) || !filters.all? { |f| f.is_a?(Array) && f.size == 3 }
45
+ raise ArgumentError, "wrong filter - #{filters}"
46
+ end
47
+
48
+ filters.map { |f| Sleek::Filter.new(*f) }
49
+ end
50
+
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
+ # Internal: Check if options include filter.
68
+ def filter?
69
+ options[:filter].present?
70
+ end
71
+
72
+ # Internal: Check if options include timeframe.
73
+ def timeframe?
74
+ options[:timeframe].present?
75
+ end
76
+
77
+ # Internal: Check if options include interval.
78
+ def series?
79
+ options[:interval].present?
80
+ end
81
+
82
+ # Internal: Run the query.
83
+ 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
91
+ end
92
+
93
+ # Internal: Perform the query on a set of events.
94
+ def perform(events)
95
+ raise NotImplementedError
96
+ end
97
+
98
+ # Internal: Validate the options.
99
+ def valid_options?
100
+ options.is_a?(Hash) && (filter? ? options[:filter].is_a?(Array) : true) &&
101
+ (series? ? timeframe? && series : true)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,21 @@
1
+ module Sleek
2
+ module Queries
3
+ # Internal: Sum query.
4
+ #
5
+ # Finds the average value for a given property.
6
+ #
7
+ # target_property - the String name of target property on event.
8
+ #
9
+ # Examples
10
+ #
11
+ # sleek.queries.sum(:purchases, target_property: "total")
12
+ # # => 2_072_70
13
+ class Sum < Query
14
+ include Targetable
15
+
16
+ def perform(events)
17
+ events.sum target_property
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ module Sleek
2
+ module Queries
3
+ module Targetable
4
+ def valid_options?
5
+ super && options[:target_property].present?
6
+ end
7
+
8
+ def target_property
9
+ "d.#{options[:target_property]}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ require 'sleek/queries/query'
2
+ require 'sleek/queries/targetable'
3
+ require 'sleek/queries/count'
4
+ require 'sleek/queries/count_unique'
5
+ require 'sleek/queries/minimum'
6
+ require 'sleek/queries/maximum'
7
+ require 'sleek/queries/average'
8
+ require 'sleek/queries/sum'
@@ -0,0 +1,25 @@
1
+ module Sleek
2
+ class QueryCollection
3
+ attr_reader :namespace
4
+
5
+ # Inernal: Initialize query collection.
6
+ #
7
+ # namespace - the Symbol namespace name.
8
+ def initialize(namespace)
9
+ @namespace = namespace
10
+ end
11
+
12
+ # Define method for each query
13
+ [:count, :count_unique, :minimum, :maximum, :sum, :average].each do |name|
14
+ klass = "Sleek::Queries::#{name.to_s.camelize}".constantize
15
+
16
+ define_method(name) do |bucket, options = {}|
17
+ klass.new(namespace, bucket, options).run
18
+ end
19
+ end
20
+
21
+ def inspect
22
+ "#<Sleek::QueryCollection ns=#{namespace}>"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,85 @@
1
+ module Sleek
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
38
+
39
+ class << self
40
+ # Internal: Transform the object passed to Timeframe initializer
41
+ # into a range of Time objects.
42
+ #
43
+ # timeframe - either a Range of Time objects, a two-element array
44
+ # of Time Objects, or a special string.
45
+ #
46
+ # Raises ArgumentError if passed object can't be processed.
47
+ def to_range(timeframe)
48
+ 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
53
+ when String, Symbol
54
+ t = parse(timeframe.to_s)
55
+ end
56
+
57
+ raise ArgumentError, "wrong timeframe" unless t
58
+ t
59
+ end
60
+
61
+ # Internal: Process timeframe string to make up a range.
62
+ #
63
+ # timeframe - the String matching
64
+ # (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)
68
+
69
+ unless category && interval
70
+ raise ArgumentError, "special timeframe string is malformed"
71
+ end
72
+
73
+ number ||= 1
74
+ number = number.to_i
75
+
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
80
+ range = range - 1.send(interval) if category == 'previous'
81
+ range
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,3 @@
1
+ module Sleek
2
+ VERSION = "0.0.1"
3
+ end
data/lib/sleek.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'mongoid'
2
+
3
+ require 'sleek/core_ext/range'
4
+ require 'sleek/core_ext/time'
5
+
6
+ require 'sleek/version'
7
+ require 'sleek/timeframe'
8
+ require 'sleek/interval'
9
+ require 'sleek/filter'
10
+ require 'sleek/event'
11
+ require 'sleek/queries'
12
+ require 'sleek/query_collection'
13
+ require 'sleek/base'
14
+
15
+ module Sleek
16
+ def self.for_namespace(namespace)
17
+ Base.new namespace
18
+ end
19
+
20
+ def self.[](namespace)
21
+ for_namespace(namespace)
22
+ end
23
+ end
data/sleek.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sleek/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sleek"
8
+ spec.version = Sleek::VERSION
9
+ spec.authors = ["Gosha Arinich"]
10
+ spec.email = ["me@goshakkk.name"]
11
+ spec.description = %q{Sleek is a library for doing analytics.}
12
+ spec.summary = %q{Sleek is a library for doing analytics.}
13
+ spec.homepage = "http://github.com/goshakkk/sleek"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'mongoid', '~> 3.1'
22
+ spec.add_runtime_dependency 'activesupport', '~> 3.2'
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency 'rspec', '~> 2.13'
27
+ spec.add_development_dependency 'database_cleaner', '~> 0.9'
28
+ end
data/sleek.png ADDED
Binary file
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Base do
4
+ subject(:sleek) { Sleek::Base.new(:default) }
5
+
6
+ describe "#initialize" do
7
+ it "sets the namespace" do
8
+ sleek = Sleek::Base.new(:my_namespace)
9
+ expect(sleek.namespace).to eq :my_namespace
10
+ end
11
+ end
12
+
13
+ describe "#record" do
14
+ it "creates an event record" do
15
+ data = { name: 'John Doe', email: 'j@d.com' }
16
+
17
+ Sleek::Event.should_receive(:create_with_namespace).with(:default, "signups", data)
18
+
19
+ sleek.record("signups", data)
20
+ end
21
+ end
22
+
23
+ describe "#queries" do
24
+ it "returns QueryCollection for current namespace" do
25
+ Sleek::QueryCollection.should_receive(:new).with(:default).and_call_original
26
+ qc = sleek.queries
27
+ expect(qc.namespace).to eq :default
28
+ end
29
+ end
30
+
31
+ describe "#delete_bucket" do
32
+ it "deletes everything from the bucket" do
33
+ events = stub('events')
34
+ sleek.should_receive(:events).with(:abc).and_return(events)
35
+ events.should_receive(:delete_all)
36
+ sleek.delete_bucket(:abc)
37
+ end
38
+ end
39
+
40
+ describe "#delete_property" do
41
+ it "it deletes the property from all events in bucket" do
42
+ events = stub('events')
43
+ sleek.should_receive(:events).with(:abc).and_return(events)
44
+ events.should_receive(:unset).with("d.test")
45
+ sleek.delete_property(:abc, "test")
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Event do
4
+ describe ".create_with_namespace" do
5
+ it "creates event record" do
6
+ expect { Sleek::Event.create_with_namespace(:default, "signups", { name: 'John Doe', email: 'john@doe.com' }) }.to change { Sleek::Event.count }.by(1)
7
+ end
8
+
9
+ it "sets namespace and bucket" do
10
+ evt = Sleek::Event.create_with_namespace(:default, "signups", {})
11
+ expect(evt.namespace).to eq :default
12
+ expect(evt.bucket).to eq "signups"
13
+ end
14
+
15
+ it "sets the data" do
16
+ data = { name: 'John Doe', email: 'j@d.com' }
17
+ evt = Sleek::Event.create_with_namespace(:ns, "buc", data)
18
+ expect(evt.data).to eq data
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Filter do
4
+ describe "#initialize" do
5
+ context "when valid operator is passed" do
6
+ it "does not raise an exception" do
7
+ expect { Sleek::Filter.new(:test, :eq, 1) }.to_not raise_exception ArgumentError
8
+ end
9
+ end
10
+
11
+ context "when invalid operator is passed" do
12
+ it "raises an exception" do
13
+ expect { Sleek::Filter.new(:test, :lol, 1) }.to raise_exception ArgumentError, "unsupported operator - lol"
14
+ end
15
+ end
16
+ end
17
+
18
+ describe "#apply" do
19
+ it "appleis the filter to the criteria" do
20
+ filter = Sleek::Filter.new(:test, :eq, 1)
21
+ criteria = stub('criteria')
22
+ criteria.should_receive(:eq).with("d.test" => 1)
23
+ filter.apply(criteria)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Interval do
4
+ describe "#initialize" do
5
+ it "transforms interval description into value" do
6
+ Sleek::Interval.should_receive(:interval_value).with(:hourly)
7
+ Sleek::Interval.new(:hourly, Sleek::Timeframe.new(Time.now.all_day))
8
+ end
9
+ end
10
+
11
+ describe "#timeframes" do
12
+ it "slices timeframe into interval-long timeframes" do
13
+ bd = Time.now.beginning_of_day
14
+ interval = Sleek::Interval.new(:hourly, Sleek::Timeframe.new(Time.now.all_day))
15
+ expect(interval.timeframes.count).to eq 23
16
+
17
+ 23.times do |i|
18
+ subtf = interval.timeframes[i]
19
+ expect(subtf.start).to eq bd + 1.hour * i
20
+ expect(subtf.end).to eq bd + 1.hour * (i + 1)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Queries::Average do
4
+ subject(:query) { Sleek::Queries::Average.new(:default, :purchases, target_property: "total") }
5
+
6
+ describe "#perform" do
7
+ it "counts the events" do
8
+ events = stub('events')
9
+ events.should_receive(:avg).with("d.total").and_return(49_35)
10
+ expect(query.perform(events)).to eq 49_35
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Queries::Count do
4
+ subject(:query) { Sleek::Queries::Count.new(:default, :purchases) }
5
+
6
+ describe "#perform" do
7
+ it "counts the events" do
8
+ events = stub('events')
9
+ events.should_receive(:count).and_return(42)
10
+ expect(query.perform(events)).to eq 42
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Queries::CountUnique do
4
+ subject(:query) { Sleek::Queries::CountUnique.new(:default, :purchases, target_property: "customer.id") }
5
+
6
+ describe "#perform" do
7
+ it "counts the events" do
8
+ events = stub('events')
9
+ distinct_events = stub('distinct_events')
10
+ events.should_receive(:distinct).with("d.customer.id").and_return(distinct_events)
11
+ distinct_events.should_receive(:count).and_return(4)
12
+ expect(query.perform(events)).to eq 4
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Queries::Maximum do
4
+ subject(:query) { Sleek::Queries::Maximum.new(:default, :purchases, target_property: "total") }
5
+
6
+ describe "#perform" do
7
+ it "counts the events" do
8
+ events = stub('events')
9
+ events.should_receive(:max).with("d.total").and_return(199_99)
10
+ expect(query.perform(events)).to eq 199_99
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Queries::Minimum do
4
+ subject(:query) { Sleek::Queries::Minimum.new(:default, :purchases, target_property: "total") }
5
+
6
+ describe "#perform" do
7
+ it "counts the events" do
8
+ events = stub('events')
9
+ events.should_receive(:min).with("d.total").and_return(19_99)
10
+ expect(query.perform(events)).to eq 19_99
11
+ end
12
+ end
13
+ end