File Coverage

blib/lib/Math/Formula/Config/JSON.pm
Criterion Covered Total %
statement 67 69 97.1
branch 29 34 85.2
condition 8 14 57.1
subroutine 13 13 100.0
pod 2 2 100.0
total 119 132 90.1


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Math-Formula version 0.18.
2             # The POD got stripped from this file by OODoc version 3.03.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2023-2025 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11             #oodist: *** DO NOT USE THIS VERSION FOR PRODUCTION ***
12             #oodist: This file contains OODoc-style documentation which will get stripped
13             #oodist: during its release in the distribution. You can use this file for
14             #oodist: testing, however the code of this development version may be broken!
15              
16             package Math::Formula::Config::JSON;{
17             our $VERSION = '0.18';
18             }
19              
20 1     1   414356 use parent 'Math::Formula::Config';
  1         4  
  1         10  
21              
22 1     1   65 use warnings;
  1         3  
  1         81  
23 1     1   9 use strict;
  1         2  
  1         37  
24              
25 1     1   7 use Log::Report qw/math-formula/;
  1         3  
  1         4  
26 1     1   388 use Scalar::Util qw/blessed/;
  1         3  
  1         83  
27 1     1   765 use File::Slurper qw/read_binary/;
  1         4943  
  1         114  
28 1     1   10 use Cpanel::JSON::XS ();
  1         2  
  1         1580  
29              
30             my $json = Cpanel::JSON::XS->new->pretty->utf8->canonical(1);
31              
32             #--------------------
33              
34             #--------------------
35              
36             sub save($%)
37 2     2 1 1009 { my ($self, $context, %args) = @_;
38 2         10 my $name = $context->name;
39              
40 2         7 my $index = $context->_index;
41 2         7 my $tree = $self->_set($index->{attributes});
42 2         6 $tree->{formulas} = $self->_set($index->{formulas});
43              
44 2   66     19 my $fn = $self->path_for($args{filename} || "$name.json");
45 2 50       447 open my $fh, '>:raw', $fn
46             or fault __x"Trying to save context '{name}' to {file}", name => $name, file => $fn;
47              
48 2         181 $fh->print($json->encode($tree));
49 2 50       12276 $fh->close
50             or fault __x"Error on close while saving '{name}' to {file}", name => $name, file => $fn;
51             }
52              
53             sub _set($)
54 4     4   8 { my ($self, $set) = @_;
55 4         7 my %data;
56 4         34 $data{$_ =~ s/^ctx_//r} = $self->_serialize($_, $set->{$_}) for keys %$set;
57 4         13 \%data;
58             }
59              
60             sub _serialize($$)
61 28     28   52 { my ($self, $name, $what) = @_;
62 28         33 my %attrs;
63              
64 28 100 66     138 if(blessed $what && $what->isa('Math::Formula'))
65 18 100       38 { if(my $r = $what->returns) { $attrs{returns} = $r };
  2         5  
66 18         38 $what = $what->expression;
67             }
68              
69 28         38 my $v = '';
70 28 100 66     94 if(blessed $what && $what->isa('Math::Formula::Type'))
    50          
    50          
71             { # strings without quote
72 24 100       130 $v = $what->isa('MF::STRING') ? $what->value
    100          
    100          
    100          
73             : $what->isa('MF::BOOLEAN') ? ($what->value ? Cpanel::JSON::XS::true : Cpanel::JSON::XS::false)
74             : $what->isa('MF::FLOAT') ? $what->value # otherwise JSON writes a string
75             : $what->token;
76             }
77             elsif(ref $what eq 'CODE')
78 0         0 { warning __x"cannot (yet) save CODE, skipped '{name}'", name => $name;
79 0         0 return undef;
80             }
81             elsif(length $what)
82 4         7 { $v = '=' . $what;
83             }
84              
85 28 100       141 if(keys %attrs)
86 2         13 { $v .= '; ' . (join ', ', map "$_='$attrs{$_}'", sort keys %attrs);
87             }
88              
89 28         123 return $v;
90             }
91              
92              
93              
94             sub load($%)
95 1     1 1 1832 { my ($self, $name, %args) = @_;
96 1   33     10 my $fn = $self->path_for($args{filename} || "$name.json");
97              
98 1         5 my $tree = $json->decode(read_binary $fn);
99 1         113 my $formulas = delete $tree->{formulas};
100              
101 1         5 my $attrs = $self->_set_decode($tree);
102 1         3 Math::Formula::Context->new(name => $name, %$attrs,
103             formulas => $self->_set_decode($formulas),
104             );
105             }
106              
107             sub _set_decode($)
108 2     2   5 { my ($self, $set) = @_;
109 2 50       5 $set or return {};
110              
111 2         2 my %forms;
112 2         9 $forms{$_} = $self->_unpack($_, $set->{$_}) for keys %$set;
113 2         16 \%forms;
114             }
115              
116             sub _unpack($$)
117 14     14   31 { my ($self, $name, $encoded) = @_;
118 14         31 my $dummy = Math::Formula->new('dummy', '7');
119              
120 14 100       39 if(ref $encoded eq 'JSON::PP::Boolean')
121 2         9 { return MF::BOOLEAN->new(undef, $encoded);
122             }
123              
124 12 100       46 if($encoded =~ m/^\=(.*?)(?:;\s*(.*))?$/)
125 2   50     13 { my ($expr, $attrs) = ($1, $2 // '');
126 2         14 my %attrs = $attrs =~ m/(\w+)\='([^']+)'/g;
127 2         10 return Math::Formula->new($name, $expr =~ s/\\"/"/gr, %attrs);
128             }
129              
130             # No JSON implementation parses floats and ints cleanly into SV
131             # So, we need to check it by hand. Gladly, ints are converted
132             # to strings again when that was the intention.
133              
134 10 100       115 $encoded =~ qr/^[0-9]+$/ ? MF::INTEGER->new(undef, $encoded + 0)
    100          
135             : $encoded =~ qr/^[0-9][0-9.e+\-]+$/ ? MF::FLOAT->new(undef, $encoded + 0.0)
136             : MF::STRING->new(undef, $encoded);
137             }
138              
139             #--------------------
140              
141             1;