OpenAPI-PerlGenerator

 view release on metacpan or  search on metacpan

lib/OpenAPI/PerlGenerator/Template/Mojo.pm  view on Meta::CPAN

  __MY_TEMPLATE__

=cut

our %template;

$template{required_parameters} = <<'__REQUIRED_PARAMETERS__';
%# Check that we received all required parameters:
% if( my $p = $elt->{parameters}) {
%     my @required = grep { $_->{required} } $elt->{parameters}->@*;
%     if( @required ) {
%         for my $p (@required) {
    croak "Missing required parameter '<%= $p->{name} %>'"
        unless exists $options{ '<%= $p->{name} %>' };
%         }

%     }
% } # parameter-required
__REQUIRED_PARAMETERS__

$template{path_parameters} = <<'__PATH_PARAMETERS__';
    my $template = URI::Template->new( '<%= $method->{path} %>' );
    my $path = $template->process(
%     for my $p ($params->@*) {
%         if( $p->{required} ) {
              '<%= $p->{name} %>' => delete $options{'<%= $p->{name} %>'},
%         } else {
        maybe '<%= $p->{name} %>' => delete $options{'<%= $p->{name} %>'},
%         }
%     }
    );
__PATH_PARAMETERS__

$template{generate_request_body} = <<'__REQUEST_BODY__';
%     for my $ct (sort keys $content->%*) {
%         if( exists $content->{$ct}->{schema}) {
%             if( $content->{$ct}->{schema}->{type} eq 'string' ) {
    my $body = delete $options{ body } // '';
%             } else {
    my $request = <%= $prefix %>::<%= $content->{$ct}->{schema}->{name} %>->new( \%options );
%             }
%         } elsif( $ct eq 'multipart/form-data' ) {
%             # nothing to do
%         } else {
              # don't know how to handle content type <%= $ct %>...
%         }
%     }
__REQUEST_BODY__

$template{inflated_response} = <<'__INFLATED_RESPONSE__';
% if( $type->{name} ) {
<%= $prefix %>::<%= $type->{name} %>->new(<%= $argname %>),
% } elsif( $type->{type} and $type->{type} eq 'array') {
%# use Data::Dumper; warn Dumper $type;
[ map { <%= include('inflated_response', { type => $type->{items}, prefix => $prefix, argname => '$_' }) %> } $payload->@* ],
% } else {
<%= $argname %>
% }
__INFLATED_RESPONSE__

$template{streaming_response} = <<'__STREAMING_RESPONSE__';
    use Future::Queue;
    my $res = Future::Queue->new( prototype => 'Future::Mojo' );
    our @store; # we should use ->retain() instead
    push @store, $r1->then( sub( $tx ) {
        my $resp = $tx->res;
        # Should we validate using OpenAPI::Modern here?!
%# Should this be its own subroutine instead?!
% for my $code (sort keys $elt->{responses}->%*) {                             # response code s
%     my $info = $elt->{responses}->{ $code };
%# XXX if streaming, we need to handle a non-streaming error response!
        <%= elsif_chain($name) %>( $resp->code <%= openapi_http_code_match( $code ) %> ) {
%     if( $info->{description} =~ /\S/ ) {
            # <%= single_line( $info->{description} ) %>
%     }
%       # Check the content type
%       # Will we always have a content type?!
%       if( keys $info->{content}->%* ) {
%           for my $ct (sort keys $info->{content}->%*) {
            my $ct = $resp->headers->content_type;
            return unless $ct;
            $ct =~ s/;\s+.*//;
            if( $ct eq '<%= $ct %>' ) {
                # we only handle ndjson currently
                my $handled_offset = 0;
                $resp->on(progress => sub($msg,@) {
                    my $fresh = substr( $msg->body, $handled_offset );
                    my $body = $msg->body;
                    $body =~ s/[^\r\n]+\z//; # Strip any unfinished line
                    $handled_offset = length $body;
                    my @lines = split /\n/, $fresh;
                    for (@lines) {
                        my $payload = decode_json( $_ );
                        $res->push(
% my $type = $info->{content}->{$ct}->{schema};
                            <%= include('inflated_response', { type => $type, prefix => $prefix, argname => '$payload' } ) %>
                        );
                    };
                    if( $msg->{state} eq 'finished' ) {
                        $res->finish();
                    }
                });
            }
%           }
%           } else { # we don't know how to handle this, so pass $res          # known content types?
%# XXX should we always use ->done or should we use ->fail for 4xx and 5xx ?!
            return Future::Mojo->done($resp);
%           }
% }
        } else {
            # An unknown/unhandled response, likely an error
            return Future::Mojo->fail($resp);
        }
    });

    my $_tx;
    $tx->res->once( progress => sub($msg, @) {
        $r1->resolve( $tx );
        undef $_tx;
        undef $r1;
    });
    $_tx = $self->ua->start_p($tx);
__STREAMING_RESPONSE__

$template{synchronous_response} = <<'__SYNCHRONOUS_RESPONSE__';
    my $res = $r1->then( sub( $tx ) {
        my $resp = $tx->res;
        # Should we validate using OpenAPI::Modern here?!
%# Should this be its own subroutine instead?!
% my $first_code = 1;
% for my $code (sort keys $elt->{responses}->%*) {                             # response code s
%     my $info = $elt->{responses}->{ $code };
        <%= elsif_chain($name) %>( $resp->code <%= openapi_http_code_match( $code ) %> ) {
%     if( $info->{description} =~ /\S/ ) {
            # <%= single_line( $info->{description} ) %>
%     }
%       # Check the content type
%       # Will we always have a content type?!
%       if( keys $info->{content}->%* ) {
%           for my $ct (sort keys $info->{content}->%*) {
            my $ct = $resp->headers->content_type;
            $ct =~ s/;\s+.*//;
            if( $ct eq '<%= $ct %>' ) {
%# These handlers for content types should come from templates? Or maybe
%# from a subroutine?!
%               if( $ct eq 'application/json' ) {
                my $payload = $resp->json();
%               } elsif( $ct eq 'application/x-ndjson' ) {
                # code missing to hack up ndjson into hashes for a non-streaming response
                my $payload = $resp->body();
%               } else {
                my $payload = $resp->body();
%               }
                return Future::Mojo->done(
% my $type = $info->{content}->{$ct}->{schema};
                    <%= include('inflated_response', { type => $type, prefix => $prefix, argname => '$payload' } ) %>
                );
            }
%           }
%           } else { # we don't know how to handle this, so pass $res          # known content types?
            return Future::Mojo->done($resp);
%           }
% }
        } else {
            # An unknown/unhandled response, likely an error
            return Future::Mojo->fail($resp);
        }
    });

    # Start our transaction
    $tx = $self->ua->start_p($tx)->then(sub($tx) {
        $r1->resolve( $tx );
        undef $r1;
    })->catch(sub($err) {
        $r1->fail( $err => $tx );
        undef $r1;
    });
__SYNCHRONOUS_RESPONSE__

$template{object} = <<'__OBJECT__';
% my @subclasses;
% my @included_types;
% if( exists $elt->{allOf}) {
%     for my $item ($elt->{allOf}->@*) {
%         if( $item->{name} ) {
%             push @subclasses, $item;
%         } else {
%             push @included_types, $item;
%         }
%     }
% } else {
%     push @included_types, $elt;
% }
%
package <%= $prefix %>::<%= $name %> 0.01;
# DO NOT EDIT! This is an autogenerated file.
use 5.020;
use Moo 2;
use experimental 'signatures';
use Types::Standard qw(Str Bool Num Int Object ArrayRef);
use MooX::TypeTiny;

=head1 NAME

<%= $prefix %>::<%= $name %> -

=head1 SYNOPSIS

  my $obj = <%= $prefix %>::<%= $name %>->new();

lib/OpenAPI/PerlGenerator/Template/Mojo.pm  view on Meta::CPAN

use experimental 'signatures';
use PerlX::Maybe;
use Carp 'croak';

# These should go into a ::Role
use YAML::PP;
use Mojo::UserAgent;
use Mojo::URL;
use Mojo::JSON 'encode_json', 'decode_json';
use OpenAPI::Modern;

use Future::Mojo;

% my @submodules = openapi_submodules($schema);
% while (my($submodule,$info) = splice( @submodules, 0, 2 )) {
%     if( $info->{type} and $info->{type} eq 'object' ) {
use <%= $prefix %>::<%= $submodule %>;
%     }
% }

=head1 SYNOPSIS

=head1 PROPERTIES

=head2 B<< openapi >>

=head2 B<< ua >>

=head2 B<< server >>

=cut

# XXX this should be more configurable, and potentially you don't want validation?!
has 'schema' => (
    is => 'lazy',
    default => sub {
        YAML::PP->new( boolean => 'JSON::PP' )->load_file( 'ollama/ollama-curated.yaml' );
    },
);

has 'openapi' => (
    is => 'lazy',
    default => sub { OpenAPI::Modern->new( openapi_schema => $_[0]->schema, openapi_uri => '/api' )},
);

# The HTTP stuff should go into a ::Role I guess
has 'ua' => (
    is => 'lazy',
    default => sub { Mojo::UserAgent->new },
);

has 'server' => (
    is => 'lazy',
    default => sub { 'http://localhost:11434/api' }, # XXX pull from OpenAPI file instead
);

=head1 METHODS

% for my $method ($methods->@*) {
% my $elt = $method->{elt};
% my $is_streaming =    exists $elt->{responses}->{200}
%                    && $elt->{responses}->{200}->{content}
%                    && [keys $elt->{responses}->{200}->{content}->%*]->[0] eq 'application/x-ndjson'
%                    ;
%
%# Sort the parameters according to where they go
% my %parameters;
% if( my $p = $elt->{parameters}) {
%     for my $p ($elt->{parameters}->@*) {
%         $parameters{ $p->{in} } //= [];
%         push $parameters{ $p->{in} }->@*, $p;
%     }
% }
%
=head2 C<< <%= $method->{name} %> >>

%# Generate the example invocation
% if( $is_streaming ) {
  use Future::Utils 'repeat';
  my $responses = $client-><%= $method->{name} %>();
  repeat {
      my ($res) = $responses->shift;
      if( $res ) {
          my $str = $res->get;
          say $str;
      }

      Future::Mojo->done( defined $res );
  } until => sub($done) { $done->get };
% } else {
  my $res = $client-><%= $method->{name} %>()->get;
% }

% if( $elt->{summary}  and $elt->{summary} =~ /\S/ ) {
<%= markdown_to_pod( $elt->{summary} =~ s/\s*$//r ) %>

%}
%# List/add the invocation parameters
% my $parameters = $elt->{parameters};
% if( $parameters ) { # parameters
=head3 Parameters

=over 4

%     for my $p ($parameters->@* ) {
=item B<< <%= $p->{name} %> >>

%     if( $p->{description} =~ /\S/ ) {
<%= markdown_to_pod( $p->{description} =~ s/\s*$//r ) %>

%     }
%     if( $p->{default}) {
Defaults to C<< <%= $p->{default} =%> >>

%         }
%     }
=back

% } # parameters
%#
%# Add the body/schema parameters:
% (my $ct) = exists $elt->{requestBody} ? keys $elt->{requestBody}->{content}->%* : ();
% my $type;
% if( $ct ) {
%     $type = $ct && $elt->{requestBody}->{content}->{$ct}->{schema};
% };
% if( $type ) {
%     my @properties = (sort keys $type->{properties}->%*);
%     if( @properties ) {

=head3 Options

=over 4

%         for my $prop (@properties) {
%             my $p = $type->{properties}->{$prop};
=item C<< <%= property_name( $prop ) %> >>

% if( $p->{description} ) {
<%= markdown_to_pod( $p->{description} =~ s/\s*$//r ) %>

% }
%         }
=back
%     }
% }

%=include('return_types', { prefix => $prefix, elt => $elt });
=cut

<%= include( 'build_request', { method => $method, parameters => \%parameters, elt => $elt, ct => $ct, prefix => $prefix, } ); %>

sub <%= $method->{name} %>( $self, %options ) {
    my $tx = $self->_build_<%= $method->{name} %>_request(%options);

    # validate our request while developing
    my $results = $self->openapi->validate_request($tx->req);
    if( $results->{error}) {
        say $results;
        say $tx->req->to_string;
    };

%# We want to handle both here, streaming (ndjson) and plain responses
%# Plain responses are easy, but for streamed, we want to register an ->on('progress')
%# handler instead of the plain part completely. In the ->on('progress') part,
%# we still run the handler, so maybe that is the same ?!

    my $r1 = Future::Mojo->new();
% if( $is_streaming ) {
<%= include('streaming_response', {
         name => $method->{name},
         elt => $elt,
         prefix => $prefix,
          }); =%>
% } else {
<%= include('synchronous_response', {
         name => $method->{name},
         elt => $elt,
         prefix => $prefix,
         });
=%>
% }

    return $res
}

% }

1;
__CLIENT_IMPLEMENTATION__

$template{client} = <<'__CLIENT__';
package <%= $prefix %>::<%= $name %> 0.01;
use 5.020;
use Moo 2;
use experimental 'signatures';

extends '<%= $prefix %>::<%= $name %>::Impl';

=head1 NAME

<%= $prefix %>::<%= $name %> - Client for <%= $prefix %>

=head1 SYNOPSIS

  use 5.020;
  use <%= $prefix %>::<%= $name %>;

  my $client = <%= $prefix %>::<%= $name %>->new(
      server => '<%= $schema->{servers}->[0]->{url} // "https://example.com/" %>',
  );
  my $res = $client->someMethod()->get;
  say $res;

=head1 METHODS

% for my $method ($methods->@*) {
% my $elt = $method->{elt};
=head2 C<< <%= $method->{name} %> >>

  my $res = $client-><%= $method->{name} %>()->get;

% if( $elt->{summary} and $elt->{summary} =~ /\S/ ) {
<%= $elt->{summary} =~ s/\s*$//r; %>

%}
%=include('return_types', { prefix => $prefix, elt => $elt });
=cut

% }



( run in 2.010 seconds using v1.01-cache-2.11-cpan-97f6503c9c8 )