File Coverage

blib/lib/SMS/Send/Adapter/Node/Red.pm
Criterion Covered Total %
statement 65 107 60.7
branch 19 54 35.1
condition 2 15 13.3
subroutine 14 18 77.7
pod 11 11 100.0
total 111 205 54.1


line stmt bran cond sub pod time code
1             package SMS::Send::Adapter::Node::Red;
2 1     1   90736 use strict;
  1         2  
  1         35  
3 1     1   5 use warnings;
  1         2  
  1         36  
4 1     1   6 use base qw{Package::New};
  1         2  
  1         568  
5 1     1   267 use JSON qw{decode_json encode_json};
  1         2  
  1         11  
6 1     1   666 use SMS::Send;
  1         24549  
  1         40  
7 1     1   1240 use CGI;
  1         32173  
  1         10  
8              
9             our $VERSION = '0.07';
10             our $PACKAGE = __PACKAGE__;
11              
12             =head1 NAME
13              
14             SMS::Send::Adapter::Node::Red - SMS::Send Adapter to Node-RED JSON HTTP request
15              
16             =head1 SYNOPSIS
17              
18             CGI Application
19              
20             use SMS::Send::Adapter::Node::Red;
21             my $service = SMS::Send::Adapter::Node::Red->new(content => join('', <>));
22             $service->cgi_response;
23              
24             PSGI Application
25              
26             use SMS::Send::Adapter::Node::Red;
27             SMS::Send::Adapter::Node::Red->psgi_app
28              
29             PSGI Plack Mount
30              
31             use SMS::Send::Adapter::Node::Red;
32             use Plack::Builder qw{builder mount};
33             builder {
34             mount '/sms' => SMS::Send::Adapter::Node::Red->psgi_app;
35             mount '/' => sub {[404=> [], []]};
36             }
37              
38             =head1 DESCRIPTION
39              
40             This Perl package provides an adapter from Node-RED HTTP request object with a JSON payload to the SMS::Send infrastructure using either a PSGI or a CGI script. The architecture works easiest with SMS::Send drivers based on the L base object since common settings can be stored in the configuration file.
41              
42             =head1 CONSTRUCTOR
43              
44             =head2 new
45              
46             my $object = SMS::Send::Adapter::Node::Red->new(content=>$string_of_json_object);
47              
48             =head1 PROPERTIES
49              
50             =head2 content
51              
52             JSON string payload of the HTTP post request.
53              
54             Example Payload:
55              
56             {
57             "to" : "7035551212",
58             "text" : "My Text Message",
59             "driver" : "VoIP::MS",
60             "options" : {}
61             }
62              
63             The Perl logic is based on this one-liner with lots of error trapping
64              
65             my $sent = SMS::Send->new($driver, %$options)->send_sms(to=>$to, text=>$text);
66              
67             I use a Node-RED function like this to format the JSON payload.
68              
69             my_text = msg.payload;
70             msg.payload = {
71             "driver" : "VoIP::MS",
72             "text" : my_text,
73             "to" : "7035551212",
74             "options" : {"key" : "value"},
75             };
76             return msg;
77              
78             =cut
79              
80             sub content {
81 4     4 1 8 my $self = shift;
82 4 50       17 die("Error: content not defined on construction") unless defined $self->{'content'};
83 4         52 return $self->{'content'};
84             }
85              
86             =head1 METHODS (STATE)
87              
88             =head2 input
89              
90             JSON Object from input that is passed to output.
91              
92             =cut
93              
94             sub input {
95 12     12 1 7677 my $self = shift;
96 12 100       91 if (not defined $self->{'input'}) {
97 4         10 local $@;
98 4         10 my $input = eval{decode_json($self->content)};
  4         22  
99 4         9 my $error = $@;
100 4 100       20 if ($error) {
    100          
101 1         6 $self->set_status_error(400=>'Error: JSON decode failed');
102             } elsif (ref($input) ne 'HASH') {
103 1         6 $self->set_status_error(400=>'Error: JSON Object required');
104             } else {
105 2         8 $self->{'input'} = $input;
106             }
107             }
108 12         89 return $self->{'input'};
109             }
110              
111             =head2 status
112              
113             HTTP Status Code returned to Node-RED is one of 200, 400, 500 or 502. Typically, a 200 means the SMS message was successfully sent to the provider, a 400 means the input is malformed, a 500 means the server is misconfigured (verify installation), and a 502 means that the remote service has issues or is unreachable.
114              
115             =cut
116              
117             sub status {
118 9     9 1 18 my $self = shift;
119 9 100       30 $self->{'status'} = shift if @_;
120 9 50       25 die("Error: status not set. sms_send method must be called first") unless $self->{'status'};
121 9         30 return $self->{'status'};
122             }
123              
124             =head2 status_string
125              
126             Format HTTP Status Code as string for web response
127              
128             =cut
129              
130             our $STATUS_STRING = {
131             200 => 'OK',
132             400 => 'Bad Request',
133             500 => 'Internal Server Error',
134             502 => 'Bad Gateway',
135             };
136              
137             sub status_string {
138 3     3 1 8 my $self = shift;
139 3         11 my $status = $self->status;
140 3 50       18 my $status_string = $STATUS_STRING->{$status} or die("Error: STATUS_STRING not defined for $status");
141 3         17 return "$status $status_string";
142             }
143              
144             =head2 error
145              
146             Error string passed in the JSON return object.
147              
148             =cut
149              
150             sub error {
151 7     7 1 16 my $self = shift;
152 7 100       27 $self->{'error'} = shift if @_;
153 7         30 return $self->{'error'};
154             }
155              
156             =head2 set_status_error
157              
158             Method to set the HTTP status and error with one function call.
159              
160             =cut
161              
162             sub set_status_error {
163 3     3 1 8 my $self = shift;
164 3 50       13 my $status = shift or die;
165 3   50     9 my $error = shift || '';
166 3         14 $self->status($status);
167 3         16 $self->error($error);
168 3         9 return $self;
169             }
170              
171             =head1 METHODS (ACTIONS)
172              
173             =head2 send_sms
174              
175             Wrapper around the SMS::Send->send_sms call.
176              
177             =cut
178              
179             sub send_sms {
180 0     0 1 0 my $self = shift;
181 0         0 my $sent = 0;
182 0         0 my $SMS = $self->SMS;
183 0 0       0 if ($SMS) {
184 0         0 my $to = $self->input->{'to'};
185 0         0 my $text = $self->input->{'text'};
186 0 0 0     0 if ($to and $text) {
    0 0        
    0 0        
187 0         0 local $@;
188 0         0 $sent = eval{$SMS->send_sms(to=>$to, text=>$text)};
  0         0  
189 0         0 my $error = $@;
190 0 0       0 if ($error) {
    0          
191 0         0 $self->set_status_error(502=>"Error: Failed call SMS::Send->send_sms. $error");
192             } elsif (!$sent) {
193 0         0 $self->set_status_error(502=>'Error: Unknown. SMS::Send->send_sms returned unsuccessful');
194             } else {
195 0         0 $self->set_status_error(200=>'');
196             }
197             } elsif (!$to and $text) {
198 0         0 $self->set_status_error(400=>'Error: JSON input missing "to"');
199             } elsif ($to and !$text) {
200 0         0 $self->set_status_error(400=>'Error: JSON input missing "text"');
201             } else {
202 0         0 $self->set_status_error(400=>'Error: JSON input missing "to" and "text"');
203             }
204             }
205 0         0 return $sent;
206             }
207              
208             =head2 cgi_response
209              
210             Formatted CGI response
211              
212             =cut
213              
214             sub cgi_response {
215 0     0 1 0 my $self = shift;
216 0 0       0 my $sent = $self->send_sms ? \1 : \0; #sets object properties
217 0         0 my %response = (sent => $sent);
218 0 0       0 $response{'error'} = $self->error if $self->error;
219 0 0       0 $response{'input'} = $self->input if $self->input;
220 0         0 print $self->CGI->header(
221             -status => $self->status_string,
222             -type => 'application/json',
223             ),
224             encode_json(\%response),
225             "\n";
226             }
227              
228             =head2 psgi_app
229              
230             Returns a PSGI application
231              
232             =cut
233              
234             sub psgi_app {
235             return sub {
236 0     0   0 my $env = shift;
237 0   0     0 my $length = $env->{'CONTENT_LENGTH'} || 0;
238 0         0 my $content = '';
239 0 0       0 if ($length > 0) {
240 0         0 my $fh = $env->{'psgi.input'};
241 0         0 $fh->read($content, $length, 0);
242             }
243 0         0 my $service = $PACKAGE->new(content => $content);
244 0 0       0 my $sent = $service->send_sms ? \1 : \0; #sets object properties
245 0         0 my %response = (sent => $sent);
246 0 0       0 $response{'error'} = $service->error if $service->error;
247 0 0       0 $response{'input'} = $service->input if $service->input;
248              
249             return [
250 0         0 $service->status,
251             [ 'Content-Type' => 'application/json' ],
252             [ encode_json(\%response), "\n" ],
253             ];
254 1     1 1 343 };
255             }
256              
257             =head1 OBJECT ACCESSORS
258              
259             =head2 CGI
260              
261             Returns a L object for use in this package.
262              
263             =cut
264              
265             sub CGI {
266 0     0 1 0 my $self = shift;
267 0 0       0 $self->{'CGI'} = CGI->new('') unless $self->{'CGI'};
268 0         0 return $self->{'CGI'};
269             }
270              
271             =head2 SMS
272              
273             Returns a L object for use in this package.
274              
275             =cut
276              
277             sub SMS {
278 2     2 1 5 my $self = shift;
279 2         9 my $input = $self->input; #undef on error
280 2 50       12 if (defined $input) {
281 2         7 my $driver = $input->{'driver'};
282 2 50       7 if ($driver) {
283 2   50     13 my $options = $input->{'options'} || {};
284 2 50       8 if (ref($options) eq 'HASH') {
285 2         5 local $@;
286 2         5 $self->{'SMS'} = eval{SMS::Send->new($driver, %$options)};
  2         20  
287 2         1043 my $error = $@;
288 2 100       11 if ($error) {
289 1         5 my $text = qq{Failed to load Perl package SMS::Send with driver "$driver". Please ensure both SMS::Send and SMS::Send::$driver are installed. $error};
290 1         5 $self->set_status_error(500=>$text);
291             }
292             } else {
293 0         0 $self->set_status_error(400=>'Error: JSON input "options" not an object.');
294             }
295             } else {
296 0         0 $self->set_status_error(400=>'Error: JSON input missing "driver".');
297             }
298             }
299 2         13 return $self->{'SMS'};
300             }
301              
302             =head1 SEE ALSO
303              
304             L, L, L
305              
306             =head1 AUTHOR
307              
308             Michael R. Davis
309              
310             =head1 COPYRIGHT AND LICENSE
311              
312             MIT License
313              
314             Copyright (c) 2020 Michael R. Davis
315              
316             Permission is hereby granted, free of charge, to any person obtaining a copy
317             of this software and associated documentation files (the "Software"), to deal
318             in the Software without restriction, including without limitation the rights
319             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
320             copies of the Software, and to permit persons to whom the Software is
321             furnished to do so, subject to the following conditions:
322              
323             The above copyright notice and this permission notice shall be included in all
324             copies or substantial portions of the Software.
325              
326             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
327             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
328             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
329             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
330             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
331             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
332             SOFTWARE.
333              
334             =cut
335              
336             1;