File Coverage

blib/lib/Yancy/Plugin/Editor.pm
Criterion Covered Total %
statement 179 185 96.7
branch 84 108 77.7
condition 35 57 61.4
subroutine 18 20 90.0
pod 1 1 100.0
total 317 371 85.4


line stmt bran cond sub pod time code
1             package Yancy::Plugin::Editor;
2             our $VERSION = '1.086';
3             # ABSTRACT: Yancy content editor, admin, and management application
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod use Mojolicious::Lite;
8             #pod # The default editor at /yancy
9             #pod plugin Yancy => {
10             #pod backend => 'sqlite://myapp.db',
11             #pod read_schema => 1,
12             #pod editor => {
13             #pod require_user => { can_edit => 1 },
14             #pod },
15             #pod };
16             #pod
17             #pod # Enable another editor for blog users
18             #pod app->plugin->yancy( Editor => {
19             #pod moniker => 'blog_editor',
20             #pod backend => app->yancy->backend,
21             #pod schema => { blog_posts => app->yancy->schema( 'blog_posts' ) },
22             #pod route => app->routes->any( '/blog/editor' ),
23             #pod require_user => { can_blog => 1 },
24             #pod } );
25             #pod
26             #pod =head1 DESCRIPTION
27             #pod
28             #pod This plugin contains the Yancy editor application which allows editing
29             #pod the data in a L.
30             #pod
31             #pod =head1 CONFIGURATION
32             #pod
33             #pod This plugin has the following configuration options.
34             #pod
35             #pod =head2 backend
36             #pod
37             #pod The backend to use for this editor. Defaults to the default backend
38             #pod configured in the L
.
39             #pod
40             #pod =head2 schema
41             #pod
42             #pod The schema to use to build the editor application. This may not
43             #pod necessarily be the full and exact schema supported by the backend: You
44             #pod can remove certain fields from this editor instance to protect them, for
45             #pod example.
46             #pod
47             #pod If not given, will use L to read the schema
48             #pod from the backend.
49             #pod
50             #pod =head2 openapi
51             #pod
52             #pod Instead of L, you can pass a full OpenAPI spec to this editor.
53             #pod This is deprecated; see L.
54             #pod
55             #pod =head2 default_controller
56             #pod
57             #pod The default controller for API routes. Defaults to
58             #pod L. Building a custom controller can allow for
59             #pod customizing how the editor works and what content it displays to which
60             #pod users.
61             #pod
62             #pod =head2 moniker
63             #pod
64             #pod The name of this editor instance. Used to build helper names and route
65             #pod names. Defaults to C. Other plugins may rely on there being
66             #pod a default editor named C. Additional instances should have
67             #pod different monikers.
68             #pod
69             #pod =head2 route
70             #pod
71             #pod A base route to add the editor to. This allows you to customize the URL
72             #pod and add authentication or authorization. Defaults to allowing access to
73             #pod the Yancy web application under C, and the REST API under
74             #pod C.
75             #pod
76             #pod This can be a string or a L object.
77             #pod
78             #pod =head2 return_to
79             #pod
80             #pod The URL to use for the "Back to Application" link. Defaults to C.
81             #pod
82             #pod =head2 title
83             #pod
84             #pod The title of the page, shown in the title bar and the page header. Defaults to C.
85             #pod
86             #pod =head2 host
87             #pod
88             #pod The host to use for the generated OpenAPI spec. Defaults to the current system's hostname
89             #pod (via L).
90             #pod
91             #pod =head2 info
92             #pod
93             #pod An OpenAPI info object, as a Perl hashref. See L
94             #pod spec|https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#infoObject>
95             #pod for what keys are allowed in this hashref.
96             #pod
97             #pod =head1 HELPERS
98             #pod
99             #pod =head2 yancy.editor.include
100             #pod
101             #pod $app->yancy->editor->include( $template_name );
102             #pod
103             #pod Include a template in the editor, before the rest of the editor. Use this
104             #pod to add your own L components to the editor.
105             #pod
106             #pod =head2 yancy.editor.menu
107             #pod
108             #pod $app->yancy->editor->menu( $category, $title, $config );
109             #pod
110             #pod Add a menu item to the editor. The C<$category> is the title of the category
111             #pod in the sidebar. C<$title> is the title of the menu item. C<$config> is a
112             #pod hash reference with the following keys:
113             #pod
114             #pod =over
115             #pod
116             #pod =item component
117             #pod
118             #pod The name of a Vue.JS component to display for this menu item. The
119             #pod component will take up the entire main area of the application, and will
120             #pod be kept active even after another menu item is selected.
121             #pod
122             #pod =back
123             #pod
124             #pod Yancy plugins should use the category C. Other categories are
125             #pod available for custom applications.
126             #pod
127             #pod app->yancy->editor->include( 'plugin/editor/custom_element' );
128             #pod app->yancy->editor->menu(
129             #pod 'Plugins', 'Custom Item',
130             #pod {
131             #pod component => 'custom-element',
132             #pod },
133             #pod );
134             #pod __END__
135             #pod @@ plugin/editor/custom_element.html.ep
136             #pod
141             #pod %= javascript begin
142             #pod Vue.component( 'custom-element', {
143             #pod template: '#custom-element-template',
144             #pod } );
145             #pod % end
146             #pod
147             #pod =for html
148             #pod screenshot of yancy editor showing custom element
149             #pod src="https://raw.github.com/preaction/Yancy/master/eg/doc-site/public/screenshot-custom-element.png?raw=true"
150             #pod style="width: 600px; max-width: 100%;" width="600" />
151             #pod
152             #pod =head2 yancy.editor.route
153             #pod
154             #pod Get the route where the editor will appear.
155             #pod
156             #pod =head2 yancy.editor.openapi
157             #pod
158             #pod my $openapi = $c->yancy->openapi;
159             #pod
160             #pod Get the L object containing the OpenAPI
161             #pod interface for this Yancy API.
162             #pod
163             #pod =head1 TEMPLATES
164             #pod
165             #pod To override these templates, add your own at the designated path inside
166             #pod your app's C directory.
167             #pod
168             #pod =head2 yancy/editor.html.ep
169             #pod
170             #pod This is the main Yancy web application. You should not override this.
171             #pod Instead, use the L helper to add new components.
172             #pod If there is something you can't do using the include helper, consider
173             #pod L
174             #pod or L.
175             #pod
176             #pod =head1 SEE ALSO
177             #pod
178             #pod L, L, L
179             #pod
180             #pod =cut
181              
182 19     19   15824 use Mojo::Base 'Mojolicious::Plugin';
  19         81  
  19         134  
183 19     19   3499 use Scalar::Util qw( blessed );
  19         43  
  19         1179  
184 19     19   140 use Mojo::JSON qw( true false );
  19         50  
  19         1190  
185 19     19   142 use Mojo::Util qw( url_escape );
  19         43  
  19         1069  
186 19     19   9036 use Sys::Hostname qw( hostname );
  19         21890  
  19         1237  
187 19     19   143 use Yancy::Util qw( derp currym json_validator load_backend );
  19         52  
  19         78429  
188              
189             has moniker => 'editor';
190             has route =>;
191             has includes => sub { [] };
192             has menu_items => sub { +{} };
193             has model =>;
194             has schema =>;
195             has app => undef, weak => 1;
196              
197             sub _helper_name {
198 194     194   606 my ( $self, $name ) = @_;
199 194         730 return join '.', 'yancy', $self->moniker, $name;
200             }
201              
202             sub register {
203 52     52 1 484 my ( $self, $app, $config ) = @_;
204              
205 52   33     730 $config->{title} //= $app->l( 'Yancy' );
206 52   33     8041 $config->{return_label} //= $app->l( 'Back to Application' );
207              
208 52 50 66     6186 if ( $config->{backend} || $config->{schema} ) {
209             my $backend = $config->{backend} ? (
210             blessed $config->{backend}
211             ? $config->{backend}
212             : load_backend( $config->{backend} )
213 52 50       283 ) : $app->yancy->backend;
    100          
214             my $model = Yancy::Model->new(
215             backend => $backend,
216             ( schema => $config->{schema} )x!!$config->{schema},
217             ( read_schema => $config->{read_schema} )x!!exists $config->{read_schema},
218 52         727 );
219 52         280 $self->model( $model );
220             }
221             else {
222 0         0 $self->model( $app->yancy->model );
223             }
224 52         607 $self->schema( my $schema = $config->{schema} );
225 52         417 $self->app( $app );
226              
227 52         666 for my $key ( grep exists $config->{ $_ }, qw( moniker ) ) {
228 1         6 $self->$key( $config->{ $key } );
229             }
230 52   50     571 $config->{default_controller} //= $config->{api_controller} // 'Yancy';
      33        
231 52 50       211 if ( $config->{api_controller} ) {
232 0         0 derp 'api_controller configuration is deprecated. Use editor.default_controller instead';
233             }
234              
235             # XXX: Throw an error if there is already a route here
236 52         243 my $route = $app->yancy->routify( $config->{route}, '/yancy' );
237 52   50     23219 $route->to( return_to => $config->{return_to} // '/' );
238              
239             # Create authentication for editor. We need to delay fetching this
240             # callback until after startup is complete so that any auth plugin
241             # can be added.
242             my $auth_under = sub {
243 95     95   5275242 my ( $c ) = @_;
244             state $auth_cb = $c->yancy->can( 'auth' )
245             && $c->yancy->auth->can( 'require_user' )
246 95   66     302 && $c->yancy->auth->require_user( $config->{require_user} || () );
247 95 50 100     1101 if ( !$auth_cb && !exists $config->{require_user} && !defined $config->{route} ) {
      66        
248 4         19 $app->log->warn(
249             qq{*** Cannot verify that admin editor is behind authentication.\n}
250             . qq{Add a Yancy Auth plugin, add a `route` to the Yancy plugin config,\n}
251             . qq{or set `editor.require_user => undef` to silence this warning\n}
252             );
253             }
254 95 100       730 return $auth_cb ? $auth_cb->( $c ) : 1;
255 52         1719 };
256 52         249 $route = $route->under( $auth_under );
257              
258             # First create the OpenAPI schema and API URL
259 52         10035 my $spec;
260 52 100 100     291 if ( $config->{openapi} && keys %{ $config->{openapi} } ) {
  10         58  
261 4         13 $spec = $config->{openapi};
262             }
263             else {
264             # Add OpenAPI spec
265 48         227 $spec = $self->_openapi_spec_from_schema( $config );
266             }
267 52         17631 $self->_openapi_spec_add_mojo( $spec, $config );
268              
269 50         408 my $openapi = $app->plugin( OpenAPI => {
270             route => $route->any( '/api' )->to( model => $self->model )->name( 'yancy.api' ),
271             spec => $spec,
272             default_response_name => '_Error',
273             validator => json_validator(),
274             } );
275 50         42115041 $_->to(format => 'json') for (@{$openapi->route->children});
  50         340  
276             $app->helper( 'yancy.openapi' => sub {
277 0     0   0 derp 'yancy.openapi helper is deprecated. Use yancy.editor.openapi instead';
278 0         0 return $openapi;
279 50         32221 } );
280 50     1   95554 $app->helper( $self->_helper_name( 'openapi' ) => sub { $openapi } );
  1         274  
281              
282             # Do some sanity checks on the config to make sure nothing bad
283             # happens
284 50         102997 for my $schema_name ( keys %$schema ) {
285 290 100       1001 if ( my $view = $schema->{ $schema_name }{ 'x-view' } ) {
286 74         254 $schema_name = $view->{schema};
287             }
288 290 50       658 next if $schema->{ $schema_name }{ 'x-view' };
289 290 100       728 if ( my $list_cols = $schema->{ $schema_name }{ 'x-list-columns' } ) {
290 134         289 for my $col ( @$list_cols ) {
291 287 100       652 if ( ref $col eq 'HASH' ) {
292             # Check template for columns
293 1         10 my @cols = $col->{template} =~ m'\{([^{]+)\}'g;
294 1 50       4 if ( my ( $col ) = grep { !exists $schema->{ $schema_name }{ properties }{ $_ } } @cols ) {
  2         11  
295 1         30 die sprintf q{Column "%s" in x-list-columns template does not exist in schema "%s"},
296             $col, $schema_name;
297             }
298             }
299             else {
300 286 100       910 if ( !exists $schema->{ $schema_name }{ properties }{ $col } ) {
301 1         48 die sprintf q{Column "%s" in x-list-columns does not exist in schema "%s"},
302             $col, $schema_name;
303             }
304             }
305             }
306             }
307             }
308              
309             # Now create the routes and helpers the editor needs
310             $route->get( '/' )->name( 'yancy.index' )
311             ->to(
312             template => 'yancy/index',
313             controller => $config->{default_controller},
314             action => 'index',
315             api_url => $openapi->route->render,
316             title => $config->{title},
317             return_label => $config->{return_label},
318 48         333 );
319             $route->post( '/upload' )->name( 'yancy.editor.upload' )
320             ->to( cb => sub {
321 1     1   146 my ( $c ) = @_;
322 1         6 my $upload = $c->param( 'upload' );
323 1         264 my $path = $c->yancy->file->write( $upload );
324 1         153 $c->res->headers->location( $path );
325 1         32 $c->render( status => 201, text => $path );
326 48         20312 } );
327              
328 48         15903 $app->helper( $self->_helper_name( 'menu' ), currym( $self, '_helper_menu' ) );
329 48         103212 $app->helper( $self->_helper_name( 'include' ), currym( $self, '_helper_include' ) );
330 48     1   104816 $app->helper( $self->_helper_name( 'route' ), sub { $route } );
  1         99  
331             $app->helper( 'yancy.route', sub {
332 0     0   0 derp 'yancy.route helper is deprecated. Use yancy.editor.route instead';
333 0         0 return $self->route;
334 48         107052 } );
335 48         108505 $self->route( $route );
336              
337              
338             }
339              
340             sub _helper_include {
341 6     6   27 my ( $self, $c, @includes ) = @_;
342 6 100       26 if ( @includes ) {
343 1         4 push @{ $self->includes }, @includes;
  1         5  
344             }
345 6         33 return $self->includes;
346             }
347              
348             sub _helper_menu {
349 6     6   26 my ( $self, $c, $category, $title, $config ) = @_;
350 6 100       42 if ( $config ) {
351 1         3 push @{ $self->menu_items->{ $category } }, {
  1         6  
352             %$config,
353             title => $title,
354             };
355             }
356 6         26 return $self->menu_items;
357             }
358              
359             sub _openapi_find_schema_name {
360 495     495   909 my ( $self, $path, $pathspec ) = @_;
361 495 50       1064 return $pathspec->{'x-schema'} if $pathspec->{'x-schema'};
362 495         656 my $schema_name;
363 495         684 for my $method ( grep !/^(parameters$|x-)/, keys %{ $pathspec } ) {
  495         2809  
364 1010         1649 my $op_spec = $pathspec->{ $method };
365 1010         1213 my $schema;
366 1010 100       2603 if ( $method eq 'get' ) {
    100          
367             # d is in case only has "default" response
368 493         685 my ($response) = grep /^[2d]/, sort keys %{ $op_spec->{responses} };
  493         2719  
369 493         1077 my $response_spec = $op_spec->{responses}{$response};
370 493 50       1206 next unless $schema = $response_spec->{schema};
371             } elsif ( $method =~ /^(put|post)$/ ) {
372             my @body_params = grep 'body' eq ($_->{in} // ''),
373 344 50       867 @{ $op_spec->{parameters} || [] },
374 344 100 50     514 @{ $pathspec->{parameters} || [] },
  344         1842  
375             ;
376 344 50       850 die "No more than 1 'body' parameter allowed" if @body_params > 1;
377 344 50       833 next unless $schema = $body_params[0]->{schema};
378             }
379             next unless my $this_ref =
380             $schema->{'$ref'} ||
381             ( $schema->{items} && $schema->{items}{'$ref'} ) ||
382 1010 100 33     4954 ( $schema->{properties} && $schema->{properties}{items} && $schema->{properties}{items}{'$ref'} );
383 591 50       2178 next unless $this_ref =~ s:^#/definitions/::;
384 591 50 66     2368 die "$method '$path' = $this_ref but also '$schema_name'"
      66        
385             if $this_ref and $schema_name and $this_ref ne $schema_name;
386 591         1189 $schema_name = $this_ref;
387             }
388 495 100       1063 if ( !$schema_name ) {
389 76         384 ($schema_name) = $path =~ m#^/([^/]+)#;
390 76 50       236 die "No schema found in '$path'" if !$schema_name;
391             }
392 495         1001 $schema_name;
393             }
394              
395             # mutates $spec
396             sub _openapi_spec_add_mojo {
397 52     52   224 my ( $self, $spec, $config ) = @_;
398 52         135 for my $path ( keys %{ $spec->{paths} } ) {
  52         352  
399 495         950 my $pathspec = $spec->{paths}{ $path };
400 495         1076 my $schema = $self->_openapi_find_schema_name( $path, $pathspec );
401             die "Path '$path' had non-existent schema '$schema'"
402 495 100       1300 if !$spec->{definitions}{$schema};
403 494         683 for my $method ( grep !/^(parameters$|x-)/, keys %{ $pathspec } ) {
  494         2000  
404 1009         1591 my $op_spec = $pathspec->{ $method };
405 1009         2257 my $mojo = $self->_openapi_spec_infer_mojo( $path, $pathspec, $method, $op_spec );
406             # XXX Allow overriding controller on a per-schema basis
407             # This gives more control over how a certain schema's items
408             # are written/read from the database
409 1008         1970 $mojo->{controller} = $config->{default_controller};
410 1008         1833 $mojo->{schema} = $schema;
411             my @filters = (
412 1008 50       3091 @{ $pathspec->{ 'x-filter' } || [] },
413 1008 50       1295 @{ $op_spec->{ 'x-filter' } || [] },
  1008         2967  
414             );
415 1008 50       1993 $mojo->{filters} = \@filters if @filters;
416             my @filters_out = (
417 1008 50       2481 @{ $pathspec->{ 'x-filter-output' } || [] },
418 1008 50       1289 @{ $op_spec->{ 'x-filter-output' } || [] },
  1008         2589  
419             );
420 1008 50       1843 $mojo->{filters_out} = \@filters_out if @filters_out;
421 1008         2261 $op_spec->{ 'x-mojo-to' } = $mojo;
422             }
423             }
424             }
425              
426             # for a given OpenAPI operation, figures out right values for 'x-mojo-to'
427             # to hook it up to the correct CRUD operation
428             sub _openapi_spec_infer_mojo {
429 1009     1009   1857 my ( $self, $path, $pathspec, $method, $op_spec ) = @_;
430             my @path_params = grep 'path' eq ($_->{in} // ''),
431 1009 100       2588 @{ $pathspec->{parameters} || [] },
432 1009 100 100     1266 @{ $op_spec->{parameters} || [] },
  1009         6211  
433             ;
434             my ($id_field) = grep defined,
435             (map $_->{'x-id-field'}, $op_spec, $pathspec),
436 1009   66     4447 (@path_params && $path_params[-1]{name});
437 1009 100       2646 if ( $method eq 'get' ) {
    100          
    100          
    100          
438             # heuristic: is per-item if have a param in path
439 493 100       927 if ( $id_field ) {
440             # per-item - GET = "read"
441             return {
442 247         808 action => 'get',
443             format => 'json',
444             };
445             }
446             else {
447             # per-schema - GET = "list"
448             return {
449 246         866 action => 'list',
450             format => 'json',
451             };
452             }
453             } elsif ( $method eq 'post' ) {
454             return {
455 171         695 action => 'set',
456             format => 'json',
457             };
458             } elsif ( $method eq 'put' ) {
459 172 50       384 die "'$method' $path needs id_field" if !$id_field;
460             return {
461 172         602 action => 'set',
462             format => 'json',
463             };
464             } elsif ( $method eq 'delete' ) {
465 172 50       402 die "'$method' $path needs id_field" if !$id_field;
466             return {
467 172         659 action => 'delete',
468             format => 'json',
469             };
470             }
471             else {
472 1         27 die "Unknown method '$method'";
473             }
474             }
475              
476             sub _openapi_spec_from_schema {
477 48     48   127 my ( $self, $config ) = @_;
478 48         108 my ( %definitions, %paths );
479 48         150 my $app = $self->app;
480 48         349 my %parameters = (
481             '$limit' => {
482             name => '$limit',
483             type => 'integer',
484             in => 'query',
485             description => $app->l( 'OpenAPI $limit description' ),
486             },
487             '$offset' => {
488             name => '$offset',
489             type => 'integer',
490             in => 'query',
491             description => $app->l( 'OpenAPI $offset description' ),
492             },
493             '$order_by' => {
494             name => '$order_by',
495             type => 'string',
496             in => 'query',
497             pattern => '^(?:asc|desc):[^:,]+$',
498             description => $app->l( 'OpenAPI $order_by description' ),
499             },
500             '$match' => {
501             name => '$match',
502             type => 'string',
503             enum => [qw( any all )],
504             default => 'all',
505             in => 'query',
506             description => $app->l( 'OpenAPI $match description' ),
507             },
508             );
509 48         22081 my $model = $self->model;
510 48         361 for my $schema_name ( $model->schema_names ) {
511             # Set some defaults so users don't have to type as much
512 234         151783 my $schema = $model->schema( $schema_name );
513 234         693 my $json_schema = $schema->json_schema;
514 234 50       1308 next if $json_schema->{ 'x-ignore' };
515 234         690 my $id_field = $schema->id_field;
516 234 100       1904 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
517 234         481 my $props = $json_schema->{properties};
518 234 50 66     803 if ( !$props && $json_schema->{'x-view'} ) {
519 35         101 my $real_schema_name = $json_schema->{'x-view'}{schema};
520 35         159 $props = $model->schema( $real_schema_name )->json_schema->{properties};
521             }
522 234         1821 my %props = %$props;
523              
524 234         606 $definitions{ $schema_name } = $json_schema;
525              
526 234         705 for my $prop ( keys %props ) {
527 1640   50     3366 $props{ $prop }{ type } ||= 'string';
528             }
529              
530             $paths{ '/' . $schema_name } = {
531             get => {
532             description => $app->l( 'OpenAPI list description' ),
533             parameters => [
534             { '$ref' => '#/parameters/%24limit' },
535             { '$ref' => '#/parameters/%24offset' },
536             { '$ref' => '#/parameters/%24order_by' },
537             { '$ref' => '#/parameters/%24match' },
538             map {
539 1640         176115 my $name = $_;
540 1640 100       4652 my $type = ref $props{ $_ }{type} eq 'ARRAY' ? $props{ $_ }{type}[0] : $props{ $_ }{type};
541 1640 100 100     8560 my $description = $app->l(
    100          
    100          
542             $type eq 'number' || $type eq 'integer' ? 'OpenAPI filter number description'
543             : $type eq 'boolean' ? 'OpenAPI filter boolean description'
544             : $type eq 'array' ? 'OpenAPI filter array description'
545             : 'OpenAPI filter string description'
546             );
547             {
548 1640         172887 name => $name,
549             in => 'query',
550             type => $type,
551             description => $app->l( 'OpenAPI filter description', $name ) . $description,
552             } } grep !exists( $props{ $_ }{'$ref'} ), sort keys %props,
553             ],
554             responses => {
555             200 => {
556             description => $app->l( 'OpenAPI list response' ),
557             schema => {
558             type => 'object',
559             required => [qw( items total )],
560             properties => {
561             total => {
562             type => 'integer',
563             description => $app->l( 'OpenAPI list total description' ),
564             },
565             items => {
566             type => 'array',
567             description => $app->l( 'OpenAPI list items description' ),
568             items => { '$ref' => "#/definitions/" . url_escape $schema_name },
569             },
570             offset => {
571             type => 'integer',
572             description => $app->l( 'OpenAPI list offset description' ),
573             },
574             },
575             },
576             },
577             default => {
578             description => $app->l( 'Unexpected error' ),
579             schema => { '$ref' => '#/definitions/_Error' },
580             },
581             },
582             },
583             $json_schema->{'x-view'} ? () : (post => {
584             parameters => [
585             {
586             name => "newItem",
587             in => "body",
588             required => true,
589             schema => { '$ref' => "#/definitions/" . url_escape $schema_name },
590             },
591             ],
592             responses => {
593             201 => {
594             description => $app->l( 'OpenAPI create response' ),
595             schema => {
596             @id_fields > 1
597             ? (
598             type => 'array',
599             items => [
600             map +{
601 64         6610 '$ref' => '#/' . join '/', map { url_escape $_ }
602             'definitions', $schema_name, 'properties', $_,
603             }, @id_fields,
604             ],
605             )
606             : (
607 234 100       843 '$ref' => '#/' . join '/', map { url_escape $_ }
  624 100       124265  
608             'definitions', $schema_name, 'properties', $id_fields[0],
609             ),
610             },
611             },
612             default => {
613             description => $app->l( "Unexpected error" ),
614             schema => { '$ref' => "#/definitions/_Error" },
615             },
616             },
617             }),
618             };
619              
620             $paths{ sprintf '/%s/{%s}', $schema_name, $id_field } = {
621             parameters => [
622             map +{
623             name => $_,
624             in => 'path',
625             required => true,
626             type => 'string',
627             'x-mojo-placeholder' => '*',
628             }, @id_fields
629             ],
630              
631             get => {
632             description => $app->l( 'OpenAPI get description' ),
633             responses => {
634             200 => {
635             description => $app->l( 'OpenAPI get response' ),
636             schema => { '$ref' => "#/definitions/" . url_escape $schema_name },
637             },
638             default => {
639             description => $app->l( "Unexpected error" ),
640             schema => { '$ref' => '#/definitions/_Error' },
641             }
642             }
643             },
644              
645 234 100       69263 $json_schema->{'x-view'} ? () : (put => {
646             description => $app->l( 'OpenAPI update description' ),
647             parameters => [
648             {
649             name => "newItem",
650             in => "body",
651             required => true,
652             schema => { '$ref' => "#/definitions/" . url_escape $schema_name },
653             }
654             ],
655             responses => {
656             200 => {
657             description => $app->l( 'OpenAPI update response' ),
658             schema => { '$ref' => "#/definitions/" . url_escape $schema_name },
659             },
660             default => {
661             description => $app->l( "Unexpected error" ),
662             schema => { '$ref' => "#/definitions/_Error" },
663             }
664             }
665             },
666              
667             delete => {
668             description => $app->l( 'OpenAPI delete description' ),
669             responses => {
670             204 => {
671             description => $app->l( 'OpenAPI delete response' ),
672             },
673             default => {
674             description => $app->l( "Unexpected error" ),
675             schema => { '$ref' => '#/definitions/_Error' },
676             },
677             },
678             }),
679             };
680             }
681              
682             return {
683             info => $config->{info} || { title => $config->{title}, version => "1" },
684             swagger => '2.0',
685 48   50     36136 host => $config->{host} // hostname(),
      33        
686             basePath => '/api',
687             schemes => [qw( http )],
688             consumes => [qw( application/json )],
689             produces => [qw( application/json )],
690             definitions => {
691             _Error => {
692             'x-ignore' => 1, # In case we get round-tripped into a Yancy::Model
693             title => $app->l( 'OpenAPI error object' ),
694             type => 'object',
695             properties => {
696             errors => {
697             type => "array",
698             items => {
699             required => [qw( message )],
700             properties => {
701             message => {
702             type => "string",
703             description => $app->l( 'OpenAPI error message' ),
704             },
705             path => {
706             type => "string",
707             description => $app->l( 'OpenAPI error path' ),
708             }
709             }
710             }
711             }
712             }
713             },
714             %definitions,
715             },
716             paths => \%paths,
717             parameters => \%parameters,
718             };
719             }
720              
721              
722             1;
723              
724             __END__