Tag: signatures

  • Tuesday at Boston.pm: Perl subroutine signatures and type validation

    I’ll be repris­ing my pre­sen­ta­tion on Perl sub­rou­tine sig­na­tures and type val­i­da­tion for the Boston Perl Mongers on Tuesday, March 9 at 7 PM EST. Visit their wiki for details; they’ll be post­ing the Jitsi URL short­ly before the meet­ing. There’s also a Meetup page.

  • Building a microservice in Perl, part 2: Up and running

    Building a microservice in Perl, part 2: Up and running

    In part 1 we designed our API using OpenAPI/​Swagger. Now it’s time to write some tests and wire it up using Mojolicious::Plugin::OpenAPI. This is a much longer arti­cle; buck­le up!

    If you haven’t already, you’ll need to install Perl. Using Linux or macOS? You’ve already got Perl installed on your sys­tem. On Windows? I rec­om­mend you install Strawberry Perl as it lets you devel­op Perl appli­ca­tions using the same tools that our Unix-​based brethren use. 

    (Advanced users may want to inves­ti­gate using perl­brew, plenv, or berry­brew for man­ag­ing mul­ti­ple ver­sions of Perl and installing more recent ver­sions than are includ­ed on your system.)

    Once you have Perl installed, it’s a sim­ple mat­ter of using either the cpan or cpanm tools to install Mojolicious and Mojolicious::Plugin::OpenAPI. The lat­ter will install some depen­den­cies as well. Here’s a tran­script of installing cpanm and the two modules:

    % curl -L https://cpanmin.us | perl - App::cpanminus
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
    100  295k  100  295k    0     0  97765      0  0:00:03  0:00:03 --:--:-- 97765
    --> Working on App::cpanminus
    Fetching http://www.cpan.org/authors/id/M/MI/MIYAGAWA/App-cpanminus-1.7044.tar.gz ... OK
    Configuring App-cpanminus-1.7044 ... OK
    Building and testing App-cpanminus-1.7044 ... OK
    Successfully installed App-cpanminus-1.7044
    1 distribution installed
    % cpanm Mojolicious Mojolicious::Plugin::OpenAPI
    --> Working on Mojolicious
    Fetching http://www.cpan.org/authors/id/S/SR/SRI/Mojolicious-9.01.tar.gz ... OK
    Configuring Mojolicious-9.01 ... OK
    Building and testing Mojolicious-9.01 ... OK
    Successfully installed Mojolicious-9.01
    --> Working on Mojolicious::Plugin::OpenAPI
    Fetching http://www.cpan.org/authors/id/J/JH/JHTHORSEN/Mojolicious-Plugin-OpenAPI-4.00.tar.gz ... OK
    Configuring Mojolicious-Plugin-OpenAPI-4.00 ... OK
    ==> Found dependencies: JSON::Validator
    --> Working on JSON::Validator
    Fetching http://www.cpan.org/authors/id/J/JH/JHTHORSEN/JSON-Validator-4.14.tar.gz ... OK
    Configuring JSON-Validator-4.14 ... OK
    ==> Found dependencies: YAML::PP, Test::Deep
    --> Working on YAML::PP
    Fetching http://www.cpan.org/authors/id/T/TI/TINITA/YAML-PP-0.026.tar.gz ... OK
    Configuring YAML-PP-0.026 ... OK
    ==> Found dependencies: Test::Warn, Test::Deep
    --> Working on Test::Warn
    Fetching http://www.cpan.org/authors/id/B/BI/BIGJ/Test-Warn-0.36.tar.gz ... OK
    Configuring Test-Warn-0.36 ... OK
    ==> Found dependencies: Sub::Uplevel
    --> Working on Sub::Uplevel
    Fetching http://www.cpan.org/authors/id/D/DA/DAGOLDEN/Sub-Uplevel-0.2800.tar.gz ... OK
    Configuring Sub-Uplevel-0.2800 ... OK
    Building and testing Sub-Uplevel-0.2800 ... OK
    Successfully installed Sub-Uplevel-0.2800
    Building and testing Test-Warn-0.36 ... OK
    Successfully installed Test-Warn-0.36
    --> Working on Test::Deep
    Fetching http://www.cpan.org/authors/id/R/RJ/RJBS/Test-Deep-1.130.tar.gz ... OK
    Configuring Test-Deep-1.130 ... OK
    Building and testing Test-Deep-1.130 ... OK
    Successfully installed Test-Deep-1.130
    Building and testing YAML-PP-0.026 ... OK
    Successfully installed YAML-PP-0.026
    Building and testing JSON-Validator-4.14 ... OK
    Successfully installed JSON-Validator-4.14
    Building and testing Mojolicious-Plugin-OpenAPI-4.00 ... OK
    Successfully installed Mojolicious-Plugin-OpenAPI-4.00
    7 distributions installed

    (The ver­sions list­ed above may dif­fer as this arti­cle gets pro­gres­sive­ly out of date.)

    Next, make a fold­er some­where to place our new microser­vice project. We’ll call it ~/Projects/blog here, but any place will do as long as you know how to get to it from your text edi­tor and your com­mand line.

    After that, go into that direc­to­ry and use the newly-​installed mojo com­mand to build the basics of our project:

    % cd ~/Projects/blog
    % mojo generate app Local::Dictionary::Microservice
      [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/script
      [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/script/local_dictionary_microservice
      [chmod] /Users/mgardner/Projects/blog/local_dictionary_microservice/script/local_dictionary_microservice 744
      [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/lib/Local/Dictionary
      [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/lib/Local/Dictionary/Microservice.pm
      [exist] /Users/mgardner/Projects/blog/local_dictionary_microservice
      [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/local-dictionary-microservice.yml
      [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/lib/Local/Dictionary/Microservice/Controller
      [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/lib/Local/Dictionary/Microservice/Controller/Example.pm
      [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/t
      [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/t/basic.t
      [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/public
      [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/public/index.html
      [mkdir] /Users/mgardner/Projects/local_dictionary_microservice/templates/layouts
      [write] /Users/mgardner/Projects/local_dictionary_microservice/templates/layouts/default.html.ep
      [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/templates/example
      [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/templates/example/welcome.html.ep

    This com­mand builds the scaf­fold­ing for our new project. We won’t be using all of it, but it does pro­vide some use­ful start­ing points.

    (Why did we put Local:: in the begin­ning of the name? Because noth­ing you place there will con­flict with oth­er mod­ules you install from CPAN.)

    Next, put the OpenAPI doc­u­ment we devel­oped in part 1 some­where in the project. We’ll choose openapi/dictionary_openapi.yml. Create a fold­er called openapi in the project, and then open your text edi­tor and paste the fol­low­ing doc­u­ment into a file called dictionary_openapi.yml.

    openapi: 3.0.3
    info:
      title: Dictionary
      description: The PhoenixTrap.com dictionary microservice
      version: 1.0.0
      license:
        name: Artistic License 2.0
        url: https://www.perlfoundation.org/artistic-license-20.html
    paths:
      /health:
        get:
          summary: Check if this service is online
          x-mojo-to: monitoring#heartbeat
          responses:
            200:
              description: All systems operational
              content:
                application/json:
                  schema:
                    type: object
            500:
              description: Something is wrong
      /word/{word}:
        get:
          summary: Get the definition of a word
          x-mojo-to: word#define
          parameters:
          - $ref: '#/components/parameters/word'
          responses:
            200:
              description: Found word
              content:
                application/json:
                  schema:
                    type: string
            404:
              description: Could not find word
        post:
          summary: Add or replace the definition of a word
          x-mojo-to: word#save
          parameters:
          - $ref: '#/components/parameters/word'
          requestBody:
            description: Definition of a word
            required: true
            content:
              application/x-www-form-urlencoded:
                schema:
                  type: object
                  properties:
                    definition:
                      type: string
          responses:
            200:
              description: Word saved
        delete:
          summary: Delete an entry from the dictionary
          x-mojo-to: word#remove
          parameters:
          - $ref: '#/components/parameters/word'
          responses:
            200:
              description: Word deleted
    components:
      parameters:
        word:
          description: A word in the dictionary
          in: path
          name: word
          required: true
          schema:
            type: string

    Calling the plugin

    Right now all our appli­ca­tion does is serve a demon­stra­tion page; you can test it by run­ning the fol­low­ing command:

    % script/local_dictionary_microservice daemon
    [2021-02-28 16:17:09.76863] [74167] [info] Listening at "http://*:3000"
    Web application available at http://127.0.0.1:3000

    …and then going to that URL on the last line.

    After you’ve ver­i­fied that it works, hold down the Control key and type the let­ter C in your ter­mi­nal to stop the script. Edit the lib/Local/Dictionary/Microservice.pm file in your text edi­tor. Replace it with this:

    package Local::Dictionary::Microservice;
    use Mojo::Base 'Mojolicious', -signatures;
    
    # This method will run once at server start
    sub startup($self) {
    
        # Load configuration from config file
        my $config = $self->plugin('NotYAMLConfig');
    
        # Configure the application
        $self->secrets( $config->{secrets} );
        $self->plugin( OpenAPI => {
            url => $self->home
              ->rel_file('openapi/dictionary_openapi.yml'),
        } );
    }
    
    1;

    Now when you run the script as a dae­mon, the URL it reports responds with the JSON ver­sion of our OpenAPI doc­u­ment. Progress!

    Note the $self->plugin() call towards the end of our class above. That’s the secret sauce that loads our OpenAPI doc­u­ment and tells Mojolicious to cre­ate respons­es from it.

    Writing our first tests

    Next, we’ll write a cou­ple of test scripts. (Why are we writ­ing tests before actu­al­ly cod­ing our microser­vice? Because we’re prac­tic­ing test-​driven devel­op­ment, in which we write our tests first, see that they fail, and then write the code to make them suc­ceed.) Here’s a mod­i­fied t/basic.t script that should suc­ceed right off the bat:

    use Mojo::Base -strict;
    
    use Test::More;
    use Test::Mojo;
    
    my $t = Test::Mojo->new('Local::Dictionary::Microservice');
    $t->get_ok('/')->status_is(200)->json_has('/openapi');
    
    done_testing();

    Run it with prove:

    % prove -vl t/basic.t
    t/basic.t .. [2021-02-28 16:32:54.79287] [74611] [debug] [3kzjyqgi] GET "/"
    [2021-02-28 16:32:54.79321] [74611] [debug] [3kzjyqgi] Routing to a callback
    [2021-02-28 16:32:54.79536] [74611] [debug] [3kzjyqgi] 200 OK (0.002493s, 401.123/s)
    ok 1 - GET /
    ok 2 - 200 OK
    ok 3 - has value for JSON Pointer "/openapi"
    1..3
    ok
    All tests successful.
    Files=1, Tests=3,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.45 cusr  0.07 csys =  0.55 CPU)
    Result: PASS

    Now for a test that will fail at first. Put this in t/word.t:

    use Mojo::Base -strict;
    
    use Test::More;
    use Test::Mojo;
    
    my $t = Test::Mojo->new('Local::Dictionary::Microservice');
    
    $t->post_ok(
        '/word/foo' =>
          form => { definition => 'A metasyntactic variable' },
    )->status_is(200);
    
    done_testing();

    And prove it:

    % prove -vl t/word.t
    t/word.t .. [2021-02-28 16:48:53.26826] [75751] [debug] [SlDSHWVE] POST "/word/foo"
    [2021-02-28 16:48:53.26897] [75751] [debug] [SlDSHWVE] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "save"
    [2021-02-28 16:48:53.27278] [75751] [debug] [SlDSHWVE] Template "word/save.html.ep" not found
    [2021-02-28 16:48:53.27287] [75751] [debug] [SlDSHWVE] Nothing has been rendered, expecting delayed response
    [2021-02-28 16:49:23.27553] [75751] [debug] Inactivity timeout
    # Premature connection close
    not ok 1 - POST /word/foo
    not ok 2 - 200 OK
    #   Failed test 'POST /word/foo'
    #   at t/word.t line 8.
    #   Failed test '200 OK'
    #   at t/word.t line 8.
    #          got: undef
    #     expected: '200'
    1..2
    # Looks like you failed 2 tests of 2.
    Dubious, test returned 2 (wstat 512, 0x200)
    Failed 2/2 subtests
    Test Summary Report
    -------------------
    t/word.t (Wstat: 512 Tests: 2 Failed: 2)
      Failed tests:  1-2
      Non-zero exit status: 2
    Files=1, Tests=2, 31 wallclock secs ( 0.02 usr  0.01 sys +  0.45 cusr  0.07 csys =  0.55 CPU)
    Result: FAIL

    Note in the sec­ond line of out­put that Mojolicious tried to route to a con­troller class at Local::Dictionary::Microservice::Controller::Word with an action of save. That’s what our OpenAPI doc­u­ment told it to do with its x-mojo-to: word#save line.

    Adding methods to make the tests pass

    Now we’ll write the con­troller class at lib/Local/Dictionary/Microservice/Controller/Word.pm:

    package Local::Dictionary::Microservice::Controller::Word;
    use Mojo::Base 'Mojolicious::Controller', -signatures;
    use DB_File;
    
    sub save($self) {
        return if not $self->openapi->valid_input;
        my ( $word, $definition ) = map { $self->param($_) }
          qw(word definition);
    
        tie my %definition, 'DB_File',
          $self->app->home->child('definitions.db');
        $definition{$word} = $definition;
    
        return $self->render( openapi => {} );
    }
    
    1;

    The save method above retrieves the word and definition para­me­ters from the URL path and POST data, respec­tive­ly, and then saves them into a hash that is tied to a Berkeley DB file stored in definitions.db. It then ren­ders an emp­ty response back to the client.

    Now run the test:

    % prove -vl t/word.t
    t/word.t .. [2021-02-28 18:06:33.77551] [78923] [debug] [VV0L-5rU] POST "/word/foo"
    [2021-02-28 18:06:33.77623] [78923] [debug] [VV0L-5rU] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "save"
    [2021-02-28 18:06:33.77751] [78923] [debug] [VV0L-5rU] 200 OK (0.001994s, 501.505/s)
    ok 1 - POST /word/foo
    ok 2 - 200 OK
    1..2
    ok
    All tests successful.
    Files=1, Tests=2,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.43 cusr  0.07 csys =  0.53 CPU)
    Result: PASS

    Woohoo, our method works! Let’s add some tests to the same t/word.t script, under­neath the first one:

    
    $t->get_ok('/word/foo')->status_is(200)
      ->content_is('"A metasyntactic variable"');
    
    $t->delete_ok('/word/foo')->status_is(200);
    
    $t->get_ok('/word/foo')->status_is(404);

    Here we’re test­ing that we get back the def­i­n­i­tion we saved, that we can delete the def­i­n­i­tion, and that when we try to retrieve it again we get a 404 Not Found error.

    The test script will fail again, but we can fix that by adding the define and remove meth­ods to our con­troller class in lib/Local/Dictionary/Microservice/Controller/Word.pm:

    sub define($self) {
        return if not $self->openapi->valid_input;
        my $word = $self->param('word');
    
        tie my %definition, 'DB_File',
          $self->app->home->child('definitions.db');
    
        return $self->render( openapi => $definition{$word},
          exists $definition{$word} ? () : (status => 404),
        );
    }
    
    sub remove($self) {
        return if not $self->openapi->valid_input;
        my $word = $self->param('word');
    
        tie my %definition, 'DB_File',
          $self->app->home->child('definitions.db');
        delete $definition{$word};
    
        return $self->render( openapi => {} );
    }

    Run the test script one last time:

    % prove -vl t/word.t
    t/word.t .. [2021-02-28 18:13:50.82904] [79162] [debug] [7PprxDOz] POST "/word/foo"
    [2021-02-28 18:13:50.82978] [79162] [debug] [7PprxDOz] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "save"
    [2021-02-28 18:13:50.83225] [79162] [debug] [7PprxDOz] 200 OK (0.003191s, 313.381/s)
    ok 1 - POST /word/foo
    ok 2 - 200 OK
    [2021-02-28 18:13:50.83531] [79162] [debug] [G0qyFA0G] GET "/word/foo"
    [2021-02-28 18:13:50.83568] [79162] [debug] [G0qyFA0G] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "define"
    [2021-02-28 18:13:50.85745] [79162] [debug] [G0qyFA0G] 200 OK (0.022096s, 45.257/s)
    ok 3 - GET /word/foo
    ok 4 - 200 OK
    ok 5 - exact match for content
    [2021-02-28 18:13:50.86182] [79162] [debug] [MoedWcOi] DELETE "/word/foo"
    [2021-02-28 18:13:50.86224] [79162] [debug] [MoedWcOi] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "remove"
    [2021-02-28 18:13:50.86337] [79162] [debug] [MoedWcOi] 200 OK (0.001535s, 651.466/s)
    ok 6 - DELETE /word/foo
    ok 7 - 200 OK
    [2021-02-28 18:13:50.86662] [79162] [debug] [G2lH8gyw] GET "/word/foo"
    [2021-02-28 18:13:50.86684] [79162] [debug] [G2lH8gyw] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "define"
    [2021-02-28 18:13:50.86964] [79162] [debug] [G2lH8gyw] 404 Not Found (0.002989s, 334.560/s)
    ok 8 - GET /word/foo
    ok 9 - 404 Not Found
    1..9
    ok
    All tests successful.
    Files=1, Tests=9,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.46 cusr  0.10 csys =  0.59 CPU)
    Result: PASS

    Congratulations! You now have a sim­ple microser­vice for stor­ing and retriev­ing def­i­n­i­tions of words. You can now write a web front-​end in HTML and JavaScript, or per­haps anoth­er microser­vice that con­sumes this one.

    (As an exer­cise, write the /health route defined in our OpenAPI doc­u­ment. It calls a heartbeat method in a con­troller class named Monitoring.)

    What next?

    • You may not want to store your def­i­n­i­tions in a DB file in your project; con­sid­er mak­ing it a con­fig­urable option. Or use one of the many mod­ules on CPAN to choose a com­plete­ly dif­fer­ent back­end.
    • Add meth­ods to list some or all def­i­n­i­tions, using hyper­text as the engine of appli­ca­tion state (HATEOAS) to ren­der links where appropriate.
    • Support mul­ti­ple def­i­n­i­tions of the same word, or expand the API to respond with syn­onyms and antonyms. (Just make sure to incre­ment the ver­sion in the OpenAPI doc­u­ment and add some way for clients to spec­i­fy the ver­sion to indi­cate you’re mak­ing a break­ing change!)

    Did you have any trou­ble fol­low­ing along? Got stuck on the instal­la­tion steps or some­where else? Please leave a com­ment below and I’ll try to help.

  • Better Perl: More on signatures and types

    A few weeks ago I wrote an arti­cle cov­er­ing six ways to do sub­rou­tine sig­na­tures and type con­straints in Perl. Some of the feed­back I got indi­cat­ed that there were more options to con­sid­er, so for the pre­sen­ta­tion I gave at Houston Perl Mongers I quick­ly sum­ma­rized at the end sev­en more mod­ules from CPAN that offer these fea­tures. Still more feed­back showed that I had missed five more mod­ules, so at the risk of becom­ing the sig­na­tures guy” here they are.

    Class::ParamParser (updated)

    First released in 2000 as part of a larg­er project, this may be the first general-​purpose para­me­ter pars­er to appear on CPAN. Although the doc­u­men­ta­tion exam­ples use it as a par­ent class, you should fol­low the sug­ges­tion fur­ther down and use its two meth­ods params_to_hash and params_to_array direct­ly from the class. Both take the same list of argu­ments, and only dif­fer on whether they return a hash ref­er­ence con­tain­ing named para­me­ters or an array ref­er­ence con­tain­ing posi­tion­al parameters.

    There are options for slurp­ing extra passed val­ues into anoth­er hash buck­et and low­er­cas­ing named para­me­ters, and it will auto­mat­i­cal­ly chop off any lead­ing hyphens in your para­me­ter names. You can also alias mul­ti­ple named para­me­ters to the same result­ing argu­ment. The mod­ule does­n’t pro­vide any facil­i­ty for type val­i­da­tion, though.

    Method::ParamValidator

    This adds the inter­est­ing wrin­kle of being con­fig­urable with an exter­nal JSON file; oth­er­wise, it seems you have to call var­i­ous meth­ods on a val­ida­tor object to add para­me­ters (which it calls fields) or meth­ods to val­i­date. It does­n’t seem to sup­port slurp­ing extra val­ues nor posi­tion­al para­me­ters, but its type val­i­da­tion lets you use arbi­trary code ref­er­ences. The doc­u­men­ta­tion could use a bit of work with more exam­ples of real-​world usage rather than test script extracts. The author says it’s just a pro­to­type,” though.

    Mojolicious::Validator

    The Mojolicious real-​time web frame­work has a built-​in val­ida­tor, but it’s geared towards val­i­dat­ing HTTP para­me­ters rather than sub­rou­tine argu­ments. There’s also a MojoX::Validator wrap­per for Input::Validator that per­forms much the same task. You could press either into ser­vice as sub­rou­tine val­ida­tors if you’re already using Mojolicious, but they aren’t struc­tured that way.

    Return::Type

    This isn’t for input para­me­ters to sub­rou­tines; rather, it pro­vides an attribute that spec­i­fies what type your sub­rou­tines return. As such, it’s com­pli­men­ta­ry to most of the oth­er tech­niques cov­ered in this series. It works with Type::Tiny, MooseX::Types, or MouseX::Types, and because Perl func­tions can tell whether they’re in scalar or list con­text, you can spec­i­fy dif­fer­ent type con­straints for each.

    There are sev­er­al oth­er mod­ules with sim­i­lar func­tion­al­i­ty, but these are out­side the scope of this article.

    routines

    Based on Function::Parameters (cov­ered pre­vi­ous­ly), this prag­ma adds sup­port for a reg­istry for stor­ing Type::Tiny type libraries. It does­n’t appear to offer any oth­er sig­na­ture fea­tures, though. Feel free to cor­rect me in the comments.

    As I said in my slide deck, maybe we’re tak­ing TMTOWTDI too far? It’s nice to be spoiled for options, though. I still rec­om­mend Type::Tinys Type::Params for its per­for­mance and flex­i­bil­i­ty, or Params::ValidationCompiler if you’re using type libraries from Specio or Moose. Some use­ful side-​by-​side bench­marks would be nice; maybe I’ll save that for anoth­er arti­cle and fur­ther risk being labeled the sig­na­tures guy.”

  • Better Perl: Subroutine Signatures and Type Validation presentation slides

    Here they are from tonight’s pre­sen­ta­tion, typo-​corrected and ready for review. Video to follow.

    Animated slideshow of Better Perl: Subroutine Signatures and Type Validation
    It seemed like the time just flew by…

    The orig­i­nal arti­cle that inspired this pre­sen­ta­tion is here.

  • Dan Book’s Perl 7: A Modest Proposal

    This pro­pos­al from Dan Book seems rea­son­able to me. A ver­sion 7 fea­ture bun­dle that ren­ders sig­na­tures non-​experimental; removes the indi­rect, mul­ti­di­men­sion­al, and bare­word file­han­dle fea­tures; enables warn­ings and utf8 by default? Sure. And more impor­tant­ly, incre­ment­ing the major ver­sion every time a new fea­ture is stable.

    Unfortunately we’re still on Perl 5.16.3 at work, and it would be a big push to make sure our code­base is com­pat­i­ble with a new­er ver­sion, much less adopt new fea­tures. But I’m will­ing to bet a rea­son­able release of ver­sion 7 might be just the push we need.