Genelet

May 15, 2026 · View on GitHub

Perl MVC Web Framework for Building REST APIs

Genelet is an Object-Oriented Perl MVC web framework designed for building large-scale REST API applications. It runs as CGI/FastCGI and provides built-in support for OAuth authentication, database operations, email notifications, and mobile push notifications.

Note: The legacy version of this framework is available in the master branch.

Table of Contents

Features

  • Strict MVC Architecture - Clean separation of concerns with Models, Views, and Controllers
  • Full REST API Support - Built-in support for GET, POST, PUT, DELETE, PATCH methods
  • Multiple Output Formats - JSON, XML, JSONP, HTML templates, and form-encoded responses
  • OAuth Integration - Built-in OAuth1 (Twitter) and OAuth2 (Google, Facebook, Github, Microsoft, LinkedIn, Zoom)
  • Database Abstraction - Support for MySQL, PostgreSQL, and SQLite with stored procedures
  • CSRF Protection - Built-in cross-site request forgery protection
  • Pagination - Automatic pagination with configurable page sizes
  • Related Data Fetching - Nextpages system for automatically fetching related data
  • Caching - File-based caching with automatic invalidation
  • Scaffolding - Generate project structure from database tables
  • CGI/FastCGI - Run in CGI mode for development, FastCGI for production speed

Requirements

Minimum Perl Version

  • Perl 5.10 or higher

Debian/Ubuntu Build Prerequisites

Some CPAN modules build against system libraries. On Debian or Ubuntu, install the common build tools and headers first:

sudo apt-get update
sudo apt-get install -y build-essential cpanminus pkg-config \
  libssl-dev zlib1g-dev \
  libsqlite3-dev default-libmysqlclient-dev libpq-dev

CPAN Dependencies

Runtime modules used by the framework:

cpanm CGI CGI::Fast DBI Digest::HMAC_SHA1 JSON LWP::UserAgent \
  HTTP::Request::Common HTTP::Response MIME::Base64 MIME::Lite \
  Net::SMTP Net::SSLeay Template URI

Test modules:

cpanm Test::More Test::Class DBD::SQLite

Optional database drivers:

cpanm DBD::SQLite   # SQLite
cpanm DBD::Pg       # PostgreSQL
cpanm DBD::mysql    # MySQL

Installation

This repository is prepared for GitHub source releases. It includes cpanfile dependency metadata, but does not currently include Makefile.PL, Build.PL, or META.* metadata for CPAN uploads.

  1. Clone the repository:
git clone https://github.com/your-repo/genelet-perl.git
cd genelet-perl
  1. Install runtime and test dependencies from cpanfile:
cpanm --cpanfile cpanfile --installdeps .
  1. Install any optional database drivers you need:
cpanm DBD::SQLite   # SQLite
cpanm DBD::Pg       # PostgreSQL
cpanm DBD::mysql    # MySQL

Quick Start

Generate a New Project

Use the help.pl scaffolding tool to generate a project from your database tables:

perl help.pl \
    --dbtype mysql \
    --dbname mydb \
    --dbuser root \
    --dbpass secret \
    --project MyApp \
    --script /cgi-bin/app.cgi \
    --dir ~/myapp \
    users posts comments

This generates:

  • Configuration files
  • Model and Filter classes for each table
  • HTML templates for CRUD operations
  • CGI script entry point

Options

OptionDescriptionDefault
--dirProject root directory$HOME/tutoperl
--dbtypeDatabase type (mysql, sqlite)mysql
--dbnameDatabase name(required)
--dbuserDatabase username(empty)
--dbpassDatabase password(empty)
--projectProject namemyproject
--scriptCGI script pathmyscript
--forceOverwrite existing filesfalse
--angularInclude Angular 1.3 filesfalse

Generated Project Structure

myapp/
├── bin/
│   └── app.cgi              # CGI entry point
├── conf/
│   └── config.json          # Configuration file
├── lib/
│   └── MyApp/
│       ├── Model.pm         # Base model class
│       ├── Filter.pm        # Base filter class
│       ├── Beacon.pm        # Beacon class
│       ├── Users/
│       │   ├── Model.pm     # Users model
│       │   ├── Filter.pm    # Users filter
│       │   └── component.json
│       ├── Posts/
│       │   ├── Model.pm
│       │   ├── Filter.pm
│       │   └── component.json
│       └── Comments/
│           ├── ...
├── views/
│   ├── admin/
│   │   ├── login.html
│   │   ├── error.html
│   │   └── users/
│   │       ├── topics.html
│   │       ├── edit.html
│   │       └── ...
│   └── public/
│       └── error.html
├── logs/
│   └── debug.log
└── www/                     # (if --angular flag used)
    └── ...

Architecture

Class Hierarchy

Genelet::Accessor (base accessor/mutator generator)
    └── Genelet::DBI (database operations)
        └── Genelet::Crud (CRUD operations)
            └── Genelet::Model (business logic)

Genelet::Base (HTTP utilities, logging, HMAC signatures)
    ├── Genelet::Controller (request routing, MVC orchestration)
    ├── Genelet::Filter (action validation, pre/post hooks)
    └── Genelet::Access (authentication, sessions)
        └── Genelet::Access::Social/* (OAuth implementations)

Request Flow

HTTP Request


┌─────────────────────────────────────────────────────┐
│  CGIController::run()                               │
│  - Parse URL: /role/tag/component?action=X          │
│  - Verify authentication (cookie/OAuth)             │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│  Controller::handler()                              │
│  - Initialize Filter and Model                      │
│  - Parse request parameters                         │
│  - Handle file uploads                              │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│  Filter::preset()                                   │
│  - CSRF validation                                  │
│  - Custom pre-validation logic                      │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│  Filter::validate()                                 │
│  - Check required fields                            │
│  - HTTP method validation                           │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│  Filter::before()                                   │
│  - Pre-action custom logic                          │
│  - Modify extra conditions                          │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│  Model::$action()                                   │
│  - Execute business logic                           │
│  - Database operations                              │
│  - Process nextpages (related data)                 │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│  Filter::after()                                    │
│  - Post-action custom logic                         │
│  - Generate CSRF token                              │
│  - Execute oncepages                                │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│  Controller (View Rendering)                        │
│  - JSON/XML/JSONP/HTML output                       │
│  - Cache management                                 │
└─────────────────────────────────────────────────────┘


HTTP Response

Configuration

config.json

{
  "Project": "MyApp",
  "Document_root": "/var/www/myapp",
  "Script": "/cgi-bin/app.cgi",
  "Server_url": "https://example.com",
  "Secret": "your-secret-key-here",
  "Pubrole": "public",

  "Db": ["DBI:mysql:mydb", "username", "password"],

  "Roles": {
    "admin": {
      "id_name": "user_id",
      "is_admin": true,
      "attributes": ["user_id", "email", "role"],
      "duration": 86400
    },
    "member": {
      "id_name": "member_id",
      "attributes": ["member_id", "name"],
      "duration": 3600
    }
  },

  "Chartags": {
    "html": {
      "Content_type": "text/html; charset=UTF-8",
      "Short": "html"
    },
    "json": {
      "Content_type": "application/json; charset=UTF-8",
      "Short": "json"
    },
    "xml": {
      "Content_type": "application/xml; charset=UTF-8",
      "Short": "xml"
    }
  },

  "Comps": ["Users", "Posts", "Comments"]
}

component.json (per component)

{
  "current_table": "posts",
  "current_key": "post_id",
  "current_id_auto": "post_id",

  "insert_pars": ["title", "content", "user_id", "created_at"],
  "update_pars": ["post_id", "title", "content", "updated_at"],
  "edit_pars": ["post_id", "title", "content", "user_id", "created_at"],
  "topics_pars": ["post_id", "title", "user_id", "created_at"],

  "actions": {
    "topics": {
      "groups": ["admin", "member", "public"],
      "validate": []
    },
    "edit": {
      "groups": ["admin", "member"],
      "validate": ["post_id"]
    },
    "insert": {
      "groups": ["admin", "member"],
      "validate": ["title", "content"]
    },
    "update": {
      "groups": ["admin", "member"],
      "validate": ["post_id", "title"]
    },
    "delete": {
      "groups": ["admin"],
      "validate": ["post_id"]
    }
  },

  "nextpages": {
    "topics": [
      {
        "model": "Comments",
        "action": "topics",
        "relate_fk": "post_id"
      }
    ],
    "edit": [
      {
        "model": "Users",
        "action": "edit",
        "relate_item": {"user_id": "user_id"}
      }
    ]
  }
}

URL Routing

URL Structure

/{role}/{tag}/{component}?action={action}&{params}
SegmentDescriptionExample
roleUser role or OAuth provideradmin, public, google
tagOutput formathtml, json, xml, jsonp
componentResource/model nameusers, posts
actionOperation to performtopics, edit, insert

Examples

# List all posts as JSON for admin role
GET /admin/json/posts?action=topics

# Get single post as HTML
GET /member/html/posts?action=edit&post_id=123

# Create new post (REST style)
POST /admin/json/posts
Content-Type: application/json
{"title": "Hello", "content": "World"}

# Update post
PUT /admin/json/posts
{"post_id": 123, "title": "Updated Title"}

# Delete post
DELETE /admin/json/posts?post_id=123

REST Default Actions

HTTP MethodDefault Action
GETtopics
GET (with ID)edit
POSTinsert
PUTupdate
DELETEdelete
PATCHinsupd

Models

Creating a Model

package MyApp::Posts::Model;

use strict;
use Genelet::Model;
use Genelet::Crud;
use Genelet::Mysql;  # or Genelet::Pg, Genelet::SQLite

our @ISA = qw(Genelet::Model Genelet::Crud Genelet::Mysql);

# Custom method
sub publish {
    my $self = shift;
    my $extra = shift || {};

    my $ARGS = $self->{ARGS};
    my $post_id = $ARGS->{post_id} or return 1040;

    # Update status
    my $err = $self->do_sql(
        "UPDATE posts SET status = 'published', published_at = NOW() WHERE post_id = ?",
        $post_id
    );
    return $err if $err;

    # Fetch updated record
    $self->{LISTS} = [];
    $err = $self->edit_hash(
        $self->{LISTS},
        ['post_id', 'title', 'status', 'published_at'],
        'post_id',
        $post_id
    );

    return $err;
}

# Override topics with custom logic
sub topics {
    my $self = shift;
    my $extra = shift || {};

    # Add default filter for non-admins
    unless ($self->{ARGS}->{_gadmin}) {
        $extra->{status} = 'published';
    }

    return $self->SUPER::topics($extra, @_);
}

1;

Built-in Model Actions

topics (SELECT multiple)

$model->topics_pars(['id', 'name', 'created_at']);
$model->args({rowcount => 20, pageno => 1, sortby => 'created_at', sortreverse => 1});
my $err = $model->topics({status => 'active'});
my $lists = $model->lists();  # Array of hashes

edit (SELECT single)

$model->edit_pars(['id', 'name', 'email', 'bio']);
$model->args({id => 123});
my $err = $model->edit();
my $record = $model->lists()->[0];

insert (INSERT)

$model->insert_pars(['name', 'email', 'created_at']);
$model->args({name => 'John', email => 'john@example.com'});
my $err = $model->insert();
my $new_id = $model->lists()->[0]->{id};  # If current_id_auto is set

update (UPDATE)

$model->update_pars(['id', 'name', 'email']);
$model->args({id => 123, name => 'Jane'});
my $err = $model->update();

delete (DELETE)

$model->args({id => 123});
my $err = $model->delete();

insupd (INSERT or UPDATE)

$model->insupd_pars(['email']);  # Unique constraint field
$model->args({email => 'john@example.com', name => 'John'});
my $err = $model->insupd();  # Inserts if not exists, updates if exists

Automatically fetch related data after the main query:

{
  "nextpages": {
    "edit": [
      {
        "model": "Comments",
        "action": "topics",
        "relate_fk": "post_id",
        "args": ["limit"]
      }
    ]
  }
}
# In Filter::before(), you can set extra conditions for nextpages
sub before {
    my $self = shift;
    my ($form, $extra, $nextextras, $onceextras) = @_;

    # Pass extra conditions to first nextpage
    $nextextras->[0] = {status => 'approved'};

    return;
}

JOIN Queries

$model->current_tables([
    {name => 'posts', alias => 'p'},
    {name => 'users', alias => 'u', type => 'LEFT', on => 'p.user_id = u.user_id'}
]);
$model->topics_pars({
    'p.post_id' => 'post_id',
    'p.title' => 'title',
    'u.name' => 'author_name'
});

Stored Procedures

# Call procedure with output parameters
my $hash = {};
my $err = $model->do_proc($hash, ['total', 'average'], 'calculate_stats', $user_id, $start_date, $end_date);
print "Total: $hash->{total}, Average: $hash->{average}\n";

# Call procedure returning result sets
my $lists = [];
$err = $model->select_proc($lists, 'get_user_posts', $user_id);

Filters

Filters handle validation, authentication hooks, and pre/post processing.

Creating a Filter

package MyApp::Posts::Filter;

use strict;
use Genelet::Filter;

our @ISA = qw(Genelet::Filter);

# Called before validation
sub preset {
    my $self = shift;
    my $ARGS = $self->{ARGS};

    # Skip CSRF for public role
    return if ($self->{PUBROLE} eq $ARGS->{_gwho});

    # Custom validation
    if ($ARGS->{_gaction} eq 'publish') {
        return 1021 unless $ARGS->{_gadmin};  # Admin only
    }

    return $self->SUPER::preset();
}

# Called before the action method
sub before {
    my $self = shift;
    my ($form, $extra, $nextextras, $onceextras) = @_;

    my $ARGS = $self->{ARGS};

    # Force user_id for non-admins
    unless ($ARGS->{_gadmin}) {
        $extra->{user_id} = $ARGS->{$ARGS->{_gidname}};
    }

    # Set created_at for inserts
    if ($ARGS->{_gaction} eq 'insert') {
        $ARGS->{created_at} = time();
    }

    return;
}

# Called after the action method
sub after {
    my $self = shift;
    my $form = shift;

    my $ARGS = $self->{ARGS};

    # Send notification email on new post
    if ($ARGS->{_gaction} eq 'insert') {
        $form->{OTHER}->{notification} = {
            File => 'emails/new_post.html',
            To => 'admin@example.com',
            Subject => 'New Post Created'
        };
    }

    return $self->SUPER::after($form, @_);
}

1;

Filter Hooks

HookWhen CalledUse Case
preset()Before validationCSRF checks, early authorization
validate()After presetField validation (usually inherited)
before()Before actionModify query conditions, set defaults
after()After actionSend emails, modify response

CSRF Token Generation

# In your template (e.g., edit.html)
<input type="hidden" name="csrf" value="[% csrf %]">

# The token is automatically generated in Filter::after()

Controllers

The Controller orchestrates the MVC flow. You typically don't need to modify it, but you can extend it:

package MyApp::Controller;

use strict;
use Genelet::CGIController;

our @ISA = qw(Genelet::CGIController);

# Custom error handling
sub error_page {
    my $self = shift;
    my ($filter, $ARGS, $error) = @_;

    # Log all errors
    $self->error("Error $error for action $ARGS->{_gaction}");

    return $self->SUPER::error_page($filter, $ARGS, $error);
}

1;

Authentication

# In your config
"Roles": {
    "admin": {
        "id_name": "admin_id",
        "is_admin": true,
        "attributes": ["admin_id", "username", "email"],
        "duration": 86400,
        "secret": "admin-secret-key",
        "surface": "admin_session",
        "coding": "base64"
    }
}

Database Authentication (Login)

package MyApp::Login::Model;

use strict;
use Genelet::Access::DBI;

our @ISA = qw(Genelet::Access::DBI);

sub authenticate {
    my $self = shift;
    my ($login, $password) = @_;

    my $sql = "SELECT admin_id, username, email FROM admins WHERE username = ? AND password = SHA2(?, 256)";
    my $lists = [];
    my $err = $self->select_sql($lists, $sql, $login, $password);
    return 1031 if ($err || !$lists->[0]);

    return $self->get_fields([
        $lists->[0]->{admin_id},
        $lists->[0]->{username},
        $lists->[0]->{email}
    ]);
}

1;

OAuth2 Authentication (Google Example)

# In config.json
"Dbis": {
    "member": {
        "google": {
            "class": "Genelet::CGIAccess::Google",
            "client_id": "your-google-client-id",
            "client_secret": "your-google-client-secret",
            "callback_url": "https://example.com/cgi-bin/app.cgi/member/html/google",
            "scope": "email profile",
            "in_pars": ["email", "name", "picture"]
        }
    }
}

Available OAuth Providers

ProviderClassOAuth Version
GoogleGenelet::Access::Social::GoogleOAuth2
FacebookGenelet::Access::Social::FacebookOAuth2
GithubGenelet::Access::Social::GithubOAuth2
MicrosoftGenelet::Access::Social::MicrosoftOAuth2
LinkedInGenelet::Access::Social::LinkedinOAuth2
ZoomGenelet::Access::Social::ZoomOAuth2
TwitterGenelet::Access::Social::TwitterOAuth1

Database Support

MySQL

use Genelet::Mysql;
our @ISA = qw(Genelet::Model Genelet::Crud Genelet::Mysql);

PostgreSQL

use Genelet::Pg;
our @ISA = qw(Genelet::Model Genelet::Crud Genelet::Pg);

SQLite

use Genelet::SQLite;
our @ISA = qw(Genelet::Model Genelet::Crud Genelet::SQLite);

Raw SQL Queries

# SELECT
my $lists = [];
my $err = $model->select_sql($lists,
    "SELECT * FROM posts WHERE status = ? ORDER BY created_at DESC LIMIT ?",
    'published', 10
);

# INSERT/UPDATE/DELETE
$err = $model->do_sql(
    "UPDATE posts SET views = views + 1 WHERE post_id = ?",
    $post_id
);

# Get single row into hash
my $hash = {};
$err = $model->get_args($hash,
    "SELECT COUNT(*) as total FROM posts WHERE user_id = ?",
    $user_id
);
print "Total posts: $hash->{total}\n";

Templates

Genelet uses Template Toolkit for HTML rendering.

Template Location

Templates are stored in views/{role}/{component}/{action}.{tag}:

views/admin/posts/topics.html
views/admin/posts/edit.html
views/public/posts/topics.html

Template Variables

<!-- views/admin/posts/topics.html -->
<!DOCTYPE html>
<html>
<head><title>Posts</title></head>
<body>
    <h1>Posts List</h1>

    <!-- Pagination info -->
    <p>Page [% pageno %] of [% maxpageno %] (Total: [% totalno %])</p>

    <!-- Data list -->
    <table>
        <tr><th>ID</th><th>Title</th><th>Author</th><th>Actions</th></tr>
        [% FOREACH post IN data %]
        <tr>
            <td>[% post.post_id %]</td>
            <td>[% post.title | html %]</td>
            <td>[% post.author_name | html %]</td>
            <td>
                <a href="[% g_script %]/[% g_role %]/html/posts?action=edit&post_id=[% post.post_id %]">Edit</a>
            </td>
        </tr>
        [% END %]
    </table>

    <!-- CSRF token for forms -->
    <form method="POST" action="[% g_script %]/[% g_role %]/json/posts">
        <input type="hidden" name="csrf" value="[% csrf %]">
        <input type="hidden" name="action" value="insert">
        <input type="text" name="title" placeholder="Title">
        <textarea name="content"></textarea>
        <button type="submit">Create Post</button>
    </form>

    <!-- Related data from nextpages -->
    [% IF relationships.Comments_topics %]
    <h2>Recent Comments</h2>
    [% FOREACH comment IN relationships.Comments_topics %]
        <p>[% comment.content | html %]</p>
    [% END %]
    [% END %]
</body>
</html>

Available Template Variables

VariableDescription
dataMain result list (array)
relationshipsRelated data from nextpages/oncepages
csrfCSRF token
g_roleCurrent role
g_tagCurrent output tag
g_componentCurrent component
g_actionCurrent action
g_scriptScript URL path
totalnoTotal record count
pagenoCurrent page number
maxpagenoTotal pages
incoming.*Request parameters

Caching

Enable Caching

# In your controller setup
use Genelet::Cache;

my $cache = Genelet::Cache->new(
    document_root => '/var/www/myapp',
    config => {
        'admin/posts/topics' => {
            keys => ['pageno', 'sortby'],
            ttl => 3600
        }
    }
);
$controller->cache($cache);

Cache Invalidation

Cache is automatically invalidated when:

  • INSERT, UPDATE, or DELETE actions are performed
  • You can manually clear cache in Filter::after()
sub after {
    my $self = shift;
    my $form = shift;

    # Force cache clear
    if ($self->{ARGS}->{_gaction} eq 'update') {
        # Cache will be cleared by Controller automatically
    }

    return $self->SUPER::after($form, @_);
}

Email & Notifications

SMTP Email

# In config.json
"Blks": {
    "smtp": {
        "class": "Genelet::SMTP",
        "Server": "smtp.example.com",
        "From": "noreply@example.com"
    }
}

# In Filter::after()
$form->{OTHER}->{smtp} = {
    To => 'user@example.com',
    Subject => 'Welcome!',
    File => 'emails/welcome.html'  # Template file
};

Postmark Email

"Blks": {
    "postmark": {
        "class": "Genelet::Postmark",
        "api_key": "your-postmark-api-key",
        "From": "noreply@example.com"
    }
}

Push Notifications

Apple Push Notification (APNS)

use Genelet::APNS;
my $apns = Genelet::APNS->new(cert_file => 'cert.pem', key_file => 'key.pem');
$apns->send($device_token, {alert => 'Hello!', badge => 1});

Google Cloud Messaging (GCM)

use Genelet::GCM;
my $gcm = Genelet::GCM->new(api_key => 'your-gcm-api-key');
$gcm->send($registration_id, {message => 'Hello!'});

Testing

The default test suite runs against temporary SQLite databases:

prove -I. -r Genelet/Test

DB-backed tests use SQLite by default. They can also run against Docker PostgreSQL or MySQL by setting GENELET_TEST_DSN, GENELET_TEST_USER, and GENELET_TEST_PASS; see TESTING.md for those flows.

Writing Tests

#!/usr/bin/perl
use strict;
use warnings;
use Test::More tests => 10;
use DBI;

use lib '.';
use lib '../..';

# Create test model
package TestModel;
use Genelet::Model;
use Genelet::Crud;
use Genelet::SQLite;
our @ISA = qw(Genelet::Model Genelet::Crud Genelet::SQLite);

package main;

# Setup test database
my $dbh = DBI->connect("dbi:SQLite:dbname=:memory:") or die $!;
$dbh->do("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");

# Create model instance
my $model = TestModel->new(
    dbh => $dbh,
    current_table => 'test',
    current_key => 'id',
    current_id_auto => 'id',
    insert_pars => ['name'],
    topics_pars => ['id', 'name']
);

# Test insert
$model->args({name => 'Test'});
my $err = $model->insert();
ok(!$err, 'Insert succeeded');
is($model->lists()->[0]->{id}, 1, 'Auto ID is 1');

# Test topics
$model->args({});
$err = $model->topics();
ok(!$err, 'Topics succeeded');
is(scalar(@{$model->lists()}), 1, 'One record found');

$dbh->disconnect;

API Reference

Error Codes

CodeDescription
1020Login required
1021Not authorized
1022Login expired
1031Login incorrect
1035Required field missing
1038HTTP method not allowed
1046CSRF token not found
1047CSRF token mismatch
1072Database connection failed
1073SQL execution failed
1171Insert failed
1172Delete failed
1173Update failed
1174Select failed

Utility Functions (Genelet::Utils)

use Genelet::Utils;

# Token generation
my $token = token($timestamp, $secret, @data);
my $time = get_tokentime($token);
my $valid = check_token($token, $secret, @data);

# Date/time
my $now = now_from_unix();           # "2024-01-15 10:30:00"
my $day = day_from_unix();           # "2024-01-15"
my $us_day = usday_from_unix();      # "January 15, 2024"
my $tomorrow = day_for_tomorrow();   # "2024-01-16"
my $rfc822 = rfc822_time();          # "Mon, 15 Jan 2024 10:30:00 PST"
my $unix = unix_from_now("2024-01-15 10:30:00");

# Random strings
my $pw = randompw(12);    # Random password
my $hex = randomhex(32);  # Random hex string

# IP conversion
my $int = ipint("192.168.1.1");      # Integer representation
my $str = ipstr($int);               # Back to string

Resources

Virtual Hosting

Genelet runs in CGI mode by default. For production, configure FastCGI using Apache's mod_fcgid:

<Directory "/var/www/myapp/cgi-bin">
    Options +ExecCGI
    SetHandler fcgid-script
    FCGIWrapper /usr/bin/perl .cgi
</Directory>

Many virtual hosting services run PHP under mod_fcgid, so Genelet can achieve similar performance. Develop in CGI mode for easier debugging, then switch to FastCGI for production.

License

GNU Lesser General Public License v2.1 (LGPL-2.1)

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Run the test suite
  5. Submit a pull request