From d91f191526e98c205cebf5a2b7d1eae504f10420 Mon Sep 17 00:00:00 2001 From: Tomas Cohen Arazi Date: Tue, 29 Nov 2022 15:14:51 -0300 Subject: [PATCH] Bug 32370: Provide a generic set of tools for JSON fields This patch introduces a new class for extending Koha::Object-based classes so they have reusable ways to deal with JSON encoded fields. As documented in the POD, classes can now ```perl use base qw(Koha::Object Koha::Object::JSONFields); ``` and will have the following methods available: * decode_json_field * set_encoded_json_field For convenience, the Koha::Auth::Identity::Provider class is tweaked to make use of this, and also to prove the behavior is correct (expected results, exceptions, encoding, etc). To test: 1. Apply this patch 2. Run: $ ktd --shell k$ prove t/db_dependent/Koha/Object/JSONField.t => SUCCESS: Tests pass! 3. Sign off :-D Signed-off-by: Nick Clemens --- Koha/Object/JSONFields.pm | 120 +++++++++++++++++++++++++ t/db_dependent/Koha/Object/JSONField.t | 63 +++++++------ 2 files changed, 155 insertions(+), 28 deletions(-) create mode 100644 Koha/Object/JSONFields.pm diff --git a/Koha/Object/JSONFields.pm b/Koha/Object/JSONFields.pm new file mode 100644 index 00000000000..a13c2ad6d33 --- /dev/null +++ b/Koha/Object/JSONFields.pm @@ -0,0 +1,120 @@ +package Koha::Object::JSONFields; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use JSON; +use Try::Tiny; + +use Koha::Exceptions; +use Koha::Exceptions::Object; + +=head1 NAME + +Koha::Object::JSONFields - Class that adds JSON field manipulation helper methods +for Koha::Object-derived classes + +=head1 SYNOPSIS + + use base qw(Koha::Object Koha::Object::JSONFields); + my $object = Koha::Object->new({ property1 => $property1, property2 => $property2, etc... } ); + my $field_name_hashref = $object->decode_json_field({ field => 'field_name' }); + $object->encode_json_field({ field => 'field_name', data => $data }); + +=head1 API + +=head2 Class methods + +=head3 decode_json_field + + my $hashref = $object->decode_json_field({ field => 'field_name' }); + +Returns a data structure representing the JSON-decoded value of field I. + +=cut + +sub decode_json_field { + my ( $self, $params ) = @_; + + Koha::Exceptions::MissingParameter->throw( parameter => 'field' ) + unless defined $params->{field}; + + my $field = $params->{field}; + + return try { + $self->$field ? $self->_json->decode( $self->$field ) : undef; + } catch { + Koha::Exceptions::Object::BadValue->throw("Error reading JSON data: $_"); + }; +} + +=head3 set_encoded_json_field + + $object->set_encoded_json_field( + { data => $data, + field => 'field_name', + } + ); + +Sets a JSON string encoded representation of I<$data> into the object's I +attribute. + +=cut + +sub set_encoded_json_field { + my ( $self, $params ) = @_; + + Koha::Exceptions::MissingParameter->throw( parameter => 'field' ) + unless defined $params->{field}; + + Koha::Exceptions::MissingParameter->throw( parameter => 'data' ) + unless exists $params->{data}; + + my $field = $params->{field}; + my $data = $params->{data}; + + return try { + $self->$field( $data ? $self->_json->encode($data) : undef ); + } catch { + Koha::Exceptions::Object::BadValue->throw("Error reading JSON data: $_"); + }; +} + +=head2 Internal methods + +=head3 _json + + my $JSON_object = $self->_json; + +Returns a JSON object with utf8 disabled. Encoding to UTF-8 should be +done later. + +=cut + +sub _json { + my ($self) = @_; + $self->{_json} //= JSON->new->utf8(0); + return $self->{_json}; +} + +=head1 AUTHOR + +Tomas Cohen Arazi, Etomascohen@theke.ioE + +=cut + +1; diff --git a/t/db_dependent/Koha/Object/JSONField.t b/t/db_dependent/Koha/Object/JSONField.t index 90f87844d90..41b58f88cae 100755 --- a/t/db_dependent/Koha/Object/JSONField.t +++ b/t/db_dependent/Koha/Object/JSONField.t @@ -37,66 +37,73 @@ subtest 'decode_json_field() and set_encoded_json_field() tests' => sub { $schema->storage->txn_begin; - my $idp = $builder->build_object({ class => 'Koha::Auth::Identity::Providers' }); + my $idp = $builder->build_object( { class => 'Koha::Auth::Identity::Providers' } ); my $data = { some => 'data' }; - $idp->set_encoded_json_field({ data => $data, field => 'config' }); + $idp->set_encoded_json_field( { data => $data, field => 'config' } ); - is_deeply( $idp->_json->decode($idp->config), $data, 'decode what we sent' ); - is_deeply( $idp->decode_json_field({ field => 'config' }), $data, 'check with decode_json_field' ); + is_deeply( $idp->_json->decode( $idp->config ), $data, 'decode what we sent' ); + is_deeply( $idp->decode_json_field( { field => 'config' } ), $data, 'check with decode_json_field' ); # Let's get some Unicode stuff into the game $data = { favorite_Chinese => [ '葑', '癱' ], latin_dancing => [ '¢', '¥', 'á', 'û' ] }; - $idp->set_encoded_json_field({ data => $data, field => 'config' })->store; + $idp->set_encoded_json_field( { data => $data, field => 'config' } )->store; + + $idp->discard_changes; # refresh + is_deeply( $idp->decode_json_field( { field => 'config' } ), $data, 'Deep compare with Unicode data' ); - $idp->discard_changes; # refresh - is_deeply( $idp->decode_json_field({ field => 'config' }), $data, 'Deep compare with Unicode data' ); # To convince you even more - is( ord( $idp->decode_json_field({ field => 'config' })->{favorite_Chinese}->[0] ), 33873, 'We still found Unicode \x8451' ); - is( ord( $idp->decode_json_field({ field => 'config' })->{latin_dancing}->[0] ), 162, 'We still found the equivalent of Unicode \x00A2' ); + is( + ord( $idp->decode_json_field( { field => 'config' } )->{favorite_Chinese}->[0] ), 33873, + 'We still found Unicode \x8451' + ); + is( + ord( $idp->decode_json_field( { field => 'config' } )->{latin_dancing}->[0] ), 162, + 'We still found the equivalent of Unicode \x00A2' + ); # Testing with sending encoded data (which we normally shouldn't do) my $utf8_data; foreach my $k ( 'favorite_Chinese', 'latin_dancing' ) { - foreach my $c ( @{$data->{$k}} ) { - push @{$utf8_data->{$k}}, Encode::encode('UTF-8', $c); + foreach my $c ( @{ $data->{$k} } ) { + push @{ $utf8_data->{$k} }, Encode::encode( 'UTF-8', $c ); } } - $idp->set_encoded_json_field({ data => $utf8_data, field => 'config' })->store; - $idp->discard_changes; # refresh - is_deeply( $idp->decode_json_field({ field => 'config' }), $utf8_data, 'Deep compare with utf8_data' ); + $idp->set_encoded_json_field( { data => $utf8_data, field => 'config' } )->store; + $idp->discard_changes; # refresh + is_deeply( $idp->decode_json_field( { field => 'config' } ), $utf8_data, 'Deep compare with utf8_data' ); + # Need more evidence? - is( ord( $idp->decode_json_field({ field => 'config' })->{favorite_Chinese}->[0] ), 232, 'We still found a UTF8 encoded byte' ); # ord does not need substr here + is( + ord( $idp->decode_json_field( { field => 'config' } )->{favorite_Chinese}->[0] ), 232, + 'We still found a UTF8 encoded byte' + ); # ord does not need substr here # pathological use cases - throws_ok - { $idp->set_encoded_json_field({ data => $data }); } - 'Koha::Exceptions::MissingParameter', + throws_ok { $idp->set_encoded_json_field( { data => $data } ); } + 'Koha::Exceptions::MissingParameter', 'Exception thrown on missing parameter'; is( $@->parameter, 'field', 'Correct parameter reported (field)' ); - throws_ok - { $idp->set_encoded_json_field({ data => $data, field => undef }); } - 'Koha::Exceptions::MissingParameter', + throws_ok { $idp->set_encoded_json_field( { data => $data, field => undef } ); } + 'Koha::Exceptions::MissingParameter', 'Exception thrown on missing parameter'; is( $@->parameter, 'field', 'Correct parameter reported (field)' ); - throws_ok - { $idp->set_encoded_json_field({ field => 'something' }); } - 'Koha::Exceptions::MissingParameter', + throws_ok { $idp->set_encoded_json_field( { field => 'something' } ); } + 'Koha::Exceptions::MissingParameter', 'Exception thrown on missing parameter'; is( $@->parameter, 'data', 'Correct parameter reported (data)' ); - $idp->set_encoded_json_field({ data => undef, field => 'config' }); + $idp->set_encoded_json_field( { data => undef, field => 'config' } ); is( $idp->config, undef, 'undef is undef' ); # set invalid data $idp->config('{'); - throws_ok - { $idp->decode_json_field({ field => 'config' }) } - 'Koha::Exceptions::Object::BadValue', + throws_ok { $idp->decode_json_field( { field => 'config' } ) } + 'Koha::Exceptions::Object::BadValue', 'Exception thrown'; like( "$@", qr/Error reading JSON data/ ); -- 2.50.1