clever_tap 0.3.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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +48 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +16 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +164 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +8 -0
  11. data/bin/setup +8 -0
  12. data/clever_tap.gemspec +33 -0
  13. data/lib/clever_tap.rb +79 -0
  14. data/lib/clever_tap/client.rb +113 -0
  15. data/lib/clever_tap/config.rb +25 -0
  16. data/lib/clever_tap/entity.rb +87 -0
  17. data/lib/clever_tap/event.rb +30 -0
  18. data/lib/clever_tap/failed_response.rb +28 -0
  19. data/lib/clever_tap/profile.rb +7 -0
  20. data/lib/clever_tap/response.rb +28 -0
  21. data/lib/clever_tap/successful_response.rb +30 -0
  22. data/lib/clever_tap/uploader.rb +72 -0
  23. data/lib/clever_tap/version.rb +3 -0
  24. data/lib/clevertap-ruby.rb +1 -0
  25. data/spec/factories/profile.rb +36 -0
  26. data/spec/integrations/clever_tap_spec.rb +81 -0
  27. data/spec/rubocop_spec.rb +12 -0
  28. data/spec/shared/clever_tap_client.rb +13 -0
  29. data/spec/shared/entity.rb +105 -0
  30. data/spec/spec_helper.rb +18 -0
  31. data/spec/units/clever_tap_client_spec.rb +279 -0
  32. data/spec/units/clever_tap_spec.rb +88 -0
  33. data/spec/units/event_spec.rb +43 -0
  34. data/spec/units/failed_response_spec.rb +31 -0
  35. data/spec/units/profile_spec.rb +29 -0
  36. data/spec/units/response_spec.rb +48 -0
  37. data/spec/units/successful_response_spec.rb +112 -0
  38. data/spec/units/uploader_spec.rb +129 -0
  39. data/spec/vcr_cassettes/CleverTap/uploading_a_many_profiles/when_only_some_are_valid/partially_succeds.yml +42 -0
  40. data/spec/vcr_cassettes/CleverTap/uploading_a_profile/when_is_invalid/fails.yml +41 -0
  41. data/spec/vcr_cassettes/CleverTap/uploading_a_profile/when_is_valid/succeed.yml +35 -0
  42. data/spec/vcr_cassettes/CleverTap/uploading_an_event/when_is_valid/succeed.yml +34 -0
  43. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_invalid_records/calls_on_failed_upload_once.yml +38 -0
  44. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_invalid_records/returns_an_array_with_one_failed_Response_object.yml +38 -0
  45. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_valid_records/and_objects_do_not_fit_upload_limit_/calls_on_successful_upload_proc_twice.yml +67 -0
  46. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_valid_records/and_objects_do_not_fit_upload_limit_/returns_an_array_with_two_successful_Response_objects.yml +67 -0
  47. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_valid_records/and_objects_fit_upload_limit_/calls_on_successful_upload_proc_once.yml +36 -0
  48. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_valid_records/and_objects_fit_upload_limit_/returns_an_array_with_one_successful_Response_object.yml +36 -0
  49. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_age_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  50. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_education_status_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +49 -0
  51. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_email_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  52. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_employment_status_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  53. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_marital_status_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  54. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_phone_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  55. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_the_creation_date_field_is_missing/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  56. data/spec/vcr_cassettes/CleverTap_Uploader/_call/with_invalid_credentials/failed_to_upload_the_profiles.yml +36 -0
  57. data/spec/vcr_cassettes/CleverTap_Uploader/_call/with_valid_data/makes_successful_upload.yml +36 -0
  58. data/spec/vcr_config.rb +13 -0
  59. metadata +199 -0
@@ -0,0 +1,25 @@
1
+ class CleverTap
2
+ # CleverTap instance's config store object
3
+ class Config
4
+ DEFAULT_IDENTITY_FIELD = 'identity'.freeze
5
+
6
+ attr_accessor :account_id, :passcode, :identity_field
7
+
8
+ def initialize(**config)
9
+ @account_id = config[:account_id]
10
+ @passcode = config[:passcode]
11
+ @identity_field = config[:identity_field] || DEFAULT_IDENTITY_FIELD
12
+ @configure_faraday = config[:configure_faraday]
13
+ end
14
+
15
+ # NOTE: reader or writer depending if the block is given
16
+ def configure_faraday(&block)
17
+ block ? @configure_faraday = block : @configure_faraday
18
+ end
19
+
20
+ def validate
21
+ raise 'Missing authentication parameter `account_id`' unless account_id
22
+ raise 'Missing authentication parameter `passcode`' unless passcode
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,87 @@
1
+ class CleverTap
2
+ class NoDataError < RuntimeError
3
+ def message
4
+ 'No `data` param provided for Event'
5
+ end
6
+ end
7
+
8
+ class MissingIdentityError < RuntimeError
9
+ def message
10
+ "Couldn'n find `identity` in CleverTap.config or `data`"
11
+ end
12
+ end
13
+
14
+ class Entity
15
+ ALLOWED_IDENTITIES = %w(objectId FBID GPID).freeze
16
+ IDENTITY_STRING = 'identity'.freeze
17
+ TIMESTAMP_STRING = 'ts'.freeze
18
+ TYPE_KEY_STRING = 'type'.freeze
19
+ UPLOAD_LIMIT = 'Needs child class value'.freeze
20
+ TYPE_VALUE_STRING = 'Needs child class value'.freeze
21
+
22
+ class << self
23
+ def upload_limit
24
+ self::UPLOAD_LIMIT
25
+ end
26
+
27
+ def all_same_type?(items)
28
+ items.all? { |i| i.class == self }
29
+ end
30
+ end
31
+
32
+ def initialize(**args)
33
+ @data = args[:data]
34
+ @identity = choose_identity(args)
35
+ @timestamp = choose_timestamp(args)
36
+ end
37
+
38
+ def to_h
39
+ put_identity_pair
40
+ .merge(put_timestamp_pair)
41
+ .merge(put_type_pair)
42
+ .merge(put_data)
43
+ end
44
+
45
+ private
46
+
47
+ def put_identity_pair
48
+ raise NoDataError if @data.to_h.empty?
49
+ raise MissingIdentityError if @identity == '' || @data[@identity].nil?
50
+ return { @identity => @data[@identity].to_s } if allowed?(@identity)
51
+ { IDENTITY_STRING => @data[@identity].to_s }
52
+ end
53
+
54
+ def put_timestamp_pair
55
+ return {} unless @timestamp
56
+ { TIMESTAMP_STRING => @timestamp }
57
+ end
58
+
59
+ def put_type_pair
60
+ { TYPE_KEY_STRING => self.class::TYPE_VALUE_STRING }
61
+ end
62
+
63
+ def put_data
64
+ raise NoDataError if @data.to_h.empty?
65
+ @data.delete(@identity) if allowed?(@identity)
66
+ {
67
+ self.class::DATA_STRING => @data
68
+ }
69
+ end
70
+
71
+ def choose_identity(args)
72
+ identity = args[:identity].to_s
73
+
74
+ return identity if allowed?(identity) && @data.to_h.key?(identity)
75
+ CleverTap.identity_field.to_s
76
+ end
77
+
78
+ def choose_timestamp(args)
79
+ return args[:custom_timestamp].to_i if args[:custom_timestamp]
80
+ return @data.delete(args[:timestamp_field].to_s).to_i if args[:timestamp_field]
81
+ end
82
+
83
+ def allowed?(identity)
84
+ ALLOWED_IDENTITIES.include?(identity)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,30 @@
1
+ class CleverTap
2
+ class MissingEventNameError < RuntimeError
3
+ def message
4
+ "Couldn't find `name:` with value in Event#new(options)"
5
+ end
6
+ end
7
+
8
+ class Event < Entity
9
+ DATA_STRING = 'evtData'.freeze
10
+ EVENT_NAME_STRING = 'evtName'.freeze
11
+ TYPE_VALUE_STRING = 'event'.freeze
12
+ UPLOAD_LIMIT = 1000
13
+
14
+ def initialize(**args)
15
+ super(**args)
16
+ @name = args[:name]
17
+ end
18
+
19
+ def to_h
20
+ super.merge(put_event_name_pair)
21
+ end
22
+
23
+ private
24
+
25
+ def put_event_name_pair
26
+ raise MissingEventNameError if @name.nil?
27
+ { EVENT_NAME_STRING => @name }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ class CleverTap
2
+ # Introduce unified interface as the `SuccessfulResponse`
3
+ class FailedResponse
4
+ FAIL_STATUS = 'fail'.freeze
5
+
6
+ attr_reader :records, :message, :code
7
+
8
+ def initialize(records:, message:, code: -1)
9
+ @records = records
10
+ @message = message
11
+ @code = code
12
+ end
13
+
14
+ def status
15
+ FAIL_STATUS
16
+ end
17
+
18
+ def success
19
+ false
20
+ end
21
+
22
+ def errors
23
+ records.map do |record|
24
+ { 'status' => FAIL_STATUS, 'code' => code, 'error' => message, 'record' => record }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,7 @@
1
+ class CleverTap
2
+ class Profile < Entity
3
+ DATA_STRING = 'profileData'.freeze
4
+ TYPE_VALUE_STRING = 'profile'.freeze
5
+ UPLOAD_LIMIT = 100
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ class CleverTap
2
+ class Response
3
+ attr_accessor :response, :success, :failures
4
+
5
+ def initialize(response)
6
+ @response = JSON.parse(response.body)
7
+ process_response
8
+ end
9
+
10
+ private
11
+
12
+ def process_response
13
+ return process_success if response['status'] == 'success'
14
+ @success = false
15
+ @failures = [response]
16
+ end
17
+
18
+ def process_success
19
+ if response['unprocessed'].to_a.empty?
20
+ @success = true
21
+ @failures = []
22
+ else
23
+ @success = false
24
+ @failures = response['unprocessed']
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ class CleverTap
2
+ # Normalize the success response data to one interface with the failure one
3
+ class SuccessfulResponse
4
+ attr_reader :raw_response, :unprocessed, :message, :code
5
+
6
+ # NOTE: raw_response can include processed, unprocessed, status
7
+ def initialize(raw_response = {})
8
+ @raw_response = raw_response
9
+ @unprocessed = raw_response['unprocessed']
10
+ @message = ''
11
+ @code = 200
12
+ end
13
+
14
+ def errors
15
+ unprocessed
16
+ end
17
+
18
+ def status
19
+ case
20
+ when success then 'success'
21
+ when raw_response['processed'].positive? then 'partial'
22
+ else 'fail'
23
+ end
24
+ end
25
+
26
+ def success
27
+ unprocessed.empty?
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,72 @@
1
+ class CleverTap
2
+ # unit uploading profile data to CleverTap
3
+ class Uploader
4
+ HTTP_PATH = 'upload'.freeze
5
+
6
+ TYPE_EVENT = :event
7
+ TYPE_PROFILE = :profile
8
+
9
+ ENTITY_DATA_NAMES = {
10
+ TYPE_EVENT => 'evtData',
11
+ TYPE_PROFILE => 'profileData'
12
+ }.freeze
13
+
14
+ attr_reader :records, :type, :identity_field, :date_field, :event_name, :dry_run
15
+
16
+ # TODO: make defaults configurable
17
+ # date_field should be a date object responding to `to_i` which
18
+ # should returns epoch time
19
+ # profile respond to .to_h
20
+ def initialize(records, identity_field:, date_field: nil, event_name: nil, dry_run: false)
21
+ @type = event_name ? TYPE_EVENT : TYPE_PROFILE
22
+ @records = records
23
+
24
+ @identity_field = identity_field
25
+ @date_field = date_field
26
+ @event_name = event_name
27
+ @dry_run = dry_run
28
+ end
29
+
30
+ def call(client)
31
+ response = client.post(HTTP_PATH, build_request_body) do |request|
32
+ request.params.merge!(dryRun: 1) if dry_run
33
+ end
34
+
35
+ parse_response(response)
36
+ end
37
+
38
+ private
39
+
40
+ def build_request_body
41
+ records.each_with_object('d' => []) do |record, request_body|
42
+ request_body['d'] << normalize_record(record)
43
+ end.to_json
44
+ end
45
+
46
+ def normalize_record(record)
47
+ ts = date_field ? record[date_field] : Time.now
48
+
49
+ {
50
+ 'identity' => pluck_identity(record).to_s,
51
+ 'ts' => ts.to_i,
52
+ 'type' => type,
53
+ ENTITY_DATA_NAMES[type] => record.to_h
54
+ }.tap do |hash|
55
+ hash.merge!('evtName' => event_name) if type == TYPE_EVENT
56
+ end
57
+ end
58
+
59
+ def parse_response(http_response)
60
+ http_response
61
+ end
62
+
63
+ def pluck_identity(record)
64
+ # TODO: symbolize record keys
65
+ identity = record[identity_field.to_s] || record[identity_field.to_sym]
66
+
67
+ raise "Missing identity field with name: '#{identity_field}' for #{record}" unless identity
68
+
69
+ identity
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,3 @@
1
+ class CleverTap
2
+ VERSION = '0.3.0'.freeze
3
+ end
@@ -0,0 +1 @@
1
+ require 'clever_tap'
@@ -0,0 +1,36 @@
1
+ # Unit representing a profile and it's different states
2
+ class Profile
3
+ class << self
4
+ attr_accessor :store
5
+ end
6
+
7
+ self.store = {
8
+ id: 0
9
+ }
10
+
11
+ def self.build_valid(extra = {})
12
+ new({
13
+ 'identity' => store[:id] += 1,
14
+ 'created_at' => Time.new,
15
+ 'Name' => 'John Rush',
16
+ 'Email' => 'example@gmail.com',
17
+ 'Gender' => 'M',
18
+ 'Phone' => '+35922333232',
19
+ 'Employed' => 'Y',
20
+ 'Education' => 'Graduate',
21
+ 'Married' => 'Y',
22
+ 'Age' => '18'
23
+ }.merge(extra))
24
+ end
25
+
26
+ attr_reader :data
27
+ alias to_h data
28
+
29
+ def initialize(data = {})
30
+ @data = data
31
+ end
32
+
33
+ def [](key)
34
+ data[key]
35
+ end
36
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CleverTap, vcr: true do
4
+ # NOTE: clear mutations in CleverTap config
5
+ subject(:clever_tap) { CleverTap.new(account_id: AUTH_ACCOUNT_ID, passcode: AUTH_PASSCODE) }
6
+
7
+ describe 'uploading a profile' do
8
+ context 'when is valid' do
9
+ let(:profile) { Profile.build_valid }
10
+
11
+ it 'succeed' do
12
+ response = clever_tap.upload_profile(profile)
13
+
14
+ aggregate_failures do
15
+ expect(response.status).to eq('success')
16
+ expect(response.errors).to be_empty
17
+ end
18
+ end
19
+ end
20
+
21
+ context 'when is invalid' do
22
+ let(:profile) { Profile.build_valid('Email' => '$$$$$') }
23
+
24
+ it 'fails' do
25
+ response = clever_tap.upload_profile(profile)
26
+
27
+ aggregate_failures do
28
+ expect(response.status).to eq('fail')
29
+ expect(response.errors).to all(be_a(Hash))
30
+ expect(response.errors).to all(
31
+ include('status', 'code', 'error', 'record')
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ describe 'uploading a many profiles' do
39
+ context 'when only some are valid' do
40
+ let(:valid_profile) { Profile.build_valid }
41
+ let(:invalid_profile) { Profile.build_valid('Email' => '$$$$$') }
42
+ let(:profiles) { [valid_profile, invalid_profile] }
43
+
44
+ it 'partially succeds' do
45
+ response = clever_tap.upload_profiles(profiles)
46
+
47
+ aggregate_failures do
48
+ expect(response.status).to eq('partial')
49
+ expect(response.errors).to all(be_a(Hash))
50
+ expect(response.errors).to all(
51
+ include('status', 'code', 'error', 'record')
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ describe 'uploading an event' do
59
+ subject(:clever_tap) do
60
+ CleverTap.new(account_id: AUTH_ACCOUNT_ID, passcode: AUTH_PASSCODE, identity_field: 'ID')
61
+ end
62
+
63
+ context 'when is valid' do
64
+ let(:event) do
65
+ {
66
+ 'ID' => 555,
67
+ 'mobile' => true
68
+ }
69
+ end
70
+
71
+ it 'succeed' do
72
+ response = clever_tap.upload_event(event, name: 'register', identity_field: 'user_id')
73
+
74
+ aggregate_failures do
75
+ expect(response.status).to eq('success')
76
+ expect(response.errors).to be_empty
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubocop'
2
+
3
+ gem_root = File.expand_path('../', __FILE__)
4
+ CONFIG_FILE_PATH = File.join(gem_root, 'config', 'rubocop_spec.yml')
5
+
6
+ describe 'rubocop' do
7
+ let(:args) { ['--format', 'simple', '-D', gem_root] }
8
+
9
+ it 'passes all cops' do
10
+ expect { RuboCop::CLI.new.run(args) }.to output(/no offenses detected/).to_stdout
11
+ end
12
+ end