File Coverage

blib/lib/Net/GrowthForecast.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Net::GrowthForecast;
2              
3 1     1   2760 use strict;
  1         3  
  1         38  
4 1     1   6 use warnings;
  1         2  
  1         31  
5 1     1   6 use Carp;
  1         11  
  1         77  
6              
7 1     1   883 use List::MoreUtils qw//;
  1         3260  
  1         30  
8              
9 1     1   1037 use Furl;
  0            
  0            
10             use JSON::XS;
11              
12             use Try::Tiny;
13              
14             our $VERSION = '0.02';
15              
16             #TODO: basic authentication support
17              
18             sub new {
19             my ($this, %opts) = @_;
20             my $prefix = $opts{prefix} || '/';
21             $prefix = '/' . $prefix unless $prefix =~ m!^/!;
22             $prefix =~ s!/$!!;
23              
24             my $self = +{
25             host => $opts{host} || 'localhost',
26             port => $opts{port} || 5125,
27             prefix => $prefix,
28             timeout => $opts{timeout} || 30,
29             useragent => 'Net::GrowthForecast',
30             };
31             $self->{furl} = Furl::HTTP->new(agent => $self->{useragent}, timeout => $self->{timeout}, max_redirects => 0);
32             $self->{debug} = $opts{debug} || 0;
33              
34             bless $self, $this;
35             $self;
36             }
37              
38             sub _url {
39             my ($self, $path) = @_;
40             my $base = 'http://' . $self->{host} . ($self->{port} == 80 ? '' : ':' . $self->{port}) . $self->{prefix};
41             $path ||= '/';
42             $base . $path;
43             }
44              
45             sub _request {
46             my ($self, $method, $path, $headers, $content) = @_;
47             my $url = $self->_url($path);
48             my @res;
49             my $list = undef;
50             if ($method eq 'GET') {
51             @res = $self->{furl}->get( $url, $headers || [], $content );
52             } elsif ($method eq 'GET_LIST') {
53             @res = $self->{furl}->get( $url, $headers || [], $content );
54             $list = 1;
55             } elsif ($method eq 'POST') {
56             @res = $self->{furl}->post( $url, $headers || [], $content );
57             } else {
58             die "not implemented here.";
59             }
60             # returns a protocol minor version, status code, status message, response headers, response body
61             my ($protocol_ver, $code, $message, $h, $c) = @res;
62             $self->_check_response($url, $method, $code, $c, $list);
63             }
64              
65             sub _check_response {
66             # check response body with "$c->render_json({ error => 1 , message => '...' })" style error status
67             my ($self, $url, $method, $code, $content, $list_flag) = @_;
68             return [] if $list_flag and $code eq '404';
69             if ($code ne '200') {
70             # TODO fix GrowthForecast::Web not to return 500 when graph not found (or other case...)
71             if ($self->{debug}) {
72             carp "GrowthForecast returns response code $code";
73             carp " request ($method) $url";
74             carp " with content $content";
75             }
76             return undef;
77             }
78             return 1 unless $content;
79             my $error;
80             my $obj;
81             try {
82             $obj = decode_json($content);
83             if (defined($obj) and ref($obj) eq 'ARRAY') {
84             return $obj;
85             } elsif (defined($obj) and $obj->{error}) {
86             warn "request ended with error:";
87             foreach my $k (keys %{$obj->{messages}}) {
88             warn " $k: " . $obj->{messages}->{$k};
89             }
90             warn " request(" . $method . "):" . $url;
91             warn " request body:" . $content;
92             $error = 1;
93             }
94             } catch { # failed to parse json
95             warn "failed to parse response content as json, with error: $_";
96             warn " content:" . $content;
97             $error = 1;
98             };
99             return undef if $error;
100             return $obj if ref($obj) eq 'ARRAY';
101             if (defined $obj->{error}) {
102             return $obj->{data} if $obj->{data};
103             return 1;
104             }
105             $obj;
106             }
107              
108             sub post { # options are 'mode' and 'color' available
109             my ($self, $service, $section, $name, $value, %options) = @_;
110             $self->_request('POST', "/api/$service/$section/$name", [], [ number => $value, %options ] );
111             }
112              
113             sub by_name {
114             my ($self, $service, $section, $name) = @_;
115             my $tree = $self->tree();
116             (($tree->{$service} || {})->{$section} || {})->{$name};
117             }
118              
119             sub graph {
120             my ($self, $id) = @_;
121             if (ref($id) and ref($id) eq 'Hash' and defined $id->{id}) {
122             $id = $id->{id};
123             }
124             $self->_request('GET', "/json/graph/$id");
125             }
126              
127             sub complex {
128             my ($self, $id) = @_;
129             if (ref($id) and ref($id) eq 'Hash' and defined $id->{id}) {
130             $id = $id->{id};
131             }
132             $self->_request('GET', "/json/complex/$id");
133             }
134              
135             sub graphs {
136             my ($self) = @_;
137             $self->_request('GET_LIST', "/json/list/graph");
138             }
139              
140             sub complexes {
141             my ($self) = @_;
142             $self->_request('GET_LIST', "/json/list/complex");
143             }
144              
145             sub all {
146             my ($self) = @_;
147             $self->_request('GET_LIST', "/json/list/all");
148             }
149              
150             sub tree {
151             my ($self) = @_;
152             my $services = {};
153             my $all = $self->all();
154             foreach my $node (@$all) {
155             $services->{$node->{service_name}} ||= {};
156             $services->{$node->{service_name}}->{$node->{section_name}} ||= {};
157             $services->{$node->{service_name}}->{$node->{section_name}}->{$node->{graph_name}} = $node;
158             }
159             $services;
160             }
161              
162             sub edit {
163             my ($self, $spec) = @_;
164             unless (defined $spec->{id}) {
165             croak "cannot edit graph without id (get graph data from GrowthForecast at first)";
166             }
167             my $path;
168             if (defined $spec->{complex} and $spec->{complex}) {
169             $path = "/json/edit/complex/" . $spec->{id};
170             } else {
171             $path = "/json/edit/graph/" . $spec->{id};
172             }
173             $self->_request('POST', $path, [], encode_json($spec));
174             }
175              
176             sub delete {
177             my ($self, $spec) = @_;
178             unless (defined $spec->{id}) {
179             croak "cannot delete graph without id (get graph data from GrowthForecast at first)";
180             }
181             my $path;
182             if (defined $spec->{complex} and $spec->{complex}) {
183             $path = "/delete_complex/" . $spec->{id};
184             } else {
185             $path = join('/', "/delete", $spec->{service_name}, $spec->{section_name}, $spec->{graph_name});
186             }
187             $self->_request('POST', $path);
188             }
189              
190             my @ADDITIONAL_PARAMS = qw(description sort gmode ulimit llimit sulimit sllimit type stype adjust adjustval unit);
191             sub add {
192             my ($self, $spec) = @_;
193             if (defined $spec->{complex} and $spec->{complex}) {
194             return $self->_add_complex($spec);
195             }
196             if (List::MoreUtils::any { defined $spec->{$_} } @ADDITIONAL_PARAMS) {
197             carp "cannot specify additional parameters for basic graph creation (except for 'mode' and 'color')";
198             }
199             $self->add_graph($spec->{service_name}, $spec->{section_name}, $spec->{graph_name}, $spec->{number}, $spec->{color}, $spec->{mode});
200             }
201              
202             sub add_graph {
203             my ($self, $service, $section, $graph_name, $initial_value, $color, $mode) = @_;
204             unless (List::MoreUtils::all { defined($_) and length($_) > 0 } $service, $section, $graph_name) {
205             croak "service, section, graph_name must be specified";
206             }
207             $initial_value = 0 unless defined $initial_value;
208             my %options = ();
209             if (defined $color) {
210             croak "color must be specified like #FFFFFF" unless $color =~ m!^#[0-9a-fA-F]{6}!;
211             $options{color} = $color;
212             }
213             if (defined $mode) {
214             $options{mode} = $mode;
215             }
216             $self->post($service, $section, $graph_name, $initial_value, %options)
217             and 1;
218             }
219              
220             sub add_complex {
221             my ($self, $service, $section, $graph_name, $description, $sumup, $sort, $type, $gmode, $stack, @data_graph_ids) = @_;
222             unless ( List::MoreUtils::all { defined($_) } ($service,$section,$graph_name,$description,$sumup,$sort,$type,$gmode,$stack)
223             and scalar(@data_graph_ids) > 0 ) {
224             croak "all arguments must be specified, but missing";
225             }
226             croak "sort must be 0..19" unless $sort >= 0 and $sort <= 19;
227             croak "type must be one of AREA/LINE1/LINE2, but '$type'" unless $type eq 'AREA' or $type eq 'LINE1' or $type eq 'LINE2';
228             croak "gmode must be one of gauge/subtract" unless $gmode eq 'gauge' or $gmode eq 'subtract';
229             my $spec = +{
230             complex => JSON::XS::true,
231             service_name => $service,
232             section_name => $section,
233             graph_name => $graph_name,
234             description => $description,
235             sumup => ($sumup ? JSON::XS::true : JSON::XS::false),
236             sort => int($sort),
237             data => [ map { +{ graph_id => $_, type => $type, gmode => $gmode, stack => $stack } } @data_graph_ids ],
238             };
239             $self->_add_complex($spec);
240             }
241              
242             sub _add_complex { # used from add_complex() and also from add() directly (with spec format argument)
243             my ($self, $spec) = @_;
244             $self->_request('POST', "/json/create/complex", [], encode_json($spec) );
245             }
246              
247             sub debug {
248             my ($self, $mode) = @_;
249             if (scalar(@_) == 2) {
250             $self->{debug} = $mode ? 1 : 0;
251             return;
252             }
253             # To use like this; $gf->debug->add(...)
254             Net::GrowthForecast->new(%$self, debug => 1);
255             }
256              
257             1;
258             __END__