name: rugo-native-module-writer description: Expert in writing native Rugo modules (.rugo libraries). Load when creating multi-file Rugo libraries, designing module APIs, or structuring require/with patterns.
Rugo Native Module Writer
Write multi-file .rugo libraries that consumers load via require and with.
Directory Layouts
Flat (simple libraries)
rugh/
client.rugo # → client namespace
issue.rugo # → issue namespace
repo.rugo # → repo namespace
main.rugo # (optional) entry point for bare require
Consumer: require "rugh" with client, issue
Nested with lib/ (larger libraries)
gummy/
gummy.rugo # barrel file: require "lib/db" etc.
lib/
db.rugo # core module
sql.rugo # helper module
fts.rugo # extension module
Consumer: require "gummy" with db — the with clause checks the root first, then lib/ as fallback.
Which to choose
- Flat: when modules are independent or loosely coupled (rugh: client, issue, repo)
- lib/: when there's a clear public API module backed by internal helpers (gummy: db is public, sql/fts are internal)
Module Resolution Rules
require "dir" (bare, no with)
Resolves entry point: <dirname>.rugo → main.rugo → sole .rugo file. A file always takes precedence over a directory of the same name.
require "dir" with mod1, mod2
Each name loads <name>.rugo from the directory root. If not found, checks lib/<name>.rugo. Root takes precedence over lib/.
Nested requires pass through
When commands.rugo requires mods/alpha and mods/beta, those namespaces (alpha, beta) are visible to the file that requires commands. They keep their own namespaces — they do NOT get re-namespaced under commands.
Barrel files
A barrel file (e.g., gummy.rugo) that only contains require statements exposes the inner namespaces to the consumer. The barrel file itself has no namespace unless it defines its own functions.
API Patterns
Structs with methods (idiomatic)
Structs are the preferred way to build module APIs:
# mydb.rugo
use "sqlite"
struct DB
conn
end
def DB.exec(sql)
return sqlite.exec(self.conn, sql)
end
def DB.close()
sqlite.close(self.conn)
end
def open(path)
return DB(sqlite.open(path))
end
Consumer calls through the namespace: mydb.open(path), mydb.exec(db, sql), mydb.close(db).
Hash-object pattern (closures)
An alternative when you want method calls directly on the object (without the namespace prefix):
def open(path)
conn = sqlite.open(path)
handle = {}
handle["query"] = fn(sql)
return sqlite.query(conn, sql)
end
handle["close"] = fn()
sqlite.close(conn)
end
return handle
end
The consumer calls db.open(path) to get a handle, then handle.query(sql), handle.close().
Record pattern — augment data with methods
def make_record(conn, table, row)
row["save"] = fn()
# update using captured conn, table, row["id"]
end
row["delete"] = fn()
sqlite.exec(conn, "DELETE FROM " + table + " WHERE id = ?", row["id"])
end
return row
end
Factory chain
Each layer returns closures: open() → handle with model() → model with insert() → record with save()/delete().
Cross-Module Dependencies
Modules can call functions from sibling modules they require:
# lib/db.rugo
require "sql"
require "fts"
def _where(conn, table, conditions)
clause = sql.build_where(conditions) # call into sql namespace
# ...
end
Dependencies are resolved at call time, not load time. This means a module must require its own dependencies — don't rely on the barrel file having loaded them.
Namespace Gotchas
Variable shadows namespace
After assignment, a variable with the same name as a namespace takes over for dot access:
require "gummy" with db
# Before assignment: db.open() calls the db namespace function
conn = db.open(":memory:")
# After assignment: db.model() is a dot-call on the conn hash
Users = conn.model("users", {name: "text"})
This is correct and intentional — local variables shadow namespaces. Use a different variable name if you need continued access to the namespace.
Private functions
Prefix with _ by convention. They compile to Go functions and ARE accessible, but signal "don't use this":
def _make_model(conn, name, columns)
# internal
end
def open(path)
# public API
end
Namespace conflicts
- A
requirenamespace cannot conflict with ausemodule (e.g., can't have bothuse "os"and a require namespaceos) - A
requirenamespace cannot conflict with animportGo bridge alias -
withandasare mutually exclusive on the samerequire
Conventions
- One public entry point function per module (e.g.,
db.open(),client.make()) - Return hashes with closure "methods" for stateful APIs
- Put
useandrequireat the top of each file — they must be top-level - Document usage in a comment at the top of each module file
- Each module file should declare its own dependencies (don't rely on load order)
- Keep barrel files thin — just
requirestatements, no logic
Testing
Write rats tests that exercise the module through the public API:
use "test"
rats "module works end to end"
tmpdir = test.tmpdir()
script = <<~SCRIPT
require "mylib" with core
result = core.do_thing("input")
puts(result)
SCRIPT
test.write_file(tmpdir + "/test.rugo", script)
result = test.run("rugo run " + tmpdir + "/test.rugo")
test.assert_eq(result["status"], 0)
test.assert_eq(result["output"], "expected")
end
For modules using absolute paths, use import "path/filepath" and filepath.abs() to construct paths.
chat Comments (0)
Sign in to join the discussion and leave a comment.
Skill Details
Related Skills
Build your own?
Join 12,000+ developers contributing to the Claude ecosystem.
No comments yet. Be the first to share your thoughts!