Bug 37762: REST API: Convert XML payload to JSON for swagger definition validation
Convert JSON response to XML if header content_type: application/xml Signed-off-by: Jason Robb <jrobb@sekls.org> Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
parent
2a1fa8d091
commit
55fa4609fa
236
Koha/REST/V1.pm
236
Koha/REST/V1.pm
|
|
@ -27,7 +27,8 @@ use Mojolicious::Plugin::OAuth2;
|
|||
use JSON::Validator::Schema::OpenAPIv2;
|
||||
|
||||
use Try::Tiny qw( catch try );
|
||||
use JSON qw( decode_json );
|
||||
use JSON qw( encode_json decode_json );
|
||||
use XML::LibXML;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
|
|
@ -70,12 +71,55 @@ sub startup {
|
|||
Koha::Caches->flush_L1_caches();
|
||||
Koha::Cache::Memory::Lite->flush();
|
||||
|
||||
# Convert XML payload to JSON and validate against schema
|
||||
if ( $c->req->headers->content_type && $c->req->headers->content_type =~ /application\/xml/ ) {
|
||||
try {
|
||||
my $xml = $c->req->body;
|
||||
|
||||
my $parser = XML::LibXML->new();
|
||||
my $doc = $parser->parse_string($xml);
|
||||
my $root = $doc->documentElement();
|
||||
my $json = $self->parse_xml($root);
|
||||
|
||||
$c->req->body( JSON::encode_json($json) );
|
||||
|
||||
unless ( $self->validate_json_payload( $c, $json ) ) {
|
||||
return $c->render( status => 400, json => { error => 'Invalid JSON payload' } );
|
||||
}
|
||||
} catch {
|
||||
|
||||
# If the request body is already JSON, don't try to parse it as XML
|
||||
my $json = $c->req->body;
|
||||
unless ( $self->validate_json_payload( $c, $json ) ) {
|
||||
return $c->render( status => 400, json => { error => 'Invalid JSON payload' } );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $next->();
|
||||
}
|
||||
);
|
||||
|
||||
$self->hook(
|
||||
after_dispatch => sub {
|
||||
my ($c) = @_;
|
||||
|
||||
# Check if the response header content_type is application/xml
|
||||
if ( $c->res->headers->content_type && $c->res->headers->content_type eq 'application/xml' ) {
|
||||
|
||||
# Convert JSON to XML
|
||||
my $xml = to_xml( decode_json( $c->res->body ) );
|
||||
|
||||
$c->res->body($xml);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
# Force charset=utf8 in Content-Type header for JSON responses
|
||||
$self->types->type( json => 'application/json; charset=utf8' );
|
||||
$self->types->type( xml => 'application/xml' );
|
||||
|
||||
# MARC-related types
|
||||
$self->types->type( marcxml => 'application/marcxml+xml' );
|
||||
|
|
@ -181,4 +225,194 @@ sub startup {
|
|||
$self->plugin( 'Mojolicious::Plugin::OAuth2' => $oauth_configuration );
|
||||
}
|
||||
|
||||
=head3 to_xml
|
||||
|
||||
my $xml_string = to_xml( $json_hashref );
|
||||
|
||||
Converts a JSON-like hashref into an XML string.
|
||||
|
||||
=cut
|
||||
|
||||
sub to_xml {
|
||||
my $json = shift;
|
||||
my $xml = XML::LibXML::Document->new( '1.0', 'UTF-8' );
|
||||
|
||||
my $root_key = ( keys %$json )[0];
|
||||
my $root = $xml->createElement($root_key);
|
||||
$xml->setDocumentElement($root); # Set the root element
|
||||
|
||||
_json_to_xml( $json->{$root_key}, $root, $xml );
|
||||
|
||||
return $xml->toString;
|
||||
}
|
||||
|
||||
=head3 _json_to_xml
|
||||
|
||||
_json_to_xml( $data, $parent_node, $xml_doc );
|
||||
|
||||
Internal recursive helper for C<to_xml> to build XML elements from nested Perl structures.
|
||||
|
||||
=cut
|
||||
|
||||
sub _json_to_xml {
|
||||
my ( $json, $parent, $doc ) = @_;
|
||||
|
||||
if ( ref $json eq 'HASH' ) {
|
||||
foreach my $key ( keys %$json ) {
|
||||
my $value = $json->{$key};
|
||||
if ( ref $value eq 'HASH' ) {
|
||||
my $elem = $doc->createElement($key);
|
||||
$parent->appendChild($elem);
|
||||
_json_to_xml( $value, $elem, $doc );
|
||||
} elsif ( ref $value eq 'ARRAY' ) {
|
||||
foreach my $item (@$value) {
|
||||
my $elem = $doc->createElement($key);
|
||||
$parent->appendChild($elem);
|
||||
_json_to_xml( $item, $elem, $doc );
|
||||
}
|
||||
} else {
|
||||
my $elem = $doc->createElement($key);
|
||||
$elem->appendText($value);
|
||||
$parent->appendChild($elem);
|
||||
}
|
||||
}
|
||||
} elsif ( ref $json eq 'ARRAY' ) {
|
||||
foreach my $item (@$json) {
|
||||
my $elem = $doc->createElement('item');
|
||||
$parent->appendChild($elem);
|
||||
_json_to_xml( $item, $elem, $doc );
|
||||
}
|
||||
} else {
|
||||
my $elem = $doc->createElement('value');
|
||||
$elem->appendText($json);
|
||||
$parent->appendChild($elem);
|
||||
}
|
||||
}
|
||||
|
||||
=head3 validate_json_payload
|
||||
|
||||
my $is_valid = $self->validate_json_payload( $c, $payload );
|
||||
|
||||
Validates a JSON payload against the REST API OpenAPI/Swagger schema.
|
||||
Returns C<1> if valid, C<0> if there are errors.
|
||||
|
||||
=cut
|
||||
|
||||
sub validate_json_payload {
|
||||
my ( $self, $c, $payload ) = @_;
|
||||
|
||||
# Load the OpenAPI schema
|
||||
my $spec_file = $self->home->rel_file("api/v1/swagger/swagger_bundle.json");
|
||||
if ( !-f $spec_file ) {
|
||||
$spec_file = $self->home->rel_file("api/v1/swagger/swagger.yaml");
|
||||
}
|
||||
|
||||
my $schema = JSON::Validator::Schema::OpenAPIv2->new;
|
||||
$schema->resolve($spec_file);
|
||||
|
||||
my $spec = $schema->bundle->data;
|
||||
|
||||
# Validate the JSON payload against the schema
|
||||
my $validator = JSON::Validator->new;
|
||||
$validator->schema($spec);
|
||||
|
||||
my $errors = $validator->validate($payload);
|
||||
|
||||
# Print the validation errors
|
||||
if ($errors) {
|
||||
|
||||
#TODO: Do something here?
|
||||
}
|
||||
|
||||
# Return 1 if the payload is valid, 0 otherwise
|
||||
return $errors ? 0 : 1;
|
||||
}
|
||||
|
||||
=head3 parse_xml
|
||||
|
||||
my $hashref = $self->parse_xml( $xml_node );
|
||||
|
||||
Converts an C<XML::LibXML::Node> into a Perl hashref, using the OpenAPI schema definitions to correctly identify and cast array properties.
|
||||
|
||||
=cut
|
||||
|
||||
sub parse_xml {
|
||||
my ( $self, $node ) = @_;
|
||||
my $hash = {};
|
||||
|
||||
my $spec_file = $self->home->rel_file("api/v1/swagger/swagger_bundle.json");
|
||||
if ( !-f $spec_file ) {
|
||||
$spec_file = $self->home->rel_file("api/v1/swagger/swagger.yaml");
|
||||
}
|
||||
|
||||
my $schema = JSON::Validator::Schema::OpenAPIv2->new($spec_file);
|
||||
|
||||
$self->_parse_node( $node, $hash, 0, $schema->data->{definitions} );
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
=head3 _parse_node
|
||||
|
||||
$self->_parse_node( $node, $hash, $is_array, $schema_definitions );
|
||||
|
||||
Internal recursive helper for C<parse_xml> to populate a Perl hashref from XML elements.
|
||||
|
||||
=cut
|
||||
|
||||
sub _parse_node {
|
||||
my ( $self, $node, $hash, $is_array, $schema_definitions ) = @_;
|
||||
|
||||
my $name = $node->localName();
|
||||
my $properties = $schema_definitions->{$name}->{properties};
|
||||
|
||||
my @array_properties;
|
||||
foreach my $key ( keys %$properties ) {
|
||||
my $property = $properties->{$key};
|
||||
push @array_properties, $key if $property->{type} && $property->{type} eq 'array';
|
||||
}
|
||||
|
||||
if ( $node->hasChildNodes() ) {
|
||||
my $child_hash = {};
|
||||
foreach my $child ( $node->childNodes() ) {
|
||||
if ( $child->nodeType() == 1 ) { # 1 is the node type for elements
|
||||
if ( grep { $_ eq $child->localName() } @array_properties ) {
|
||||
$self->_parse_node( $child, $child_hash, 1, $schema_definitions );
|
||||
} else {
|
||||
$self->_parse_node( $child, $child_hash, 0, $schema_definitions );
|
||||
}
|
||||
}
|
||||
}
|
||||
if (%$child_hash) {
|
||||
if ( $is_array || exists $hash->{$name} ) {
|
||||
if ( ref( $hash->{$name} ) eq 'ARRAY' ) {
|
||||
push @{ $hash->{$name} }, $child_hash;
|
||||
} else {
|
||||
$hash->{$name} = [$child_hash];
|
||||
}
|
||||
} else {
|
||||
$hash->{$name} = $child_hash;
|
||||
}
|
||||
} else {
|
||||
my $text = $node->textContent();
|
||||
$text =~ s/^\s+//; # remove leading whitespace
|
||||
$text =~ s/\s+$//; # remove trailing whitespace
|
||||
if ( $is_array || exists $hash->{$name} ) {
|
||||
if ( ref( $hash->{$name} ) eq 'ARRAY' ) {
|
||||
push @{ $hash->{$name} }, $text;
|
||||
} else {
|
||||
$hash->{$name} = [$text];
|
||||
}
|
||||
} else {
|
||||
$hash->{$name} = $text;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
my $text = $node->textContent();
|
||||
$text =~ s/^\s+//; # remove leading whitespace
|
||||
$text =~ s/\s+$//; # remove trailing whitespace
|
||||
$hash->{$name} = $text;
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue