Group
Extension

CodeGen-Protection/lib/CodeGen/Protection/Tutorial.pod

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

__END__

=pod

=encoding UTF-8

=head1 NAME

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

=head1 VERSION

version 0.06

=head1 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
L<DBIx::Class::Schema::Loader|https://metacpan.org/pod/DBIx::Class::Schema::Loader>,
you probably have a pretty good idea of what's going on here.

=head1 OpenAPI EXAMPLE

For this example, imagine you're writing code to autogenerate
L<OpenAPI|https://swagger.io/> 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 path 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 L<openapi-generator|https://github.com/OpenAPITools/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 C<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 C<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
L<CodeGen::Protection|https://metacpan.org/pod/CodeGen::Protection>.

=head1 CREATING A NEW DOCUMENT

Let's create a new document using the example above. We will assume
you have a module named C<My::OpenAPI::CodeGen> that generates the following
routes if you have a single path of C<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 C<< #<<< >> and C<< #>>> >> are the
"start and end markers" for the protected code. Do not change I<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 C<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');
    }

=head1 REWRITING A DOCUMENT

Later, someone has added the path for C<GET /users/{userId}> to the OpenAPI
specification document, so you want to regenerate your code. Now, however, you
need to read and write the C<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 C<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 I<rewritten> the protected part of this document, but the
C<sub get {...}> code the developer added has remained. This allows you to
keep regenerating these documents, but without breaking the existing code.

=head2 Why rewrite_code() might fail

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

=over 4

=item * The checksums were not found in the C<$existing> document

=item * The start and end checksums are not identical

=item * The checksum generated doesn't match the text between the start and end markers

=item * There is no valid C<CodeGen::Protection::Format::$type> module for C<$type>

=back

In short, C<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 C<get_userId> to C<get_user_id>" and thus breaking your
code.

=head1 TESTING

Note that C<CodeGen::Protection> manipulates documents (e.g., strings), but
does no I/O. So let's assume we've written the above document to
C<lib/My/OpenAPI/Controller/Users.pm>.  If you want to write a test to verify
that it's good, you use
L<Test::CodeGen::Protection|https://metacpan.org/pod/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;

=head1 AUTHOR

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

=head1 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.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.