Flavio Poletti

NAME

Plack::Middleware::ReviseEnv - Revise request environment at will

VERSION

This document describes Plack::Middleware::ReviseEnv version 0.004.

Build Status Perl Version Current CPAN version Kwalitee CPAN Testers CPAN Testers Matrix

SYNOPSIS

   use Plack::Middleware::ReviseEnv;

   my $mw = Plack::Middleware::ReviseEnv->new(

      # straight value
      var1 => 'a simple, overriding value',

      # value from %ENV
      var2 => '[% ENV:USER %]',

      # value from other element in $env
      var3 => '[% env:foobar %]',

      # mix and match, values are templates actually
      var4 => 'Hey [% ENV:USER %] this is [% env:var1 %]',

      # to delete an element just "undef" it
      X_REMOVE_ME => undef,

      # overriding is the default behaviour, but you can disable it
      X_FOO => {
         value => 'Get this by default',
         override => 0,
      },

      # the key is a template too!
      '[% ENV:USER %]' => '[% ENV:HOME %]',

      # the "key" can be specified inside, ignoring the "outer" one
      IGNORED_KEY => {
         key => 'THIS IS THE KEY!',
         value => 'whatever',
      },

      # more examples and features below in example with array ref

   );

   # you can also pass the key/value pairs as a hash reference
   # associated to a key named 'revisors'. This is necessary e.g. if
   # you want to set a variable in $env with name 'app', 'opts' or
   # 'revisors'
   my $mw2 = Plack::Middleware::ReviseEnv->new(revisors => \%revisors);

   # when evaluation order or repetition is important... use an array
   # reference for 'revisors'. You can also avoid passing the external
   # key here, and just provide a sequence of hash definitions
   my $mw3 = Plack::Middleware::ReviseEnv->new(
      revisors => [
         KEY => { ... specification ... },

         # by default inexistent/undef inputs are expanded as empty
         # strings.
         {
            key => 'weird',
            value => '[% ENV:HOST %]:[% ENV:UNDEFINED %]',

            # %ENV = (HOST => 'www.example.com'); # no UNDEFINED
            # # weird => 'www.example.com:' # note trailing colon...
         },

         # you can "fail" generating a variable if something is missing,
         # so you can avoid the trailing colon above in two steps:
         {
            key => 'correct_port_spec',
            value => ':[% ENV:PORT %]',
            require_all => 1,

            # %ENV = (); # no PORT
            # # -> no "correct_port_spec" is generated in $env

            # %ENV = (PORT => 8080);
            # # correct_port_spec => ':8080'
         },
         {
            key => 'host_and_port',
            value => '[% ENV:HOST %][% env:correct_port_spec %]',

            # %ENV = (HOST => 'www.example.com'); # no PORT
            # # host_and_port => 'www.example.com'

            # %ENV = (HOST => 'www.example.com', PORT => 8080);
            # # host_and_port => 'www.example.com:8080'
         }

         # the default value is "undef" which ultimately means "do not
         # generate" or "delete if existent". You can set a different
         # one for the key and the value separately
         {
            key         => '[% ENV:USER %]',
            default_key => 'nobody',

            value         => '[% ENV:HOME %]',
            default_value => '/tmp',
         },

         # the default is applied only when the outcome is "undef", but
         # you can extend it to the empty string too. This is useful to
         # obtain the same effect of shell's test [ -z "$VAR" ] which is
         # true both for missing and empty values
         {
            key         => '[% ENV:USER %]',
            default_key => 'nobody',

            value         => '[% ENV:HOME %]',
            default_value => '/tmp',

            empty_as_default => 1,
         }

         # We can revisit the example on host/port and set defaults for
         # the missing variables, using two temporary variables that
         # will be cleared afterwards
         {
            key => '_host',
            value => '[% ENV:HOST %]',
            default_value => 'www.example.com',
            empty_as_default => 1,
         },
         {
            key => '_port',
            value => '[% ENV:PORT %]',
            default_value => '8080',
            empty_as_default => 1,
         },
         host_and_port => '[% env:_host %]:[% env:_port %]',
         _host => undef, # clear temporary variable
         _port => undef, # ditto
      ]
   );

DESCRIPTION

This module allows you to reshape Plack's $env that is passed along to the sequence of apps, taking values from an interpolation of items in %ENV and $env.

At the most basic level, it allows you to get selected values from the environment and override some values in $env accordingly. For example, if you want to use environment variables to configure a reverse proxy setup, you can use the following revisor definitions:

   ...
   'psgi.url_scheme' => '[% ENV:RP_SCHEME %]',
   'HTTP_HOST'       => '[% ENV:RP_HOST   %]',
   'SCRIPT_NAME'     => '[% ENV:RP_PATH   %]',
   ...

This would basically implement the functionality provided by Dancer::Middleware::Rebase (without the strip capabilities).

Value definitions are actually templates with normal text and variables expansions between delimiters. So, the following definition does what you think:

   salutation => 'Hello, [% ENV:USER %], welcome [% ENV:HOME %]',

You are not limited to taking values from the environment and peek into $env too:

   ...
   bar => 'baz', # no expansion in this template, just returns 'bar'
   foo => '[% env:bar %]',
   ...

As you can understand, if you want to peek at other values in $env and these values are generated too, order matters! Take a look at "Ordering Revisors" to avoid being biten by this, but the bottom line is: use the array-reference form and put revisors in the order you want them evaluated.

Defining Revisors

There are multiple ways you can provide the definition of a revisor. Before explaining the details, it's useful to notice that you can invoke the constructor for Plack::Middleware::ReviseEnv in different ways:

   # the "hash" way, where %hash MUST NOT contain the "revisors" key
   my $mwh  = Plack::Middleware::ReviseEnv->new(%hash);

   # the "hash reference" way
   my $mwhr = Plack::Middleware::ReviseEnv->new(revisors => \%hash);

   # the "array reference" way
   my $mwar = Plack::Middleware::ReviseEnv->new(revisors => \@array);

The first two will be eventually turned into the last one by means of "normalize_input_structure" by simply putting the sequence of key/value pairs in the array, ordered by key.

In the array reference form, for each revisor you can provide:

  • a single hash reference with the details on the revisor (see below for the explaination), OR

  • a string key (that we will call external key) and a hash reference. If the hash reference contains the key key (sorry!) then the external key will be ignored, otherwise it will be set corresponding to key key. Example:

       foo => { value => 'ciao' }

    is interpreted as:

       { key => 'foo', value => 'ciao' }

    while:

       foo => { key => 'bar', value => 'baz' }

    is interpreted as:

       { key => 'bar', value => 'baz' }

    This is useful when you start from the hash or hash-reference forms, because the external key will be used for ordering revisors only (see "Ordering Revisors");

  • two strings, one for the key and one for the value. Example:

       foo => 'bar'

    is interpreted as:

       { key => 'foo', value => 'bar' }

While the normal key/value pairs should be sufficient in the general case, to trigger more advanced features you have to pass the whole hash reference definition for a revisors. The hash can contain the following keys:

cache

after computing a value the first time, cache the result for all following invocations. This will speed up the execution at the expense of flexibility.

You might want to use this option if you're only relying on value coming from %ENV and your code is not going to change its items dynamically. As this is probably the most common case, this option defaults to 1, which means that the value will be cached. You can disable it either in the opts in the constructor, or per-revisor, by setting it to a Perl-false value;

default_key
default_value

when the computed value for either the key or the value are undefined, the corresponding key is deleted. If you set a defined value, this will be used instead.

Setting a default value makes sense only if either empty_as_default or require_all are set too; otherwise, whatever expansion will always yield a defined value (possibly empty).

empty_as_default

when the computed value is empty, treat it as it were undefined. This is a single setting for both key and value.

It is useful if you suspect that your environment might actually contain a variable, but with an empty value that you want to override with a default.

esc

the escape character to use when parsing templates. It defaults to a single backslash, but you can override this with a different string as long as it's not empty, it does not start with a space and is different from both start and stop (see below) values. This might come handy in the (unlikely) case that you must use lots of backslashes.

key

the key that will be set in $env. It is a template itself, so it is subject to expansion and other rules explained here.

If you set the revisor with the key/value pair style, the key will be used as the default value here; if you just provide a specification revisor via a hash reference, you MUST provide a key though.

override

boolean flag to indicate that you want to overwrite any previously existing value in $env for a specific computed key.

It defaults to true, but you can set it to e.g. 0 to disable overriding and set the value in $env only if there's nothing there already.

require_all

boolean flag that makes an expansion fail (returning undef) if any component is missing. Defaults to a false value, meaning that missing values are expanded as empty (but defined!) strings.

For example, consider the following revisors:

   ...
   inexistent => undef, # this removes inexistent from $env

   set_but_awww => 'Foo: [% env:inexistent %]',

   not_set_at_all => {
      value => 'Foo: [% env:inexistent %]',
      require_all => 1,
   },
   ...

As a final result, $env->{set_but_empty} ends up being present with value Foo: , while $env->{not_set_at_all} is not set or deleted if present.

This can be also combined with default_key or default_value.

start
stop

the delimiters for the expansion sections, defaulting to [% and %] respectively (or whatever option was set in opts at object creation). You can override them with any non-empty string.

value

the template for the value.

Templates

Both the key and the value of a revisor are templates. They are initially parsed (during prepare_app) and later expanded when needed (i.e. during call).

The parsing verifies that the template adheres to the "Template rules"; the expansion is explained in section "Expansion".

Template rules

Templates are a sequence of plain text and variable expansion sections. The latter ones are delimited by a start and stop character sequence. So, for example, with the default start and stop markers the following text:

   Foo [% ENV:BAR %] baz

is interpreted as:

   plain text        'Foo '
   expansion section ' ENV:BAR '
   plain text        ' baz'

Plain text sections can contain whatever character sequences, except (unescaped) start for a variable expansion section. If you want to include a start sequence, prepend it with an escape sequence (defaulting to a single backslash), like this:

   Foo \[% ENV:BAR %] baz

is interpreted as:

   plain text 'Foo \[% ENV:BAR %] baz'

The escape just makes the character immediately following it be ignored during parsing, which happens in the expansion sections too. So, suppose that you have a variable whose name contains the end sequence, you can still use it like this:

   Foo [% env:bar \%] %] baz

is interpreted as:

   plain text        'Foo '
   expansion section ' env:bar \%] '
   plain text        ' baz'

After dividing the input template into sections, the plain text sections are just unescaped, while the expansion sections are futher analyzed:

  • the section string is trimmed while still honoring escape characters (i.e. escaped trailing spaces are kept, even if it can sound crazy);

  • then it is unescaped;

  • then it is split into two components separated by a colon, checking that the first part is either ENV or env (the source for the expansion) and the second is the name of the item inside the source.

Example:

                      ' ENV:FOO\ \  '
     trimmed to   --> 'ENV:FOO\ \ '
     unescaped to --> 'ENV:FOO  '
     split to     --> 'ENV', 'FOO  '

In the example, the expansion section will be used to get the value of item FOO (with two trailing spaces) from %ENV.

You can set different start, stop and escape sequences by:

  • setting options start, stop and esc (respectively) in configuration hash opts in the constructor, or

  • setting options start, stop and esc (respectively) in the revisor definition (this takes precedence with respect to the ones in the opts for the object, of course).

Expansion

While parsing happens once at the beginning (during phase prepare_app), usage of a parsed template happens at call time, i.e. at every request hitting the plugin.

The first time the request comes, the parsed template is evaluated according to what described below. Depending on the value of cache (which can be set both in opts for the constructor, and in each revisor singularly), this value might be reused for following calls (providing better performance) or computed each time (providing greater flexibility to cope with changing inputs). Caching is enabled by default, assuming that most of the times you will just want to get values from an unchanging environment; if you need to do fancier things, though, you can disable it altogether (setting option cache to a false value in the constructor parameters) or for each single revisor that needs special attention.

During expansion, text parts are passed verbatim, while expansion sections take the value from either %ENV or $env depending on the expansion section itself. If the corresponding value is not present or is undef:

  • by default the empty string is used

  • if option require_all in the revisor definition is set to a (Perl) true value, the whole expansion fails and returns undef or whatever default value has been set in default_key or default_value for keys and values respectively.

If the expansion above yields undef:

  • if it's the expansion of a key, it is skipped;

  • if it's the expansion of a value, "Removing Variables" applies (i.e. the variable is not set and removed if present).

Removing Variables

In addition to setting values, you can also remove them (e.g. suppose that you are getting some headers and you want to silence them (e.g. for debugging purposes). To do this, just set the corresponding key to undef:

   ...
   remove_me => undef,
   ...

This actually works whenever the expanded value returns undef, although this never happens by default because undef values in the expansion are turned into empty strings:

   ...
   will_be_empty => '[% ENV:inexistent_value %]',
   ...

See "Expansion" for making the above return undef (via require_all) and trigger the removal of will_be_empty.

Ordering Revisors

If you plan using intermediate variables for building up complex values, you might want to switch to the array reference form of the revisor definition (see "Defining Revisors"), because the hash-based alternatives require more care.

As an example, the following will NOT do what you think:

   # using plain hash way... and being BITEN HARD!
   my $me = Plack::Middleware::ReviseEnv->new(
      foo => 'FOO',
      bar => 'Hey [% env:foo %]',
   );

This is because the following array-based rendition will be used:

   [
      bar => 'Hey [% env:foo %]',
      foo => 'FOO',
   ]

i.e. bar will be eventually expanded before foo. This is because keys are used for ordering revisors when transforming to the array-based form.

The ordering part is actually there to help you, because by default Perl does not guarantee any kind of order when you expand a hash to the list of key/value pairs. So, at least, in this case you have some guarantees!

So what can you do? You can take advantage of the full form for defining a revisor, like this:

   # using plain hash way... more verbose but correct now
   my $me = Plack::Middleware::ReviseEnv->new(
      '1' => { key => foo => value => 'FOO' },
      '2' => { key => bar => value => 'Hey [% env:foo %]'},
   );

The hash keys 1 and 2 will be used to order revisors, so they are set correctly now:

   [
      '1' => { key => foo => value => 'FOO' },
      '2' => { key => bar => value => 'Hey [% env:foo %]'},
   ]

Note that the revisor definitions already contain a key field, so neither 1 nor 2 will be used to override this field, which is the same as the following array form:

   [
      { key => foo => value => 'FOO' },
      { key => bar => value => 'Hey [% env:foo %]'},
   ]

i.e. what you were after in the first place.

Takeaway: if you can, always use the array-based form!

METHODS

The following methods are implemented as part of the interface for a Plack middleware. Although you can override them... there's probably little sense in doing this!

call
prepare_app

Methods described in the following subsections can be overridden or used in derived classes, with the exception of "new".

escaped_index

   my $i = $obj->escaped_index($template, $str, $esc, $pos);

Low-level method for finding the first unescaped occurrence of $str inside $template, starting from $pos and considering $esc as the escape sequence. Returns -1 if the search is unsuccessful, otherwise the index value in the string (indexes start from 0), exactly as CORE::index.

escaped_trim

   my $trimmed = $obj->escaped_trim($str, $esc);

Low-level function to trim away spaces from an escaped string. It takes care to remove all leading spaces, and all unescaped trailing spaces (there can be no "escaped leading spaces" because escape sequences cannot start with a space).

Note that trimming targets only plain horizontal spaces (ASCII 0x20).

generate_revisor

   my $revisor = $obj->generate_revisor($input_definition);

Generate a revisor from an input definition.

The input definition MUST be a hash reference with fields explained in section "Defining Revisors".

Returns a new revisor.

It is used by "prepare_app" to set the list of revisors that will be used during expansion.

new

   # Alternative 1, when %hash DOES NOT contain a "revisors" key
   my $mw_h = Plack::Middleware::ReviseEnv->new(%hash);

   # Alternative 2, "revisors" points to a hash ref
   my $mw_r = Plack::Middleware::ReviseEnv->new(
      revisors => $hash_or_array_ref, # array ref is PREFERRED
      opts     => \%hash_with_options
   )

You are not supposed to use this method directly, although these are exactly the same parameters that you are supposed to pass e.g. to builder in Plack::Builder.

The first form is quick and dirty and should be fine in most of the simple cases, like if you just want to set a few variables taking them from the environment (%ENV) and you're fine with the default options.

The second form allows you to pass options, e.g. to change the delimiters for expansion sections, and also to define the sequence of revisors as an array reference, which is quite important if you are going to do fancy things (see "Ordering Revisors" for example).

Available opts are:

cache

boolean flag indicating, when true, that expanded values (see "Expansion") should be cached for later reuse in following calls. This improves performance (values are computed only the first time they are needed) at the expense of flexibility (if you have a changing %ENV or rely on values in $env that depend on the specific call, caching will not take those changes). This can be overridden on a per-revisor basis.

Defaults to 1 i.e. caching is enabled for all revisors by default; disable it inside a revisor that needs dynamic computing of its value.

esc

the escape sequence to use when parsing a template (see "Template rules"). This can be overridden on a per-revisor basis.

Defaults to a single backslash \.

start

the start sequence to use when parsing a template (see "Template rules"). This can be overridden on a per-revisor basis.

Defaults to string [%, in Template::Toolkit spirit.

stop

the stop sequence to use when parsing a template (see "Template rules"). This can be overridden on a per-revisor basis.

Defaults to string %], in Template::Toolkit spirit.

normalize_input_structure

   my $normal = $obj->normalize_input_structure($source, $defaults);

normalizes the object internally landing you with the following fields:

app
revisors
opts

where revisors is in the array form and opts has any missing item initialised to the corresponding default (if not already present).

parse_template

   my $expandable = $obj->parse_template($template, $start, $stop, $esc);

applies the parsing explained in section "Template rules" and returns an array reference of sequences of either plain text chunks, or hash references each containing:

src

either env or ENV

key

the key to use inside the src for expanding a variable.

This method is quite low-level and you have to explicitly pass the start, stop and escaping sequences, making sure they don't tread on each other.

Used by "generate_revisor".

unescape

   my $text = $obj->unescape($escaped_text, $esc);

Removes the escaping sequence $esc from $escaped_text.

BUGS AND LIMITATIONS

Report bugs either through RT or GitHub (patches welcome).

SEE ALSO

Plack, Plack::Middleware::ForceEnv, Plack::Middleware::ReverseProxy, Plack::Middleware::SetEnvFromHeader, Plack::Middleware::SetLocalEnv.

AUTHOR

Flavio Poletti <polettix@cpan.org>

COPYRIGHT AND LICENSE

Copyright (C) 2016 by Flavio Poletti <polettix@cpan.org>

This module is free software. You can redistribute it and/or modify it under the terms of the Artistic License 2.0.

This program 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.