File Coverage

blib/lib/Language/FormulaEngine.pm
Criterion Covered Total %
statement 29 29 100.0
branch 14 24 58.3
condition 6 12 50.0
subroutine 11 11 100.0
pod 2 3 66.6
total 62 79 78.4


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