in reply to STDIN typeglob

One of the benefits of writing tests (and particularly of TDD) is that it can give you a signal about your interface: if something is difficult to write tests for, maybe it's the interface that should change.

A module providing a function that reads from STDIN would be an example of that: perhaps it would be easier to test, _and_ provide a more powerful, flexible function if the function were to accept the filehandle to read from as an argument instead.

A typical way to use such a function to read from STDIN would be to pass a reference to the glob:

MyModule::function(\*STDIN);

Typeglobs aren't really a "legacy left over from the days before Perl had references", rather they expose aspects of how Perl works internally. The introduction of references certainly reduced the number of situations where one needs to use globs, but because filehandles and directory handles don't have their own sigil to address them directly (the way $STDIN, @STDIN, %STDIN, &STDIN do), a glob reference as in the example above is still a perfectly fine way to access them.

Another example is for getting clever with generated code, for example to auto-generate accessors for an object:

for my $accessor (qw{ foo bar }) { my $method = sub { my($self) = @_; return $self->{$accessor}; }; # inject it as a named subroutine no strict 'refs'; *$accessor = $method; }

This works due to one of the "magic" aspects of globs: if you assign a reference to a glob, it will store the thing referenced in the appropriate slot. In this case we are assigning a subroutine reference, so that loops creates subroutines "foo" and "bar" (almost) exactly as if we had defined them in the normal way like:

sub foo { my($self) = @_; return $self->{foo}; }

Replies are listed 'Best First'.
Re^2: STDIN typeglob
by afoken (Chancellor) on Jun 12, 2023 at 09:35 UTC
    One of the modules I have recently released to CPAN reads JSON data from STDIN on a webserver.

    One of the benefits of writing tests (and particularly of TDD) is that it can give you a signal about your interface: if something is difficult to write tests for, maybe it's the interface that should change.

    Reading from STDIN on a webserver sounds very much like CGI. That's not a problem as such, but there are many other interfaces where data from the web browser is not passed via STDIN. Already FastCGI, which is only a tiny step away from CGI, does not use that simple interface (but FCGI and CGI::Fast can do a lot to hide that fact). And when it comes to other interfaces to webservers, like modperl, STDIN is not used at all (again, there are compatibility layers like ModPerl::Registry).

    In other words, passing a handle to the reading function might be a smarter solution. Perhaps, your module should not fetch the data at all, but just accept the data as a scalar value. Both would also allow for easier testing.

    Alexander

    --
    Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)

      Perhaps, your module should not fetch the data at all, but just accept the data as a scalar value. Both would also allow for easier testing.

      That is exactly what I was thinking!

      In the hope it clarifies Bod's question, I think the module in question is Business::Stripe::Webhook, whose version 1.0 constructor is:

      sub new { my $class = shift; my %vars = @_; $vars{'error'} = ''; $vars{'reply'} = { 'status' => 'noaction', 'sent_to' => [ ], 'sent_to_all' => 'false', }; if (exists $ENV{'GATEWAY_INTERFACE'}) { read(STDIN, $vars{'payload'}, $ENV{'CONTENT_LENGTH'}); $vars{'webhook'} = decode_json($vars{'payload'}) if $vars{'pay +load'}; $vars{'error'} = 'No payload data' unless $vars{'webhook'}; } else { $vars{'error'} = 'Looks like this is not a web request!'; } return bless \%vars, $class; }

      Though I'm definitely not a Web programmer, from an interface and TDD point of view, I pulled a face the instant I saw the constructor using an environment variable to decide whether to read from STDIN or not.

      It seems clearer and easier to test if this module were to simply accept a payload property. That way, the module's tests can easily pass in all sorts of dodgy payloads to see how it handles bad input.

      That is, instead of trying to do everything in one module, use several, smaller, more cohesive modules to get the job done.

        Though I'm definitely not a Web programmer, from an interface and TDD point of view, I pulled a face the instant I saw the constructor using an environment variable to decide whether to read from STDIN or not.

        That's clearly CGI. Or, in other words, this module won't work with anything but CGI scripts. Maybe, but just maybe, the magic implemented by FCGI/CGI::Fast resp. ModPerl::Registry might be sufficient to fake enough CGI environment (i.e. faked environment, faked STDIN) to make it work.

        Oh, and if you use some CGI library with that module, it is a question of initialisation order if this module will work or not. CGI prefers to read STDIN by itself, and I think it won't be happy if you steal its input data. If you init CGI first, input data for Bod's module is gone.

        So, in any case, passing the JSON data as a parameter is way better.

        Alexander

        --
        Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
        sub new { my $class = shift; my %vars = @_; $vars{'error'} = ''; $vars{'reply'} = { 'status' => 'noaction', 'sent_to' => [ ], 'sent_to_all' => 'false', }; if (!exists{'payload'}) { # Obtaining payload from STDIN only # exists for backward compatability # This option is deprecated and will # be removed in a future version if (exists $ENV{'GATEWAY_INTERFACE'}) { read(STDIN, $vars{'payload'}, $ENV{'CONTENT_LENGTH'}); $vars{'webhook'} = decode_json($vars{'payload'}) if $vars{ +'payload'}; $vars{'error'} = 'No payload data' unless $vars{'webhook +'}; } else { $vars{'error'} = 'Looks like this is not a web request!' +; } } return bless \%vars, $class; }
        It seems clearer and easier to test if this module were to simply accept a payload property

        Is that a better constructor?
        The production version of the module has been released so I don't want to break that. So the new method now accepts a payload property. If that is missing and it is running on a webserver with the right environment, it falls back to the previous behaviour.

        A couple of versions down the line I will remove the fallback code and set an error condition if payload is not a valid JSON scalar.

Re^2: STDIN typeglob
by Bod (Parson) on Jun 11, 2023 at 21:48 UTC
    if something is difficult to write tests for, maybe it's the interface that should change

    It's not that it is difficult to write the tests, it is difficult to simulate the module being connected to the API when it is being tested and isn't connected to a live API.