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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +1 -0
- data/.rubocop.yml +48 -0
- data/.travis.yml +6 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +164 -0
- data/Rakefile +6 -0
- data/bin/console +8 -0
- data/bin/setup +8 -0
- data/clever_tap.gemspec +33 -0
- data/lib/clever_tap.rb +79 -0
- data/lib/clever_tap/client.rb +113 -0
- data/lib/clever_tap/config.rb +25 -0
- data/lib/clever_tap/entity.rb +87 -0
- data/lib/clever_tap/event.rb +30 -0
- data/lib/clever_tap/failed_response.rb +28 -0
- data/lib/clever_tap/profile.rb +7 -0
- data/lib/clever_tap/response.rb +28 -0
- data/lib/clever_tap/successful_response.rb +30 -0
- data/lib/clever_tap/uploader.rb +72 -0
- data/lib/clever_tap/version.rb +3 -0
- data/lib/clevertap-ruby.rb +1 -0
- data/spec/factories/profile.rb +36 -0
- data/spec/integrations/clever_tap_spec.rb +81 -0
- data/spec/rubocop_spec.rb +12 -0
- data/spec/shared/clever_tap_client.rb +13 -0
- data/spec/shared/entity.rb +105 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/units/clever_tap_client_spec.rb +279 -0
- data/spec/units/clever_tap_spec.rb +88 -0
- data/spec/units/event_spec.rb +43 -0
- data/spec/units/failed_response_spec.rb +31 -0
- data/spec/units/profile_spec.rb +29 -0
- data/spec/units/response_spec.rb +48 -0
- data/spec/units/successful_response_spec.rb +112 -0
- data/spec/units/uploader_spec.rb +129 -0
- data/spec/vcr_cassettes/CleverTap/uploading_a_many_profiles/when_only_some_are_valid/partially_succeds.yml +42 -0
- data/spec/vcr_cassettes/CleverTap/uploading_a_profile/when_is_invalid/fails.yml +41 -0
- data/spec/vcr_cassettes/CleverTap/uploading_a_profile/when_is_valid/succeed.yml +35 -0
- data/spec/vcr_cassettes/CleverTap/uploading_an_event/when_is_valid/succeed.yml +34 -0
- data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_invalid_records/calls_on_failed_upload_once.yml +38 -0
- 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
- 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
- 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
- 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
- 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
- data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_age_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
- data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_education_status_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +49 -0
- data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_email_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
- data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_employment_status_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
- data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_marital_status_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
- data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_phone_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
- 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
- data/spec/vcr_cassettes/CleverTap_Uploader/_call/with_invalid_credentials/failed_to_upload_the_profiles.yml +36 -0
- data/spec/vcr_cassettes/CleverTap_Uploader/_call/with_valid_data/makes_successful_upload.yml +36 -0
- data/spec/vcr_config.rb +13 -0
- 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,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 @@
|
|
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
|