CULTIVA IA · IA-Ingeniería-MLOps
patrones-de-perl-moderno

Patrones de Perl Moderno 5.36+

DataPulse Analytics — Meta Ads Connector Refactorización legacy → moderno 12 patrones aplicados
🐪
Versión objetivo
Perl 5.36+
📦
Módulos clave
Moo · Try::Tiny
🏢
Cliente
DataPulse SaaS
Patrones
12 aplicados
1
Pragma use v5.36 — Reemplaza el boilerplate antiguo
core · siempre aplicar
Legacy (pre-5.36)
# DataPulse::Connector::Meta — código heredado
use strict;
use warnings;
use feature 'say', 'signatures', 'state';
no warnings 'experimental::signatures';

sub connect_meta {
    my ($app_id, $secret) = @_;
    # ...
}
Moderno (5.36+)
# Una sola línea activa todo
use v5.36;
# → strict ON
# → warnings ON
# → say, state, signatures ON
# → no experimental warnings

sub connect_meta($app_id, $secret) {
    # firma limpia, arity checked
}
2
OO Moderno con Moo — Atributos tipados y declarativos
DataPulse::Connector::Meta
📄 lib/DataPulse/Connector/Meta.pm — clase Moo Moo 2.005+
package DataPulse::Connector::Meta;
use v5.36;
use Moo;
use Types::Standard qw(Str Int Bool);
use namespace::autoclean;

# Atributos declarativos con tipos y defaults
has app_id      => (is => 'ro', isa => Str,  required => 1);
has app_secret  => (is => 'ro', isa => Str,  required => 1);
has access_token=> (is => 'rw', isa => Str,  default  => sub { '' });
has api_version => (is => 'ro', isa => Str,  default  => sub { 'v19.0' });
has max_retries => (is => 'ro', isa => Int,  default  => sub { 3 });
has debug       => (is => 'ro', isa => Bool, default  => sub { 0 });

# Constantes — mejor que variables globales mutables ($TIMEOUT)
use constant {
    BASE_URL         => 'https://graph.facebook.com',
    ERROR_RATE_LIMIT => 17,
    ERROR_EXPIRED    => 190,
};
3
Firmas de subrutina + Try::Tiny — Parámetros con defaults y excepciones
authenticate · get_campaign_insights
🔐 Método authenticate — firma + Try::Tiny Try::Tiny 0.31+
# MODERNO: firma con $self, parámetro opcional con default
sub get_campaign_insights($self, $campaign_id, $date_range = undef) {

    # Try::Tiny: bloques try/catch explícitos (sin $@ mágico)
    my $token = try {
        my $raw = $self->_fetch_token();
        DataPulse::Util::sanitize_token($raw);    # puede morir
    }
    catch {
        warn "[Meta] Auth error: $_";
        undef;    # valor de retorno del catch
    };

    # Retry con back-off — label de bucle explícito
    my $attempt = 0;
    RETRY: while ($attempt < $self->max_retries) {
        $attempt++;
        my $res = try { $self->_http_get(...) }
                  catch { warn "HTTP err: $_"; undef };

        next RETRY unless defined $res;
        return $self->_parse_insights($res);
    }
}
4
Regex con /x y capturas nombradas — Parseando errores de Meta API
qr// precompilado · $+{name}
Positional (difícil mantener)
# $1, $2, $3 — ¿qué era cada uno?
if ($res =~ /"error".*?"code":(\d+).*?"message":"([^"]+)"/s) {
    my ($code, $msg) = ($1, $2);
    warn "Error $code: $msg";
}
Nombrado + /x (legible)
# qr// compilado una sola vez al cargar
my $meta_err_re = qr{
    "error"\s*:\s*\{
        .*?
        "code"\s*:\s*(?<code> \d+ )
        .*?
        "message"\s*:\s*"(?<message> [^"]+ )"
    \}
}xs;

if ($res =~ $meta_err_re) {
    my ($code, $msg) = @+{qw(code message)};
}
5
DataPulse::Util — Funciones exportables con firmas
trim · slugify · format_currency · sanitize_token
🔧 lib/DataPulse/Util.pm Exporter nativo
package DataPulse::Util;
use v5.36;
use Exporter 'import';      # solo importar 'import', no todo Exporter

our @EXPORT_OK   = qw(trim slugify format_currency sanitize_token);
our %EXPORT_TAGS = (all => \@EXPORT_OK);

# /r devuelve copia modificada (no muta $str)
sub trim($str) { $str =~ s/^\s+|\s+$//gr }

# Normaliza acentos + guiones para slugs de campañas
sub slugify($text) {
    my $s = lc $text;
    $s =~ s/[áàäâ]/a/g; $s =~ s/[éèëê]/e/g;
    $s =~ s/[íìïî]/i/g;  $s =~ s/[óòöô]/o/g;
    $s =~ s/ñ/n/g;         $s =~ s/[^a-z0-9]+/-/g;
    $s =~ s/^-|-$//g;     $s
}

# Parámetros opcionales con default en la firma
sub format_currency($amount, $symbol = '€', $decimals = 2) {
    sprintf "%s %.*f", $symbol, $decimals, $amount
}

# Regex /x con captura nombrada para validar tokens OAuth
my $token_re = qr{
    ^ (?<token> [A-Za-z0-9\-_\.~]+ ) $    # chars válidos OAuth
}x;

sub sanitize_token($token) {
    return $+{token} if $token =~ $token_re;
    die "Token inválido: caracteres no permitidos\n";
}
1;
6
Tabla de referencia rápida — Legacy vs Moderno
12 sustituciones directas
Patrón Legacy Sustitución Moderna Versión
use strict; use warnings; use v5.36; 5.36+
my ($x, $y) = @_; sub foo($x, $y) { ... } 5.36+
@{ $ref } $ref->@* 5.24+
open FH, "< $file" open my $fh, '<:encoding(UTF-8)', $file 5.6+
bless \%args, $class Moo + Types::Standard CPAN
$1, $2, $3 (capturas) $+{nombre} (capturas nombradas) 5.10+
eval { }; if ($@) Try::Tiny / try/catch (5.40+) CPAN / 5.40
BEGIN { require Exporter } use Exporter 'import'; 5.8+
blessed($o) && $o->isa('X') $o isa 'X' 5.32+
our $TIMEOUT = 30; use constant TIMEOUT => 30; 5.0+
eval "require $module" Module::Runtime::require_module($m) CPAN
open FH, $filename Path::Tiny->new($file)->slurp_utf8 CPAN
7
Anti-patrones encontrados en DataPulse — y sus correcciones
código legacy auditado
✗ open FH, $filename (shell injection)
✓ open my $fh, '<:encoding(UTF-8)', $filename or use Path::Tiny
Riesgo de inyección si $filename viene de input externo (parámetros GET, webhooks Meta)
✗ my $obj = new Foo(...) (indirect object syntax)
✓ my $obj = Foo->new(...)
El parser de Perl puede malinterpretar ambiguamente en ciertos contextos
✗ eval "require $module" (code injection)
✓ Module::Runtime::require_module($module)
El módulo era cargado dinámicamente desde config.json sin sanitización
✗ our $TIMEOUT = 30; (global mutable)
✓ use constant TIMEOUT => 30; o atributo Moo con default
La variable global era sobreescrita accidentalmente en tests de integración
✗ no strict 'refs' (casi siempre incorrecto)
✓ Usar hash %registry = () en su lugar
Se usaba para un registro dinámico de plugins — reemplazado con hash léxico
8
Stack de módulos CPAN recomendado — cpanfile
battle-tested · con versión mínima
Moo ≥ 2.005
OO ligero con tipos. Alternativa a Moose sin el metaprotocolo completo.
Types::Standard
Str, Int, Bool, ArrayRef[Str]... coerciones incluidas.
Try::Tiny
try/catch semántico. Fix del $@ mágico de Perl que se puede perder.
Path::Tiny
API fluida para I/O: slurp_utf8, spew, children(qr/\.pm$/).
JSON::MaybeXS
Wrapper XS con fallback puro. encode_json / decode_json.
Test2::V0
Suite de tests moderna. Reemplaza Test::More con mejor diagnóstico.
📦 cpanfile — dependencias declaradas
requires 'Moo',          '>= 2.005';
requires 'Types::Standard';
requires 'Try::Tiny';
requires 'Path::Tiny';
requires 'JSON::MaybeXS';
requires 'namespace::autoclean';
requires 'Module::Runtime';

on test => sub {
    requires 'Test2::V0';
    requires 'Test::MockModule';
};