File Coverage

blib/lib/MikroTik/Client/Query.pm
Criterion Covered Total %
statement 62 62 100.0
branch 44 46 95.6
condition 18 25 72.0
subroutine 10 10 100.0
pod 1 1 100.0
total 135 144 93.7


line stmt bran cond sub pod time code
1             package MikroTik::Client::Query;
2 6     6   55239 use MikroTik::Client::Mo;
  6         36  
  6         29  
3              
4 6     6   35 use Exporter 'import';
  6         10  
  6         139  
5 6     6   28 use Scalar::Util 'blessed';
  6         9  
  6         5188  
6              
7             our @EXPORT_OK = ('build_query');
8              
9              
10             sub build_query {
11 20 50   20 1 8485 my $query = blessed $_[0] ? $_[1] : $_[0];
12              
13 20 100 66     60 return $$query if ref $query eq 'REF' && ref $$query eq 'ARRAY';
14              
15 19 100       37 if (my $type = ref $query) {
16 18         37 return [_block(_ref_op($type), $query)];
17             }
18 1         4 else { return [] }
19             }
20              
21             sub _block {
22 30     30   56 my ($logic, $items) = @_;
23              
24 30 100       85 @{($items = [])} = map { $_ => $items->{$_} } sort keys %$items
  19         38  
  32         66  
25             if ref $items eq 'HASH';
26 30         48 my ($count, @words) = (0, ());
27              
28 30         61 while (my $el = shift @$items) {
29              
30 60         71 my @expr;
31 60 100 66     187 if (ref $el eq 'REF' && ref $$el eq 'ARRAY') {
    100          
    100          
    100          
32 1         1 @expr = @{$$el};
  1         2  
33              
34             }
35             elsif (my $type = ref $el) {
36 5         9 @expr = _block(_ref_op($type), $el);
37              
38             }
39             elsif ($el =~ /^-(?:and|or)$/) {
40 7         14 @expr = _block(_ref_op($el), shift @$items);
41              
42             }
43             elsif ($el =~ /^-has(?:_not)?$/) {
44 2 100       9 push @words, '?' . ($el eq '-has_not' ? '-' : '') . (shift @$items);
45 2         3 $count++;
46 2         5 next;
47              
48             }
49             else {
50 45         67 @expr = _value($el, shift @$items);
51             }
52              
53 58 100 33     243 ++$count && push @words, @expr if @expr;
54             }
55              
56 30 100       76 push @words, '?#' . ($logic x ($count - 1)) if $count > 1;
57 30         115 return @words;
58             }
59              
60             sub _ref_op {
61             return
62 34 50 100 34   120 ($_[0] eq 'HASH' || $_[0] eq '-and') ? '&'
    100 66        
63             : ($_[0] eq 'ARRAY' || $_[0] eq '-or') ? '|'
64             : '';
65             }
66              
67             sub _value {
68 45     45   64 my ($name, $val) = @_;
69              
70 45         56 my $type = ref $val;
71 45 100       77 if ($type eq 'HASH') {
    100          
72 7         13 return _value_hash($name, $val);
73              
74             }
75             elsif ($type eq 'ARRAY') {
76 6         11 return _value_array($name, '=', $val);
77             }
78              
79             # SCALAR
80 32   100     85 return "?$name=" . ($val // '');
81             }
82              
83             sub _value_array {
84 10     10   15 my ($name, $op, $block) = @_;
85              
86 10 100       22 return () unless @$block;
87              
88 8         10 my $logic = '|';
89 8 100 100     31 $logic = _ref_op(shift @$block)
90             if @$block[0] eq '-and' || @$block[0] eq '-or';
91              
92 8         12 my ($count, @words) = (0, ());
93 8         13 for (@$block) {
94             my @expr
95 24 100       49 = ref $_ eq 'HASH'
96             ? _value_hash($name, $_)
97             : _value_scalar($name, $op, $_);
98              
99 24 100 33     66 ++$count && push @words, @expr if @expr;
100             }
101              
102 8 100       29 push @words, '?#' . ($logic x ($count - 1)) if $count > 1;
103 8         30 return @words;
104             }
105              
106             sub _value_hash {
107 12     12   18 my ($name, $block) = @_;
108              
109 12         14 my @words = ();
110              
111 12         30 for my $op (sort keys %$block) {
112 12         18 my $val = $block->{$op};
113 12 100       25 return _value_array($name, $op, $val) if ref $val eq 'ARRAY';
114 8         13 push @words, _value_scalar($name, $op, $val);
115             }
116              
117 8         14 my $count = keys %$block;
118 8 100       15 push @words, '?#' . ('&' x ($count - 1)) if $count > 1;
119 8         18 return @words;
120             }
121              
122             sub _value_scalar {
123 27   100 27   52 my ($name, $op, $val) = (shift, shift, shift // '');
124              
125 27 100       46 return ("?$name=$val", '?#!') if $op eq '-not';
126 23         46 return '?' . $name . $op . $val;
127             }
128              
129             1;
130              
131              
132             =encoding utf8
133              
134             =head1 NAME
135              
136             MikroTik::Client::Query - Build MikroTik queries from perl structures
137              
138             =head1 SYNOPSIS
139              
140             use MikroTik::Client::Query qw(build_query);
141              
142             # (a = 1 OR a = 2) AND (b = 3 OR c = 4 OR d = 5)
143             my $query = {
144             a => [1, 2],
145             [
146             b => 3,
147             c => 4,
148             d => 5
149             ]
150             };
151              
152              
153             # Some bizarre nested expressions.
154             # (a = 1 OR b = 2 OR (e = 5 AND f = 6 AND g = 7))
155             # OR
156             # (c = 3 AND d = 4)
157             # OR
158             # (h = 8 AND i = 9)
159             $query = [
160             -or => {
161             a => 1,
162             b => 2,
163             -and => {e => 5, f => 6, g => 7}
164             },
165              
166             # OR
167             -and => [
168             c => 3,
169             d => 4
170             ],
171              
172             # OR
173             {h => 8, i => 9}
174             ];
175              
176             =head1 DESCRIPTION
177              
178             Simple and supposedly intuitive way to build MikroTik API queries. Following
179             ideas of L.
180              
181             =head1 METHODS
182              
183             =head2 build_query
184              
185             use MikroTik::Client::Query qw(build_query);
186              
187             # (type = 'ipip-tunnel' OR type = 'gre-tunnel') AND running = 'true'
188             # $query
189             # = ['?type=ipip-tunnel', '?type=gre-tunnel', '?#|', '?running=true', '?#&'];
190             my $query
191             = build_query({type => ['ipip-tunnel', 'gre-tunnel'], running => 'true'});
192              
193             Builds a query and returns an arrayref with API query words.
194              
195             =head1 QUERY SYNTAX
196              
197             Basic idea is that everything in arrayrefs are C'ed and everything in hashrefs
198             are C'ed unless specified otherwise. Another thing is, where a C is
199             expected, you should be able to use a list to compare against a set of values.
200              
201             =head2 Key-value pairs
202              
203             # type = 'gre-tunnel' AND running = 'true'
204             my $query = {type => 'gre-tunnel', running => 'true'};
205              
206             # disabled = 'true' OR running = 'false'
207             $query = [disabled => 'true', running => 'false'];
208              
209             Simple attribute value comparison.
210              
211             =head2 List of values
212              
213             # type = 'ether' OR type = 'wlan'
214             my $query = {type => ['ether', 'wlan']};
215              
216             You can use arrayrefs for a list of possible values for an attribute. By default,
217             it will be expanded into an C statement.
218              
219             =head2 Comparison operators
220              
221             # comment isn't empty (more than empty string)
222             my $query = {comment => {'>', ''}};
223              
224             # mtu > 1000 AND mtu < 1500
225             $query = {mtu => {'<' => 1500, '>' => 1000}};
226              
227             Hashrefs can be used for specifying operator for comparison. Well, any of three
228             of them. :) You can put multiple operator-value pairs in one hashref and they
229             will be expanded into an C statement.
230              
231             # mtu < 1000 OR mtu > 1500
232             $query = {mtu => [{'<', 1000}, {'>', 1500}]};
233              
234             # Or like this
235             # mtu < 1000 OR (mtu > 1400 AND mtu < 1500)
236             $query = {mtu => [{'<', 1000}, {'>', 1400, '<', 1500}]};
237              
238             Hashrefs can be also put in lists. If you want them combined into an C
239             statement, for example.
240              
241             # status = 'active' OR status = 'inactive'
242             $query = {mtu => {'=', ['active', 'inactive']}};
243              
244             Or you can use list as a value in a hashref pair. B: In this case, every
245             other pair in the hash will be ignored.
246              
247             =head2 Negation
248              
249             # !(interface = 'ether5')
250             my $query = {interface => {-not => 'ether5'}};
251              
252             # !(interface = 'ether5') AND !(interface = 'ether1')
253             $query = {interface => {-not => [-and => 'ether5', 'ether1']}};
254              
255             Since MikroTik API does not have 'not equal' operator, it ends up been 'opposite
256             of a equals b' expressions.
257              
258             =head2 Checking for an attribute
259              
260             my $query = {-has => 'dafault-name'};
261              
262             $query = {-has_not => 'dafault-name'};
263              
264             Checks if an element has an attribute with specific name.
265              
266             =head2 Literal queries
267              
268             my $query = \['?type=ether', '?running=true', '?actual-mtu=1500', '?#&&'];
269              
270             $query = [
271             type => 'ipip-tunnel',
272             \['?type=ether', '?running=true', '?actual-mtu=1500', '?#&&']
273             ];
274              
275             Reference to an arrayref can be used to pass list of prepared words. Those will
276             be treated as blocks in nested expressions.
277              
278             =head2 Logic and nesting
279              
280             # (mtu = 1460 AND actual-mtu = 1460)
281             # AND
282             # (running = 'false' OR disabled = 'true')
283              
284             my $query = {
285             {mtu => 1460, 'actual-mtu' => 1460},
286             [running => 'false', disabled => 'true']
287             };
288              
289             Conditions can be grouped and nested if needed. It's like putting brackets around
290             them.
291              
292             # Same thing, but with prefixes
293             my $query = {
294             -and => [mtu => 1460, 'actual-mtu' => 1460],
295             -or => {running => 'false', disabled => 'true'}
296             };
297              
298             You can change logic applied to a block by using keywords. Those keywords
299             will go outside for blocks that affect multiple attributes, or ...
300              
301             # !(type = 'ether') AND !(type = 'wlan')
302              
303             # Will produce the same result
304             my $query = {type => [-and => {-not => 'ether'}, {-not => 'wlan'}]};
305             $query = {type => {-not => [-and => 'ether', 'wlan']}};
306              
307             # Wrong, second condition will replace first
308             $query = {type => {-not => 'ether', -not => 'wlan'}};
309              
310             ... inside for a list of values of a single attribute.
311              
312             # This is wrong
313             my $query = [
314             -and =>
315             {type => 'ether'},
316             {running => 'true'}
317             ];
318              
319             # It will actually results in
320             # type = 'ether' OR running = 'true'
321              
322             C<-and> will be treated as prefix for the first hashref and, since this hash has
323             only one element, won't affect anything at all.
324              
325             =cut
326