File Coverage

blib/lib/WebService/Async/Segment.pm
Criterion Covered Total %
statement 95 95 100.0
branch 18 20 90.0
condition 11 16 68.7
subroutine 25 25 100.0
pod 7 7 100.0
total 156 163 95.7


line stmt bran cond sub pod time code
1             package WebService::Async::Segment;
2              
3 2     2   215048 use strict;
  2         18  
  2         58  
4 2     2   10 use warnings;
  2         5  
  2         49  
5              
6 2     2   1106 use Net::Async::HTTP;
  2         261940  
  2         103  
7 2     2   21 use IO::Async::Loop;
  2         4  
  2         52  
8 2     2   11 use Scalar::Util qw(blessed);
  2         11  
  2         86  
9 2     2   11 use URI;
  2         5  
  2         52  
10 2     2   468 use JSON::MaybeUTF8 qw(encode_json_utf8 decode_json_utf8);
  2         9146  
  2         102  
11 2     2   979 use Syntax::Keyword::Try;
  2         3978  
  2         11  
12 2     2   1036 use Log::Any qw($log);
  2         18383  
  2         9  
13 2     2   4680 use Time::Moment;
  2         1464  
  2         61  
14              
15 2     2   12 use parent qw(IO::Async::Notifier);
  2         5  
  2         13  
16              
17 2     2   1285 use WebService::Async::Segment::Customer;
  2         5  
  2         68  
18              
19 2     2   14 use constant SEGMENT_BASE_URL => 'https://api.segment.io/v1/';
  2         4  
  2         105  
20 2     2   12 use constant TIMEOUT => 5;
  2         4  
  2         143  
21 2         2071 use constant SNAKE_FIELDS => {
22             anonymous_id => 'anonymousId',
23             user_id => 'userId',
24             sent_at => 'sentAt',
25             traits => {
26             created_at => 'createdAt',
27             first_name => 'firstName',
28             last_name => 'lastName',
29             address => {
30             postal_code => 'postalCode',
31             },
32             },
33             context => {
34             user_agent => 'userAgent',
35             group_id => 'groupId',
36             device => {
37             advertising_id => 'advertisingId',
38             ad_tracking_enabled => 'adTrackingEnabled'
39             },
40             },
41 2     2   11 };
  2         3  
42              
43             our $VERSION = '0.001';
44              
45             =head1 NAME
46              
47             WebService::Async::Segment - Unofficial support for the Segment service
48              
49             =head1 DESCRIPTION
50              
51             This class acts as a L-based async Perl wrapper for segment HTTP API.
52              
53             =cut
54              
55             =head1 METHODS
56              
57             =head2 configure
58              
59             Overrides the same method of the parent class L; required for object initialization.
60              
61             parameters:
62              
63             =over 4
64              
65             =item * C - the API token of a Segment source.
66              
67             =item * C - the base uri of the Segment host, primarily useful for setting up test mock servers.
68              
69             =back
70              
71             =cut
72              
73             sub configure {
74 2     2 1 693 my ($self, %args) = @_;
75              
76 2         7 for my $k (qw(write_key base_uri)) {
77 4 50       22 $self->{$k} = delete $args{$k} if exists $args{$k};
78             }
79              
80 2         14 $self->next::method(%args);
81             }
82              
83             =head2 write_key
84              
85             API token of the intended Segment source
86              
87             =cut
88              
89 14     14 1 111 sub write_key { shift->{write_key} }
90              
91             =head2 base_uri
92              
93             Server endpoint. Defaults to C<< https://api.segment.io/v1/ >>.
94              
95             Returns a L instance.
96              
97             =cut
98              
99             sub base_uri {
100 14     14 1 27 my $self = shift;
101 14 100       114 return $self->{base_uri} if blessed($self->{base_uri});
102 2   50     24 $self->{base_uri} = URI->new($self->{base_uri} // SEGMENT_BASE_URL);
103 2         16066 return $self->{base_uri};
104             }
105              
106             =head2 ua
107              
108             A L object acting as HTTP user agent
109              
110             =cut
111              
112             sub ua {
113 14     14 1 25 my ($self) = @_;
114              
115 14 100       49 return $self->{ua} if $self->{ua};
116              
117 2         19 $self->{ua} = Net::Async::HTTP->new(
118             fail_on_error => 1,
119             decode_content => 1,
120             pipeline => 0,
121             stall_timeout => TIMEOUT,
122             max_connections_per_host => 2,
123             user_agent => 'Mozilla/4.0 (WebService::Async::Segment; DERIV@cpan.org; https://metacpan.org/pod/WebService::Async::Segment)',
124             );
125              
126 2         308 $self->add_child($self->{ua});
127              
128 2         228 return $self->{ua};
129             }
130              
131             =head2 basic_authentication
132              
133             Settings required for basic HTTP authentication
134              
135             =cut
136              
137             sub basic_authentication {
138 14     14 1 29 my $self = shift;
139              
140             #C basic authentication information
141             return {
142 14   50     33 user => $self->write_key // '',
143             pass => ''
144             };
145             }
146              
147             =head2 method_call
148              
149             Makes a Segment method call. It automatically defaults C to the current time and C<< context->{library} >> to the current module.
150              
151             It takes the following named parameters:
152              
153             =over 4
154              
155             =item * C - required. Segment method name (such as B and B).
156              
157             =item * C - optional. Method arguments represented as a dictionary. This may include either common, method-specific or custom fields.
158              
159             =back
160              
161             Please refer to L for a full list of common fields supported by Segment.
162              
163             It returns a L object.
164              
165             =cut
166              
167             sub method_call {
168 18     18 1 19909 my ($self, $method, %args) = @_;
169              
170 18   66     273 $args{sent_at} ||= Time::Moment->now_utc->to_string();
171 18         85 $args{context}->{library}->{name} = ref $self;
172 18         45 $args{context}->{library}->{version} = $VERSION;
173              
174 18 100       57 return Future->fail('ValidationError', 'segment', 'Method name is missing', 'segment', $method, %args) unless $method;
175              
176             return Future->fail('ValidationError', 'segment', 'Both user_id and anonymous_id are missing', $method, %args)
177 17 100 100     96 unless $args{user_id} or $args{anonymous_id};
178              
179 14         37 %args = _snake_case_to_camelCase(\%args, SNAKE_FIELDS)->%*;
180              
181 14         95 $log->tracef('Segment method %s called with params %s', $method, \%args);
182              
183             return $self->ua->POST(
184             URI->new_abs($method, $self->base_uri),
185             encode_json_utf8(\%args),
186             content_type => 'application/json',
187 14         4706 %{$self->basic_authentication},
188             )->then(
189             sub {
190 13     13   2552 my $result = shift;
191              
192 13         50 $log->tracef('Segment response for %s method received: %s', $method, $result);
193              
194 13         105 my $response_str = $result->content;
195              
196 13 100       922 if ($result->code == 200) {
197 12         700 $log->tracef('Segment %s method call finished successfully.', $method);
198              
199 12         56 return Future->done(1);
200             }
201              
202 1         71 return Future->fail('RequestFailed', 'segment', $response_str);
203             }
204             )->on_fail(
205             sub {
206 2     2   156 $log->errorf('Segment method %s call failed: %s', $method, $_[0]);
207 14         71 })->retain;
208             }
209              
210             =head2 new_customer
211              
212             Creates a new C object as the starting point of making B and B calls.
213             It may takes the following named standard arguments to populate the customer onject with:
214              
215             =over 4
216              
217             =item * C or C - Unique identifier of a user.
218              
219             =item * C or C- A pseudo-unique substitute for a User ID, for cases when you don't have an absolutely unique identifier.
220              
221             =item * C - Free-form dictionary of traits of the user, like email or name.
222              
223             =back
224              
225             =cut
226              
227             sub new_customer {
228 4     4 1 2915 my ($self, %args) = @_;
229              
230 4         20 $args{api_client} = $self;
231              
232 4         21 $log->tracef('A new customer is being created with: %s', \%args);
233              
234 4         33 return WebService::Async::Segment::Customer->new(%args);
235             }
236              
237             =head2 _snake_case_to_camelCase
238              
239             Creates a deep copy of API call args, replacing the standard snake_case keys with equivalent camelCases, necessary to keep consistent with Segment HTTP API.
240             It doesn't automatically alter any non-standard custom keys even they are snake_case.
241              
242             =over 4
243              
244             =item * C<$args> - call args as a hash reference.
245              
246             =item * C<$snake_fields> - a hash ref representing mapping from snake_case to camelCase.
247              
248             =back
249              
250             Returns a hash reference of args with altered keys.
251              
252             =cut
253              
254             sub _snake_case_to_camelCase {
255 134     134   308 my ($args, $snake_fields) = @_;
256              
257 134 100       412 return $args unless ref($args) eq 'HASH';
258 47 100       119 $snake_fields = {} unless ref($snake_fields) eq 'HASH';
259              
260 47         70 my $result;
261 47         127 for my $key (keys %$args) {
262 120 50       230 next unless defined $args->{$key};
263              
264 120 100 100     333 if ($snake_fields->{$key} and not(ref $snake_fields->{$key})) {
265 40         71 my $camel = $snake_fields->{$key};
266 40   33     183 $result->{$camel} = _snake_case_to_camelCase($args->{$camel} // $args->{$key}, $snake_fields->{$camel});
267 40         93 next;
268             }
269 80         182 $result->{$key} = _snake_case_to_camelCase($args->{$key}, $snake_fields->{$key});
270             }
271 47         161 return $result;
272             }
273              
274             1;
275              
276             __END__