The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

CodeGen::Protection::Tutorial — What the heck is this thing for?

VERSION

version 0.05

RATIONALE

Sometimes you write code that writes code, but other people might change that code, breaking it. You don't want that. You also want to be able to regenerate your code so that others can use it after it's upgraded. So we'll walk through the process. If you've already used DBIx::Class::Schema::Loader, you probably have a pretty good idea of what's going on here.

OpenAPI EXAMPLE

For this example, imagine you're writing code to autogenerate OpenAPI server code. In OpenAPI, you have a JSON or YAML document that specifies OpenAPI routes. Ignoring the rest of the document, let's just look at a couple of paths that might be listed:

    paths:
      /users:
        get:
          summary: Returns a list of users.
          description: Get a list of users
          responses:
            '200':    # status code
              description: A JSON array of user names
              content:
                application/json:
                  schema: 
                    type: array
                    items: 
                      type: string

Without getting into detail, the above describes an HTTP request which might be made to your server:

    GET /users

In OpenAPI, you don't want to manually write a bunch of repetitive code. You want code to read a spec and have most of that code written for you. In fact, the openapi-generator will write out most of the code for you, but sadly, it only writes client code for Perl, not server code. So you want to read the above JSON document and autogenerate code that looks like this:

    package My::OpenAPI::Controller::Users;

    use strict;
    use warnings;
    use My::OpenAPI::Server;

    use My::OpenAPI::Handler qw(declare_routes);

    declare_routes(
        route => 'GET /users', to => 'get',
    );

    1;

And then you turn that over to a developer and all they have to do is write the get function. Later on, your OpenAPI definition is expanded to add the ability to fetch a single user:

      /users/{userId}:
        get:
          summary: Returns a user.
          description: Returns a User
          responses:
            '200':    # status code
              description: A JSON object describing a user
              content:
                application/json:
                  schema: 
                    type: object
                    ... more stuff here

And you have a new route added:

    GET /users/$user_id

If you simply regenerate your My::OpenAPI::Controller::Users module to add the new route, you overwrite the code your developer added. But if you manually add all of the code, you lose the power of code generation and you're more likely to make mistakes (and your author has previously done this with huge OpenAPI documents; it's not fun). So instead, you decide to use CodeGen::Protection.

CREATING A NEW DOCUMENT

Let's create a new document using the example above. We will assume you have a module named My::OpenAPI::CodeGen that generates the following routes if you have a single path of GET /users:

    use My::OpenAPI::Handler qw(declare_routes);

    declare_routes(
        route => 'GET /users', method => 'get',
    );

And using that in your code generator:

    #!/usr/bin/env perl

    use strict;
    use warnings;
    use My::OpenAPI::CodeGen qw(generate_route_code);
    use CodeGen::Protection qw(create_protected_code);

    my $code      = generate_route_code('path/to/openapi.json');
    my $protected = create_protected_code(
        type           => 'Perl',
        protected_code => $code,
    );

    print <<"END";
    package My::OpenAPI::Controller::Users;

    use strict;
    use warnings;
    use My::OpenAPI::Server;

    $protected

    1;
    END

And that prints out something similar to the following:

    package My::OpenAPI::Controller::Users;

    use strict;
    use warnings;
    use My::OpenAPI::Server;

    #<<< CodeGen::Protection::Format::Perl 0.05. Do not touch any code between this and the end comment. Checksum: cb12361766d6729093553d38122d8aba
    
    use My::OpenAPI::Handler qw(declare_routes);
    
    declare_routes(
        route => 'GET /users', method => 'get',
    );
    
    #>>> CodeGen::Protection::Format::Perl 0.05. Do not touch any code between this and the start comment. Checksum: cb12361766d6729093553d38122d8aba

    1;

In the above, the lines beginning with #<<< and #>>> are the "start and end markers" for the protected code. Do not change anything in or between those lines. If you do, code regeneration will fail.

Now you can write that document to a file and safely hand it to a developer. They just need to write the get method and you're good. Let's pretend that this is what the developer has added to the end of that file:

    sub get {
        my ($request) = @_;
        return My::OpenAPI::Server->list('users');
    }

REWRITING A DOCUMENT

Later, someone has added the path for GET /users/{userId} to the OpenAPI specification document, so you want to regenerate your code. Now, however, you need to read and write the lib/My/OpenAPI/Controller/Users.pm file.

    #!/usr/bin/env perl

    use strict;
    use warnings;
    use My::OpenAPI::CodeGen qw(generate_route_code);
    use CodeGen::Protection qw(rewrite_code);

    my $controller = 'lib/My/OpenAPI/Controller/Users.pm';

    # open our file in read/write mode
    open my $fh, '+<', $controller
      or die "Cannot open $controller in read-write mode: $!";
    my $existing = do { local $/; <$fh> };

    # generate our protected "route" code
    my $code      = generate_route_code('path/to/openapi.json');

    # rewrite the protected section of the $existing code with
    # our regenerated route code
    my $rewritten = rewrite_code(
        type           => 'Perl',
        protected_code => $code,
        existing_code  => $existing,
    );

    # write it back to the file
    seek $fh, 0,0;
    print {$fh} $rewritten;

And now your lib/My/OpenAPI/Controller/Users.pm file will resemble:

    package My::OpenAPI::Controller::Users;
    
    use strict;
    use warnings;
    use My::OpenAPI::Server;
    
    #<<< CodeGen::Protection::Format::Perl 0.05. Do not touch any code between this and the end comment. Checksum: ebb0ca5eaea8c69ef08bddc39a27272a
    
    use My::OpenAPI::Handler qw(declare_routes);
    
    declare_routes(
        route => 'GET /users',          method => 'get',
        route => 'GET /users/{userID}', method => 'get_userId',
    );
    
    #>>> CodeGen::Protection::Format::Perl 0.05. Do not touch any code between this and the start comment. Checksum: ebb0ca5eaea8c69ef08bddc39a27272a
    
    sub get {
        my ($request) = @_;
        return My::OpenAPI::Server->list('users');
    }

    1;

Note that we have rewritten the protected part of this document, but the sub get {...} code the developer added has remained. This allows you to keep regenerating these documents, but without breaking the existing code.

Why rewrite_code() might fail

If you run rewrite_code(), it can fail for several reason:

  • The checksums were not found in the $existing document

  • The start and end checksums are not identical

  • The checksum generated doesn't match the text between the start and end markers

  • There is no valid CodeGen::Protection::Format::$type module for $type

In short, rewrite_code() will generally fail if anythign about the protected code has been changed. This will stop a developer from thinking "hey, I want to change get_userId to get_user_id" and thus breaking your code.

TESTING

Note that CodeGen::Protection manipulates documents (e.g., strings), but does no I/O. So let's assume we've written the above document to lib/My/OpenAPI/Controller/Users.pm. If you want to write a test to verify that it's good, you use Test::CodeGen::Protection:

    #!/usr/bin/env perl

    use Test::Most;
    use Test::CodeGen::Protection;

    is_protected_file_ok 'Perl', 'lib/My/OpenAPI/Controller/Users.pm',
        'Protected code in Users.pm controller has not been touched';

    done_testing;

AUTHOR

Curtis "Ovid" Poe <ovid@allaroundtheworld.fr>

COPYRIGHT AND LICENSE

This software is copyright (c) 2021 by Curtis "Ovid" Poe.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.