Lua - Libraries & LuaRocks

Libraries & LuaRocks

Time: ~30 minutes

Explore the Lua library ecosystem, learn to use LuaRocks package manager, discover essential libraries for various domains, and understand how to create and distribute your own Lua libraries.

Learning Objectives

  • Understand the Lua library ecosystem and package management
  • Install and use LuaRocks package manager
  • Work with essential Lua libraries for different domains
  • Create and structure your own Lua libraries
  • Publish packages to LuaRocks repository
  • Manage dependencies and version compatibility

LuaRocks Package Manager

Installation and Setup

# Ubuntu/Debian
sudo apt-get install luarocks

# macOS with Homebrew
brew install luarocks

# Windows - Download from luarocks.org
# Or install via scoop: scoop install luarocks

# Verify installation
luarocks --version

Basic LuaRocks Commands

# Search for packages
luarocks search json

# Install a package
luarocks install lua-cjson

# Install specific version
luarocks install penlight 1.13.1

# List installed packages
luarocks list

# Show package information
luarocks show lua-cjson

# Remove a package
luarocks remove lua-cjson

# Update package index
luarocks update

Using Installed Libraries

-- After installing with: luarocks install lua-cjson
local cjson = require("cjson")

-- JSON encoding
local data = {
    name = "John Doe",
    age = 30,
    skills = {"Lua", "Python", "JavaScript"},
    active = true
}

local json_string = cjson.encode(data)
print("JSON:", json_string)

-- JSON decoding
local decoded = cjson.decode(json_string)
print("Name:", decoded.name)
print("Skills:", table.concat(decoded.skills, ", "))

Expected Output:

JSON: {"active":true,"age":30,"name":"John Doe","skills":["Lua","Python","JavaScript"]}
Name: John Doe
Skills: Lua, Python, JavaScript

Essential Libraries by Domain

Web Development Libraries

# HTTP client library
luarocks install http

# Web framework
luarocks install lapis

# URL parsing
luarocks install net-url
-- HTTP Client Example
local http_request = require("http.request")
local cjson = require("cjson")

-- Make HTTP GET request
local request = http_request.new_from_uri("https://api.github.com/users/octocat")
local headers, stream = request:go()

if headers:get(":status") == "200" then
    local body = stream:get_body_as_string()
    local user_data = cjson.decode(body)
    
    print("GitHub User Info:")
    print("Login:", user_data.login)
    print("Name:", user_data.name)
    print("Public Repos:", user_data.public_repos)
    print("Followers:", user_data.followers)
else
    print("HTTP Error:", headers:get(":status"))
end

-- URL parsing example
local url = require("net.url")

local parsed = url.parse("https://example.com:8080/path?query=value#fragment")
print("Host:", parsed.host)
print("Port:", parsed.port)
print("Path:", parsed.path)
print("Query:", parsed.query)

File System and OS Libraries

# File system operations
luarocks install luafilesystem

# System utilities
luarocks install lua-system

# Path manipulation
luarocks install luapath
local lfs = require("lfs")
local path = require("path")

-- Directory operations
print("Current directory:", lfs.currentdir())

-- Create directory if it doesn't exist
local test_dir = "test_directory"
if not lfs.attributes(test_dir) then
    lfs.mkdir(test_dir)
    print("Created directory:", test_dir)
end

-- List directory contents
print("\nDirectory contents:")
for file in lfs.dir(".") do
    if file ~= "." and file ~= ".." then
        local attr = lfs.attributes(file)
        local file_type = attr.mode == "directory" and "DIR" or "FILE"
        print(string.format("%-4s %s", file_type, file))
    end
end

-- Path manipulation
local file_path = path.join("data", "config", "settings.json")
print("\nPath operations:")
print("Joined path:", file_path)
print("Directory:", path.dirname(file_path))
print("Filename:", path.basename(file_path))
print("Extension:", path.extension(file_path))

Data Processing Libraries

# Penlight utilities
luarocks install penlight

# CSV processing
luarocks install lua-csv

# Template engine
luarocks install lustache
-- Penlight - Swiss Army Knife for Lua
local pl = require("pl.import_into")()

-- String utilities
local text = "  Hello, World!  "
print("Trimmed:", pl.stringx.strip(text))
print("Split:", pl.stringx.split("a,b,c,d", ","))

-- List operations
local numbers = {1, 2, 3, 4, 5}
local doubled = pl.tablex.map(function(x) return x * 2 end, numbers)
print("Doubled:", pl.pretty.write(doubled, ""))

local sum = pl.tablex.reduce("+", numbers)
print("Sum:", sum)

-- CSV processing
local csv = require("csv")

-- Write CSV data
local csv_data = {
    {"Name", "Age", "City"},
    {"Alice", "25", "New York"},
    {"Bob", "30", "London"},
    {"Charlie", "35", "Tokyo"}
}

local csv_file = csv.open("people.csv", "w")
for i, row in ipairs(csv_data) do
    csv_file:write(row)
end
csv_file:close()

-- Read CSV data
print("\nReading CSV:")
csv_file = csv.open("people.csv", "r")
for fields in csv_file:lines() do
    print(string.format("%-10s %-5s %s", fields[1], fields[2], fields[3]))
end
csv_file:close()

-- Template processing with Lustache
local lustache = require("lustache")

local template = "Hello {{name}}! You have {{count}} {{#plural}}messages{{/plural}}{{^plural}}message{{/plural}}."
local data = {
    name = "Alice",
    count = 3,
    plural = true
}

local result = lustache:render(template, data)
print("\nTemplate result:", result)

Creating Your Own Library

Library Structure and Organization

-- mathutils.lua - A mathematical utilities library
local mathutils = {}

-- Module version
mathutils._VERSION = "1.0.0"
mathutils._DESCRIPTION = "Mathematical utilities for Lua"
mathutils._LICENSE = "MIT"

-- Private helper function
local function is_integer(n)
    return type(n) == "number" and n == math.floor(n)
end

-- Public functions
function mathutils.factorial(n)
    if not is_integer(n) or n < 0 then
        error("factorial requires non-negative integer")
    end
    
    if n <= 1 then
        return 1
    else
        return n * mathutils.factorial(n - 1)
    end
end

function mathutils.gcd(a, b)
    if not (is_integer(a) and is_integer(b)) then
        error("gcd requires integers")
    end
    
    while b ~= 0 do
        a, b = b, a % b
    end
    return math.abs(a)
end

function mathutils.lcm(a, b)
    if a == 0 or b == 0 then
        return 0
    end
    return math.abs(a * b) / mathutils.gcd(a, b)
end

function mathutils.is_prime(n)
    if not is_integer(n) or n < 2 then
        return false
    end
    
    if n == 2 then
        return true
    end
    
    if n % 2 == 0 then
        return false
    end
    
    for i = 3, math.sqrt(n), 2 do
        if n % i == 0 then
            return false
        end
    end
    
    return true
end

function mathutils.prime_factors(n)
    if not is_integer(n) or n < 2 then
        return {}
    end
    
    local factors = {}
    local d = 2
    
    while d * d <= n do
        while n % d == 0 do
            table.insert(factors, d)
            n = n / d
        end
        d = d + 1
    end
    
    if n > 1 then
        table.insert(factors, n)
    end
    
    return factors
end

-- Statistics functions
mathutils.stats = {}

function mathutils.stats.mean(numbers)
    if #numbers == 0 then
        return nil
    end
    
    local sum = 0
    for _, v in ipairs(numbers) do
        sum = sum + v
    end
    return sum / #numbers
end

function mathutils.stats.median(numbers)
    if #numbers == 0 then
        return nil
    end
    
    local sorted = {}
    for _, v in ipairs(numbers) do
        table.insert(sorted, v)
    end
    table.sort(sorted)
    
    local len = #sorted
    if len % 2 == 0 then
        return (sorted[len/2] + sorted[len/2 + 1]) / 2
    else
        return sorted[math.ceil(len/2)]
    end
end

function mathutils.stats.mode(numbers)
    if #numbers == 0 then
        return nil
    end
    
    local counts = {}
    for _, v in ipairs(numbers) do
        counts[v] = (counts[v] or 0) + 1
    end
    
    local max_count = 0
    local mode_val = nil
    for val, count in pairs(counts) do
        if count > max_count then
            max_count = count
            mode_val = val
        end
    end
    
    return mode_val
end

return mathutils

Testing Your Library

-- test_mathutils.lua
local mathutils = require("mathutils")

print("Testing mathutils library v" .. mathutils._VERSION)
print()

-- Test basic functions
print("Factorial Tests:")
print("5! =", mathutils.factorial(5))
print("0! =", mathutils.factorial(0))

print("\nGCD/LCM Tests:")
print("gcd(48, 18) =", mathutils.gcd(48, 18))
print("lcm(12, 15) =", mathutils.lcm(12, 15))

print("\nPrime Tests:")
print("is_prime(17) =", mathutils.is_prime(17))
print("is_prime(15) =", mathutils.is_prime(15))
print("prime_factors(60) =", table.concat(mathutils.prime_factors(60), " × "))

-- Test statistics
local data = {1, 2, 2, 3, 4, 4, 4, 5}
print("\nStatistics for {1, 2, 2, 3, 4, 4, 4, 5}:")
print("Mean:", mathutils.stats.mean(data))
print("Median:", mathutils.stats.median(data))
print("Mode:", mathutils.stats.mode(data))

-- Error handling test
print("\nError Handling:")
local success, result = pcall(mathutils.factorial, -1)
if not success then
    print("Caught error:", result)
end

Expected Output:

Testing mathutils library v1.0.0

Factorial Tests:
5! = 120
0! = 1

GCD/LCM Tests:
gcd(48, 18) = 6
lcm(12, 15) = 60

Prime Tests:
is_prime(17) = true
is_prime(15) = false
prime_factors(60) = 2 × 2 × 3 × 5

Statistics for {1, 2, 2, 3, 4, 4, 4, 5}:
Mean: 3.125
Median: 3.5
Mode: 4

Error Handling:
Caught error: factorial requires non-negative integer

Publishing to LuaRocks

Creating a Rockspec File

-- mathutils-1.0.0-1.rockspec
package = "mathutils"
version = "1.0.0-1"
source = {
   url = "git://github.com/yourusername/mathutils",
   tag = "v1.0.0"
}
description = {
   summary = "Mathematical utilities for Lua",
   detailed = [[
      A collection of mathematical functions including
      factorial, GCD/LCM, prime testing, and basic statistics.
   ]],
   homepage = "https://github.com/yourusername/mathutils",
   license = "MIT"
}
dependencies = {
   "lua >= 5.1"
}
build = {
   type = "builtin",
   modules = {
      mathutils = "mathutils.lua"
   }
}

Local Testing and Installation

# Test rockspec locally
luarocks make mathutils-1.0.0-1.rockspec

# Install from local rockspec
luarocks install mathutils-1.0.0-1.rockspec

# Upload to LuaRocks (requires account)
luarocks upload mathutils-1.0.0-1.rockspec

Popular Lua Libraries Overview

Domain Library Description
JSON lua-cjson, dkjson Fast JSON encoding/decoding
HTTP http, lua-resty-http HTTP client libraries
Web Frameworks Lapis, OpenResty Web application frameworks
Database luasql, lua-resty-mysql Database connectivity
File System luafilesystem, luapath File and directory operations
Utilities penlight, lua-std General utility functions
Testing busted, luaunit Unit testing frameworks
Crypto luacrypto, lua-resty-openssl Cryptographic functions
XML/HTML lua-expat, htmlparser XML/HTML parsing
Logging lualogging, lua-resty-logger Logging frameworks

Dependency Management Best Practices

Version Constraints

-- In rockspec dependencies
dependencies = {
   "lua >= 5.1, < 5.5",     -- Version range
   "penlight >= 1.13.0",    -- Minimum version
   "lua-cjson ~> 2.1"       -- Compatible version
}

Managing Multiple Environments

# Create project-specific package environment
mkdir myproject && cd myproject
luarocks init

# Install dependencies locally
./luarocks install penlight
./luarocks install lua-cjson

# Run with local packages
./lua myscript.lua

Creating Dependency Lists

# Generate current package list
luarocks list --porcelain > packages.txt

# Install from package list
cat packages.txt | while read pkg; do
    luarocks install $pkg
done

Library Development Tips

Best Practices

  • Module Structure: Return a table with functions and metadata
  • Error Handling: Use error() for programming errors, return nil/error for runtime issues
  • Documentation: Include usage examples and API documentation
  • Testing: Write comprehensive tests before publishing
  • Versioning: Follow semantic versioning (major.minor.patch)

Common Pitfalls

Global Pollution: Always return a table, don't create globals

Missing Dependencies: Specify all dependencies in rockspec

Platform Issues: Test on multiple platforms before publishing

Version Conflicts: Be careful with dependency version constraints

Checks for Understanding

  1. What is LuaRocks and how do you install packages with it?
  2. How should you structure a Lua library for distribution?
  3. What information is required in a rockspec file?
  4. How do you manage project-specific dependencies?
Show Answers
  1. LuaRocks is Lua's package manager. Install packages with luarocks install packagename.
  2. Return a table with functions and metadata, include version info, handle errors properly, provide clear API.
  3. Package name, version, source location, description, dependencies, and build instructions.
  4. Use luarocks init to create project-local package environment, or maintain dependency lists.

Exercises

  1. Create a utility library for string manipulation functions and publish it locally.
  2. Install and use three different web-related libraries to build a simple HTTP client.
  3. Write a rockspec for an existing Lua script and test the installation process.