File Coverage

blib/lib/Language/FormulaEngine.pm
Criterion Covered Total %
statement 32 35 91.4
branch 15 28 53.5
condition 6 12 50.0
subroutine 12 12 100.0
pod 3 4 75.0
total 68 91 74.7


line stmt bran cond sub pod time code
1             package Language::FormulaEngine;
2 7     7   1634487 use Moo;
  7         77197  
  7         35  
3 7     7   10252 use Carp;
  7         17  
  7         385  
4 7     7   1567 use Try::Tiny;
  7         3919  
  7         349  
5 7     7   46 use Module::Runtime 'require_module';
  7         15  
  7         35  
6              
7             # ABSTRACT: Parser/Interpreter/Compiler for simple spreadsheet formula language
8             our $VERSION = '0.08'; # VERSION
9              
10              
11             has parser => (
12             is => 'lazy',
13       7     builder => sub {},
14             coerce => sub { _coerce_instance($_[0], 'parse', 'Language::FormulaEngine::Parser') }
15             );
16             has namespace => (
17             is => 'lazy',
18       4     builder => sub {},
19             coerce => sub { _coerce_instance($_[0], 'get_function', 'Language::FormulaEngine::Namespace::Default') },
20             trigger => sub { my ($self, $val)= @_; $self->compiler->namespace($val) },
21             );
22             has compiler => (
23             is => 'lazy',
24       6     builder => sub {},
25             coerce => sub { _coerce_instance($_[0], 'compile', 'Language::FormulaEngine::Compiler') }
26             );
27              
28             sub BUILD {
29 7     7 0 14530 my $self= shift;
30 7         173 $self->compiler->namespace($self->namespace);
31             }
32              
33             sub _coerce_instance {
34 21     21   103 my ($thing, $req_method, $default_class)= @_;
35 21 100 100     163 return $thing if ref $thing and ref($thing)->can($req_method);
36            
37             my $class= !defined $thing? $default_class
38 20 0 66     86 : ref $thing eq 'HASH'? $thing->{CLASS} || $default_class
    0 0        
    50          
    100          
39             : ref $thing? $default_class
40             : ($req_method eq 'get_function' && $thing =~ /^[0-9]+$/)? "Language::FormulaEngine::Namespace::Default::V$thing"
41             : $thing;
42 20 100       268 require_module($class)
43             unless $class->can($req_method);
44            
45 20 50       127 my @args= !ref $thing? ()
    100          
46             : (ref $thing eq 'ARRAY')? @$thing
47             : $thing;
48 20         233 return $class->new(@args);
49             }
50              
51              
52             sub parse {
53 2     2 1 12926 my ($self, $text, $error_ref)= @_;
54 2 50       61 unless ($self->parser->parse($text)) {
55 0 0       0 die $self->parser->error unless $error_ref;
56 0         0 $$error_ref= $self->parser->error;
57 0         0 return undef;
58             }
59 2         54 return Language::FormulaEngine::Formula->new(
60             engine => $self,
61             orig_text => $text,
62             parse_tree => $self->parser->parse_tree,
63             functions => $self->parser->functions,
64             symbols => $self->parser->symbols,
65             );
66             }
67              
68              
69             sub evaluate {
70 126     126 1 784244 my ($self, $text, $vars)= @_;
71 126 50       3226 $self->parser->parse($text)
72             or die $self->parser->error;
73 126         2677 my $ns= $self->namespace;
74 126 50 33     1668 $ns= $ns->clone_and_merge(variables => $vars) if $vars && %$vars;
75 126         4991 return $self->parser->parse_tree->evaluate($ns);
76             }
77              
78              
79             sub compile {
80 129     129 1 19392 my ($self, $text)= @_;
81 129 50       2886 $self->parser->parse($text)
82             or die $self->parser->error;
83 129         2729 $self->compiler->namespace($self->namespace);
84 129 50       2078 $self->compiler->compile($self->parser->parse_tree)
85             or die $self->compiler->error;
86             }
87              
88              
89             require Language::FormulaEngine::Formula;
90             1;
91              
92             __END__
93              
94             =pod
95              
96             =encoding UTF-8
97              
98             =head1 NAME
99              
100             Language::FormulaEngine - Parser/Interpreter/Compiler for simple spreadsheet formula language
101              
102             =head1 VERSION
103              
104             version 0.08
105              
106             =head1 SYNOPSIS
107              
108             my $vars= { foo => 1, bar => 3.14159265358979, baz => 42 };
109            
110             my $engine= Language::FormulaEngine->new();
111             $engine->evaluate( 'if(foo, round(bar, 3), baz*100)', $vars );
112            
113             # or for more speed on repeat evaluations
114             my $formula= $engine->compile( 'if(foo, round(bar, 3), baz*100)' );
115             print $formula->($vars);
116            
117            
118             package MyNamespace {
119             use Moo;
120             extends 'Language::FormulaEngine::Namespace::Default';
121             sub fn_customfunc { print "arguments are ".join(', ', @_)."\n"; }
122             };
123             my $engine= Language::FormulaEngine->new(namespace => MyNamespace->new);
124             my $formula= $engine->compile( 'CustomFunc(baz,2,3)' );
125             $formula->($vars); # prints "arguments are 42, 2, 3\n"
126              
127             =head1 DESCRIPTION
128              
129             This set of modules implement a parser, evaluator, and optional code generator for a simple
130             expression language similar to those used in spreadsheets.
131             The intent of this module is to help you add customizable behavior to your applications that an
132             "office power-user" can quickly learn and use, while also not opening up security holes in your
133             application.
134              
135             In a typical business application, there will always be another few use cases that the customer
136             didn't know about or think to tell you about, and adding support for these use cases can result
137             in a never-ending expansion of options and chekboxes and dropdowns, and a lot of time spent
138             deciding the logical way for them to all interact.
139             One way to solve this is to provide some scripting support for the customer to use. However,
140             you want to make the language easy to learn, "nerfed" enough for them to use safely, and
141             prevent security vulnerabilities. The challenge is finding a language that they find familiar,
142             that is easy to write correct programs with, and that dosn't expose any peice of the system
143             that you didn't intend to expose. I chose "spreadsheet formula language" for a project back in
144             2012 and it worked out really well, so I decided to give it a makeover and publish it.
145              
146             The default syntax is pure-functional, in that each operation has exactly one return value, and
147             cannot modify variables; in fact none of the default functions have any side-effects. There is
148             no assignment, looping, or nested data structures. The language does have a bit of a Perl twist
149             to it's semantics, like throwing exceptions rather than returning C<< #VALUE! >>, fluidly
150             interpreting values as strings or integers, and using L<DateTime> instead of days-since-1900
151             numbers for dates, but most users probably won't mind. And, all these decisions are fairly
152             easy to change with a subclass.
153             (but if you want big changes, you should L<review your options|/"SEE ALSO"> to make sure you're
154             starting with the right module.)
155              
156             The language is written with security in mind, and (until you start making changes)
157             should be safe for most uses, since the functional design promotes O(1) complexity
158             and shouldn't have side effects on the data structures you expose to the user.
159             The optional L</compile> method does use C<eval> though, so you should do an audit for
160             yourself if you plan to use it where security is a concern.
161              
162             B<Features:>
163              
164             =over
165              
166             =item *
167              
168             Standard design with scanner/parser, syntax tree, namespaces, and compiler.
169              
170             =item *
171              
172             Can compile to perl coderefs for fast repeated execution
173              
174             =item *
175              
176             Provides metadata about what it compiled
177              
178             =item *
179              
180             Designed for extensibility
181              
182             =item *
183              
184             Light-weight, few dependencies, clean code
185              
186             =item *
187              
188             Recursive-descent parse, which is easier to work with and gives helpful error messages,
189             though could get a bit slow if you extend the grammar too much.
190             (for simple grammars like this, it's pretty fast)
191              
192             =back
193              
194             =head1 ATTRIBUTES
195              
196             =head2 parser
197              
198             A parser for the language. Responsible for tokenizing the input and building the
199             parse tree.
200              
201             Defaults to an instance of L<Language::FormulaEngine::Parser>. You can initialize this
202             attribute with an object instance, a class name, or arguments for the default parser.
203              
204             =head2 namespace
205              
206             A namespace for looking up functions or constants. Also determines some aspects of how the
207             language works, and responsible for providing the perl code when compiling expressions.
208              
209             Defaults to an instance of L<Language::FormulaEngine::Namespace::Default>.
210             You can initialize this with an object instance, class name, version number for the default
211             namespace, or hashref of arguments for the constructor.
212              
213             =head2 compiler
214              
215             A compiler for the parse tree. Responsible for generating Perl coderefs, though the Namespace
216             does most of the perl code generation.
217              
218             Defaults to an instance of L<Language::FormulaEngine::Compiler>.
219             You can initialize this attribute with a class instance, a class name, or arguments for the
220             default compiler.
221              
222             =head1 METHODS
223              
224             =head2 parse
225              
226             my $formula= $fe->parse( $formula_text, \$error );
227              
228             Return a L<Language::FormulaEngine::Formula|Formula object> representing the expression.
229             Dies if it can't parse the expression, unless you supply C<$error> then the error is
230             stores in that scalarref and the methods returns C<undef>.
231              
232             =head2 evaluate
233              
234             my $value= $fe->evaluate( $formula_text, \%variables );
235              
236             This method creates a new namespace from the default plus the supplied variables, parses the
237             formula, then evaluates it in a recursive interpreted manner, returning the result. Exceptions
238             may be thrown during parsing or execution.
239              
240             =head2 compile
241              
242             my $coderef= $fe->compile( $formula_text );
243              
244             Parses and then compiles the C<$formula_text>, returning a coderef. Exceptions may be thrown
245             during parsing or execution.
246              
247             =head1 CUSTOMIZING THE LANGUAGE
248              
249             The module is called "FormulaEngine" in part because it is designed to be customized.
250             The functions are easy to extend, the variables are somewhat easy to extend, the compilation
251             can be extended after a little study of the API, and the grammar itself can be extended with
252             some effort.
253              
254             If you are trying to addd I<lots> of functionality, you might be starting with the wrong module.
255             See the notes in the L</"SEE ALSO"> section.
256              
257             =head2 Adding Functions
258              
259             The easiest thing to extend is the namespace of available functions. Just subclass
260             L<Language::FormulaEngine::Namespace> and add the functions you want starting with the prefix
261             C<fn_>.
262              
263             =head2 Complex Variables
264              
265             The default implementation of Namespace requires all variables to be stored in a single hashref.
266             This default is safe and fast. If you want to traverse nested data structures or call methods,
267             you also need to subclass L<Language::FormulaEngine::Namespace/get_value>.
268              
269             =head2 Changing Semantics
270              
271             The namespace is also in control of the behavior of the functions and operators (which are
272             themselves just functions). It controls both the way they are evaluated and the perl code they
273             generate if compiled.
274              
275             =head2 Adding New Operators
276              
277             If you want to make small changes to the grammar, such as adding new prefix/suffix/infix
278             operators, this can be accomplished fairly easily by subclassing the Parser. The parser just
279             returns trees of functions, and if you look at the pattern used in the recursive descent
280             C<parse_*> methods it should be easy to add some new ones.
281              
282             =head2 Bigger Grammar Changes
283              
284             Any customization involving bigger changes to the grammar, like adding assignments or multi-
285             statement blocks or map/reduce, would require a bigger rewrite. Consider starting with
286             a different more powerful parsing system for that.
287              
288             =head1 SEE ALSO
289              
290             =over
291              
292             =item L<Language::Expr>
293              
294             A bigger more featureful expression language; perl-like syntax and data structures.
295             Also much more complicated and harder to customize.
296             Can also compile to Javascript!
297              
298             =item L<Math::Expression>
299              
300             General-purpose language, including variable assignment and loops, arrays,
301             and with full attention to security. However, grammar is not customizable at all,
302             and math-centric.
303              
304             =item L<Math::Expression::Evaluator>
305              
306             Very similar to this module, but no string support and not very customizable.
307             Supports assignments, and compilation.
308              
309             =item L<Math::Expr>
310              
311             Similar expression parser, but without string support.
312             Supports arbitrary customization of operators.
313             Not suitable for un-trusted strings, according to BUGS documentation.
314              
315             =back
316              
317             =head1 AUTHOR
318              
319             Michael Conrad <mconrad@intellitree.com>
320              
321             =head1 COPYRIGHT AND LICENSE
322              
323             This software is copyright (c) 2023 by Michael Conrad, IntelliTree Solutions llc.
324              
325             This is free software; you can redistribute it and/or modify it under
326             the same terms as the Perl 5 programming language system itself.
327              
328             =cut