File Coverage

blib/lib/Modern/OpenAPI/Generator/Spec.pm
Criterion Covered Total %
statement 84 98 85.7
branch 19 42 45.2
condition 9 31 29.0
subroutine 16 17 94.1
pod 6 6 100.0
total 134 194 69.0


line stmt bran cond sub pod time code
1             package Modern::OpenAPI::Generator::Spec;
2              
3 7     7   79088 use v5.26;
  7         19  
4 7     7   28 use strict;
  7         9  
  7         166  
5 7     7   23 use warnings;
  7         1490  
  7         324  
6 7     7   38 use Carp qw(croak);
  7         30  
  7         401  
7 7     7   5838 use Path::Tiny qw(path);
  7         68910  
  7         408  
8 7     7   3060 use Storable qw(dclone);
  7         20840  
  7         8702  
9              
10             sub load {
11 8     8 1 130774 my ( $class, $file ) = @_;
12 8         44 my $p = path($file);
13 8 100       338 croak "Spec not found: $file" unless $p->is_file;
14              
15 7         355 my $data;
16 7 100       62 if ( $p =~ /\.json\z/i ) {
17 1         440 require JSON::MaybeXS;
18 1         6895 $data = JSON::MaybeXS::decode_json( $p->slurp_utf8 );
19             }
20             else {
21 6         627 require YAML::PP;
22 6         55632 $data = YAML::PP->new( boolean => 'JSON::PP' )->load_file("$p");
23             }
24 7 50       132561 croak 'Spec must be a hash' unless ref $data eq 'HASH';
25              
26 7         45 bless {
27             raw => $data,
28             path => "$p",
29             }, $class;
30             }
31              
32 61     61 1 245 sub raw { $_[0]{raw} }
33              
34             # Deep-clone spec and set x-mojo-to => "Controller#operationId" for Mojolicious::Plugin::OpenAPI
35             sub clone_with_mojo_to {
36 5     5 1 29 my ( $self, $controller_short ) = @_;
37 5         560 my $copy = dclone( $self->{raw} );
38 5   50     22 my $paths = $copy->{paths} // return $copy;
39 5         20 for my $p ( keys %$paths ) {
40 5         12 my $item = $paths->{$p};
41 5 50       19 next unless ref $item eq 'HASH';
42 5         27 for my $m (qw(get put post delete patch options head trace)) {
43 40         54 my $op = $item->{$m};
44 40 100       65 next unless ref $op eq 'HASH';
45 5 50       24 my $oid = $op->{operationId} or next;
46 5         18 ( my $sub = $oid ) =~ s/[^A-Za-z0-9_]/_/g;
47 5         16 $op->{'x-mojo-to'} = "$controller_short#$sub";
48             }
49             }
50 5         14 return $copy;
51             }
52              
53             sub openapi_version {
54 23     23 1 55 my ($self) = @_;
55 23   33     87 return $self->{raw}{openapi} // $self->{raw}{swagger} // '';
      0        
56             }
57              
58             sub title {
59 6     6 1 24 my ($self) = @_;
60 6   50     35 return $self->{raw}{info}{title} // 'API';
61             }
62              
63             # Returns list of hashrefs:
64             # operation_id, method, path, path_params, query_params, header_params,
65             # has_body, tags, operation_hash,
66             # response_schema_ref (e.g. #/components/schemas/Foo), response_is_array
67             sub operations {
68 12     12 1 23 my ($self) = @_;
69 12   50     51 my $paths = $self->{raw}{paths} // {};
70 12         21 my @ops;
71 12         47 for my $path ( sort keys %$paths ) {
72 12         24 my $item = $paths->{$path};
73 12 50       35 next unless ref $item eq 'HASH';
74 12         20 my $path_level = $item->{parameters};
75 12         26 for my $method (qw(get put post delete patch options head trace)) {
76 96         112 my $op = $item->{$method};
77 96 100       144 next unless ref $op eq 'HASH';
78 12   33     34 my $oid = $op->{operationId} // _default_operation_id( $method, $path );
79 12         49 my $merged = _merge_parameters( $path_level, $op->{parameters} );
80 12         37 my ( $ref, $is_arr ) = _success_response_json_ref($op);
81             push @ops,
82             {
83             operation_id => $oid,
84             method => uc $method,
85             path => $path,
86             path_params => _params_in( $merged, 'path' ),
87             query_params => _params_in( $merged, 'query' ),
88             header_params => _params_in( $merged, 'header' ),
89             has_body => _has_json_body($op),
90 12   50     71 tags => $op->{tags} // [],
91             operation_hash => $op,
92             response_schema_ref => $ref,
93             response_is_array => $is_arr,
94             };
95             }
96             }
97 12         39 return \@ops;
98             }
99              
100             # First 2xx response with application/json and a concrete schema $ref (object or array of $ref).
101             sub _success_response_json_ref {
102 12     12   20 my ($op) = @_;
103 12   50     44 my $responses = $op->{responses} // {};
104 12         20 for my $code (qw(200 201 202 204)) {
105 12 50       28 next unless exists $responses->{$code};
106 12   50     34 my $content = $responses->{$code}{content} // {};
107 12         23 for my $ct (qw(application/json application/problem+json)) {
108 12 50       28 next unless exists $content->{$ct};
109 12         21 my $sch = $content->{$ct}{schema};
110 12 50       29 next unless ref $sch eq 'HASH';
111 12 50       34 if ( my $r = $sch->{'$ref'} ) {
112 12         35 return ( $r, 0 );
113             }
114 0 0 0     0 if ( ( $sch->{type} // '' ) eq 'array' && ref $sch->{items} eq 'HASH' ) {
      0        
115 0         0 my $it = $sch->{items};
116 0 0       0 if ( my $r = $it->{'$ref'} ) {
117 0         0 return ( $r, 1 );
118             }
119             }
120             }
121             }
122 0         0 return ( undef, 0 );
123             }
124              
125             sub _has_json_body {
126 12     12   35 my ($op) = @_;
127 12   50     163 my $rb = $op->{requestBody} // return 0;
128 0 0       0 return 0 unless ref $rb eq 'HASH';
129 0   0     0 my $c = $rb->{content} // {};
130 0         0 return !!( $c->{'application/json'} );
131             }
132              
133             sub _merge_parameters {
134 12     12   45 my ( $a, $b ) = @_;
135 12         18 my @m;
136 12 50       29 push @m, @$a if ref $a eq 'ARRAY';
137 12 50       23 push @m, @$b if ref $b eq 'ARRAY';
138 12         24 return \@m;
139             }
140              
141             sub _params_in {
142 36     36   67 my ( $params, $in ) = @_;
143 36 50       83 return [] unless ref $params eq 'ARRAY';
144 36         130 my @names;
145 36         55 for my $p (@$params) {
146 0 0       0 next unless ref $p eq 'HASH';
147 0 0 0     0 next unless ( $p->{in} // '' ) eq $in;
148 0 0       0 push @names, $p->{name} if defined $p->{name};
149             }
150 36         103 return \@names;
151             }
152              
153             sub _default_operation_id {
154 0     0     my ( $m, $path ) = @_;
155 0           ( my $s = $path ) =~ s{[^A-Za-z0-9]+}{_}g;
156 0           return uc($m) . '_' . $s;
157             }
158              
159             1;
160              
161             __END__