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
masterbranch.
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Project Structure
- Architecture
- Configuration
- URL Routing
- Models
- Filters
- Controllers
- Authentication
- Database Support
- Templates
- Caching
- Email & Notifications
- Testing
- API Reference
- Resources
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.
- Clone the repository:
git clone https://github.com/your-repo/genelet-perl.git
cd genelet-perl
- Install runtime and test dependencies from
cpanfile:
cpanm --cpanfile cpanfile --installdeps .
- 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 |
|---|---|---|
Genelet::Access::Social::Google | OAuth2 | |
Genelet::Access::Social::Facebook | OAuth2 | |
| Github | Genelet::Access::Social::Github | OAuth2 |
| Microsoft | Genelet::Access::Social::Microsoft | OAuth2 |
Genelet::Access::Social::Linkedin | OAuth2 | |
| Zoom | Genelet::Access::Social::Zoom | OAuth2 |
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
- Developer Manual: http://www.genelet.com/index.php/2017/02/08/perl-development-manual/
- Tutorial: http://www.genelet.com/index.php/tutorial-perl/
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
- Fork the repository
- Create a feature branch
- Make your changes
- Run the test suite
- Submit a pull request