package Peace::Swagger; use v5.30.0; use strict; use warnings; use Data::Dumper; use Params::ValidationCompiler qw/validation_for/; use Types::Standard qw/ArrayRef Str HashRef Int/; use Const::Fast; use Email::Valid; const my $EMAIL_VALIDATOR => Type::Tiny->new( name => 'Email', constraint => sub { return Email::Valid->rfc822($_); }, message => sub { return "$_ is not an email address."; }, ); const my $UUID_VALIDATOR => Type::Tiny->new( name => 'Uuid', constraint => sub { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; }, message => sub { return "$_ is not an uuid."; }, ); const my $DEVELOPER_IDENTIFIER_VALIDATOR => Type::Tiny->new( name => 'DeveloperIdentifier', constraint => sub { return 1 if $EMAIL_VALIDATOR->check($_); return 1 if $UUID_VALIDATOR->check($_); return 0; }, message => sub { return "$_ is not a developer identifier."; }, ); sub new { my $class = shift; my $self = bless {}, $class; } sub schema { return { openapi => "3.1.0", info => { title => 'Peace API', version => '0.0.1', license => { name => 'AGPLv3', url => 'https://www.gnu.org/licenses/agpl-3.0.en.html', } }, paths => { developer(), } }; } { my $validator = validation_for( params => { type => { type => Str }, } ); sub _type_to_type_tiny { my $self = shift; my %params = $validator->(@_); my ($type) = $params{type}; return Str if $type eq 'string'; return Int if $type eq 'integer'; die 'No such type declared.'; } } { my $validator = validation_for( params => { format => { type => Str }, } ); sub _format_to_type_tiny { my $self = shift; my %params = $validator->(@_); my ($format) = $params{format}; return $EMAIL_VALIDATOR if $format eq 'email'; return $DEVELOPER_IDENTIFIER_VALIDATOR if $format eq 'developer_identifier'; die "No such format declared."; } } sub developer { return ( "/developer" => { summary => "Allows to work with developers.", post => developer_post(), }, "/developer/{identifier}/application" => { summary => "Allows to work with applications with developer's permissions.", post => developer_application_post(), } ); } { my $validator = validation_for( params => { json => { type => HashRef }, spec => { type => HashRef }, headers => { type => HashRef }, stash => { type => HashRef }, } ); sub validate_request { my $self = shift; my %params = $validator->(@_); my $json = $params{json}; my $spec = $params{spec}; my $headers = $params{headers}; my $stash = $params{stash}; my $spec_parameters = $spec->{parameters}; my $validated_json_keys = {}; for my $parameter_spec (@$spec_parameters) { $self->_validate_parameter( parameter_spec => $parameter_spec, json => $json, headers => $headers, stash => $stash, validated_json_keys => $validated_json_keys, ); } ## Avoiding fancy exploits. my @keys_to_remove = grep { !defined $validated_json_keys->{$_} } keys %$json; for my $key (@keys_to_remove) { delete $json->{$key}; } } } { my $validator = validation_for( params => { parameter_spec => { type => HashRef }, json => { type => HashRef }, headers => { type => HashRef }, stash => { type => HashRef }, validated_json_keys => { type => HashRef }, } ); sub _validate_parameter { my $self = shift; my %params = $validator->(@_); my ( $parameter_spec, $json, $headers, $stash, $validated_json_keys ) = @params{ qw/ parameter_spec json headers stash validated_json_keys/ }; my $name = $parameter_spec->{name}; my $schema = $parameter_spec->{schema}; my $required = $parameter_spec->{required}; my $in = $parameter_spec->{in}; my $type = $schema->{type}; my $format = $schema->{format}; my $pattern = $schema->{pattern}; my $value; if ( $in eq 'query' ) { $value = $json->{$name}; $validated_json_keys->{$name} = 1; } print Data::Dumper::Dumper $headers; $value = $headers->{$name} if $in eq 'header'; $value = $stash->{$name} if $in eq 'path'; if ( !defined $value ) { die "$name is required." if $required; return; } unless ( $self->_type_to_type_tiny( type => $type )->check($value) ) { die "$value is not $type."; } if ( defined $format ) { my $type_format = $self->_format_to_type_tiny( format => $format ); unless ( $type_format->check($value) ) { die "$value is not $format."; } } if ( defined $pattern ) { die "$value doesn\'t match $pattern." if $value !~ /^$pattern$/; } } } sub developer_application_post { return { summary => 'Creates a application.', parameters => [ { name => 'identifier', description => 'Developer\'s email or uuid.', required => 1, in => 'path', schema => { type => "string", format => "developer_identifier", } }, { name => 'api_key', description => '', required => 1, in => 'header', schema => { type => "string", } }, { name => 'name', description => 'Project\'s name.', required => 1, in => 'query', schema => { type => "string", } }, { name => 'description', required => 1, in => 'query', description => 'Application\'s description.', schema => { type => "string", } }, { name => 'url', required => 1, in => 'query', description => 'Project\'s url.', pattern => '^https?://', schema => { type => "string", } }, { name => 'price', required => 1, in => 'query', description => 'Price in dollar cents.', schema => { type => "integer", } }, { name => 'git_repo', required => 1, in => 'query', description => 'Project\'s git repository.', schema => { type => "string", pattern => 'https?://.*' } }, { name => 'app_id', required => 1, in => 'query', description => 'Project\'s application id in flatpak format.', schema => { type => "string", } }, { name => 'flatpak_builder_file', required => 1, in => 'query', description => 'Project\'s path to the flatpak builder file.', schema => { type => "string", } }, { name => 'flatpak_repo', required => 1, in => 'query', description => 'Flatpak repo to be attached to the .flatpak file.', schema => { type => "string", } } ] }; } sub developer_post { return { summary => 'Creates a developer', parameters => [ { name => 'secret', required => 1, in => 'query', description => 'Access credential for Authenticate latter as the created developer.', schema => { type => "string", # Bcrypt limit is 72 characters. pattern => ".{10,72}" } }, { name => 'name', required => 1, in => 'query', description => 'Developer\'s real name', schema => { type => "string", } }, { name => 'surname', required => 1, in => 'query', description => 'Developer\'s real surname', schema => { type => "string", } }, { name => 'surname', required => 1, in => 'query', description => 'Developer\'s real surname', schema => { type => "string", } }, { name => 'email', required => 1, in => 'query', description => 'Developer\'s email', schema => { type => "string", format => 'email', } }, { name => 'country', description => 'Developer\'s country in iso3166format', required => 1, in => 'query', schema => { type => "string", pattern => '[A-Z]{2}', } } ] }; } 1; =encoding utf8 =head1 NAME Peace::Swagger - OpenAPI definitions for the Peace API. =head1 SYNOPSIS my $swagger = Peace::Swagger->new; my $spec = $swagger->schema; =head1 DESCRIPTION This module aims to help in the programmatic description of all API endpoints in a way that makes possible to reuse those schemas in the code and in the documentation. =head1 INSTANCE METHODS Peace::Swagger implements the following instance methods: =head2 new my $swagger = Peace::Swagger->new; Instances a Peace::Swagger. =head1 METHODS Peace::Swagger implements the following methods: =head2 schema my $schema = $swagger->schema; Returns the complete openapi schema. =head2 validate_request $swagger->validate_request(json => $json, spec => $spec); Validates the spec for a specific endpoint say developer_post against the json got from the user and dies if the check is unsuccesful. =head2 developer my $developer_schema = $swagger->developer; Returns the schemas associated with the L object. =head2 developer_post my $developer_post = $swagger->developer_post; Returns the schema of the post request to the /developer enpoint. =head2 developer_application_post my $developer_application_post = $swagger->developer_application_post; Returns the schema of the post request to the /developer/:identifier/application endpoint. =cut