Skip to content

Latest commit

 

History

72 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Genelet

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

Option Description Default
--dir Project root directory $HOME/tutoperl
--dbtype Database type (mysql, sqlite) mysql
--dbname Database name (required)
--dbuser Database username (empty)
--dbpass Database password (empty)
--project Project name myproject
--script CGI script path myscript
--force Overwrite existing files false
--angular Include Angular 1.3 files false

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}
Segment Description Example
role User role or OAuth provider admin, public, google
tag Output format html, json, xml, jsonp
component Resource/model name users, posts
action Operation to perform topics, 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 Method Default Action
GET topics
GET (with ID) edit
POST insert
PUT update
DELETE delete
PATCH insupd

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

Nextpages (Related Data)

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

Hook When Called Use Case
preset() Before validation CSRF checks, early authorization
validate() After preset Field validation (usually inherited)
before() Before action Modify query conditions, set defaults
after() After action Send 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

Cookie-Based 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

Provider Class OAuth Version
Google Genelet::Access::Social::Google OAuth2
Facebook Genelet::Access::Social::Facebook OAuth2
Github Genelet::Access::Social::Github OAuth2
Microsoft Genelet::Access::Social::Microsoft OAuth2
LinkedIn Genelet::Access::Social::Linkedin OAuth2
Zoom Genelet::Access::Social::Zoom OAuth2
Twitter Genelet::Access::Social::Twitter OAuth1

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

Variable Description
data Main result list (array)
relationships Related data from nextpages/oncepages
csrf CSRF token
g_role Current role
g_tag Current output tag
g_component Current component
g_action Current action
g_script Script URL path
totalno Total record count
pageno Current page number
maxpageno Total 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

Code Description
1020 Login required
1021 Not authorized
1022 Login expired
1031 Login incorrect
1035 Required field missing
1038 HTTP method not allowed
1046 CSRF token not found
1047 CSRF token mismatch
1072 Database connection failed
1073 SQL execution failed
1171 Insert failed
1172 Delete failed
1173 Update failed
1174 Select 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

About

Perl Version of the Genelet Web Development Framework

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages