-
Notifications
You must be signed in to change notification settings - Fork 0
3. Plugin Development Guide
This guide provides an in-depth walkthrough for creating EasyREST plugins. It covers:
- 🔗 Handshake & go‑plugin Basics
- 🔍 Plugin Interfaces:
DBPlugin
vsCachePlugin
- 📦 Project Structure & Binary Naming
- ⚙️ InitConnection: URI parsing & connection setup
- 🔧 BuildPluginContext: HTTP →
ctx
map breakdown - 🔄 Transactions &
Prefer
header - 🕰️ Timezone propagation
- 📊 CRUD Methods: arguments & behaviors
- 🚀 RPC (
CallFunction
) - 📑 Schema Introspection (
GetSchema
) - 🗃️ CachePlugin details
- 🔔 Putting It All Together
EasyREST uses HashiCorp’s go-plugin to load external binaries:
-
Handshake: Ensures client/server compatibility via a shared
HandshakeConfig
ineasyrest.Handshake
. -
Serve: In your
main()
, register plugin roles:hplugin.Serve(&hplugin.ServeConfig{ HandshakeConfig: easyrest.Handshake, Plugins: map[string]hplugin.Plugin{ "db": &easyrest.DBPluginPlugin{Impl: myDB}, "cache": &easyrest.CachePluginPlugin{Impl: myCache}, // optional }, })
-
Binary Naming Convention:
Place your compiled binary inPATH
aseasyrest-plugin-<scheme>-<os>-<arch>
.
E.g.easyrest-plugin-mysql-linux-amd64
for MySQL on Linux AMD64.
EasyREST defines two interfaces in github.com/onegreyonewhite/easyrest/plugin
:
type DBPlugin interface {
InitConnection(uri string) error
TableGet(userID, table string, selectFields []string,
where map[string]any, ordering, groupBy []string,
limit, offset int, ctx map[string]any) ([]map[string]any, error)
TableCreate(userID, table string, data []map[string]any, ctx map[string]any) ([]map[string]any, error)
TableUpdate(userID, table string, data map[string]any, where map[string]any, ctx map[string]any) (int, error)
TableDelete(userID, table string, where map[string]any, ctx map[string]any) (int, error)
CallFunction(userID, funcName string, data map[string]any, ctx map[string]any) (any, error)
GetSchema(ctx map[string]any) (any, error)
}
type CachePlugin interface {
InitConnection(uri string) error
Set(key, value string, ttl time.Duration) error
Get(key string) (string, error)
}
-
Directory layout:
my-plugin/ ├── go.mod ├── main.go └── plugin.go
-
Compile:
GOOS=linux GOARCH=amd64 go build -o easyrest-plugin-myscheme-linux-amd64 main.go
Responsibilities:
-
Validate URI prefix (
myscheme://
,redis://
, etc.). - Parse connection details (path, user, password, host, port, query params).
- Open connection (SQL driver, Redis client).
- Configure connection pool / client settings (timeouts, pool sizes).
- Ping or test connection.
Example: MySQL
func (m *mysqlPlugin) InitConnection(uri string) error {
if !strings.HasPrefix(uri, "mysql://") {
return errors.New("invalid MySQL URI")
}
// Parse URL
parsed, err := url.Parse(uri)
// Extract query params: maxOpenConns, timeout, ...
// Build DSN: "user:pass@tcp(host:port)/dbname?..."
m.db, err = sql.Open("mysql", dsn)
m.db.SetMaxOpenConns(...)
ctx, cancel := context.WithTimeout(...); defer cancel()
return m.db.PingContext(ctx)
}
Example: Redis
func (r *redisCache) InitConnection(uri string) error {
opts, err := redis.ParseURL(uri)
r.client = redis.NewClient(opts)
ctx, cancel := context.WithTimeout(...); defer cancel()
return r.client.Ping(ctx).Err()
}
EasyREST constructs a ctx map[string]any
by:
func BuildPluginContext(r *http.Request) map[string]any {
// 1. cfg := GetConfig()
// 2. dbKey := mux.Vars(r)["db"]
// 3. dbConfig := cfg.PluginMap[dbKey]
// 4. Collect headers: map[key] = joined values
// 5. Extract JWT claims → map[string]any with lowercase keys
// 6. Parse `Prefer` header (see below)
// 7. Default timezone if not in Prefer
return map[string]any{
"timezone": timezone, // string, e.g. "UTC" or "America/Los_Angeles"
"headers": headers, // map[string]any of HTTP headers
"claims": plainClaims, // JWT claims map
"jwt.claims": plainClaims, // alias for plugins expecting this key
"method": r.Method, // "GET", "POST", ...
"path": r.URL.Path, // request path
"query": r.URL.RawQuery, // raw query string
"prefer": prefer, // map[string]any of Prefer keys
}
}
- Header format:
Prefer: timezone=UTC tx=rollback custom=foo
- Keys lowercased.
-
timezone
: overridesctx["timezone"]
→ used for date/time contexts. -
tx
: if plugin’sDbTxEnd
allows override, set to"commit"
or"rollback"
. - All tokens stored in
ctx["prefer"]
map.
-
"commit"
: transaction is committed on success. -
"rollback"
: transaction is explicitly rolled back, even on success. - Validation: any other value yields an error before executing.
res, err := p.handleTransaction(ctx, func(tx *sql.Tx) (any,error) {
// exec queries inside tx
})
func (p *plug) handleTransaction(ctxMap, operation) (any,error) {
tx, _ := db.BeginTx(...)
pref, err := getTxPreference(ctxMap) // from ctx["prefer"]["tx"]
if err != nil { return nil, err }
injectContext(tx, ctxMap) // see next
result, err := operation(tx)
if err != nil { tx.Rollback(); return nil, err }
if pref=="rollback" { tx.Rollback() } else { tx.Commit() }
return result, nil
}
-
ctx["timezone"]
holds the request’s timezone. -
Plugins should pass this to the database session:
-
MySQL/SQLite:
SET time_zone = ?
-
PostgreSQL:
SELECT set_config('timezone', $1, true)
-
MySQL/SQLite:
-
Ensures date/time functions (e.g.
NOW()
) respect client timezone.
TableGet(
userID string, // from JWT claim e.g. sub
table string, // API path segment
selectFields []string, // e.g. ["id","name"]
where map[string]any, // filter conditions
ordering []string, // ORDER BY values
groupBy []string, // GROUP BY values
limit int, // max rows
offset int, // start position
ctx map[string]any, // plugin context
) ([]map[string]any, error)
-
where
keys: column names; values: operator maps ({"eq":42}
,{"like":"%foo%"}
). - Plugins use
easyrest.BuildWhereClause(where)
to renderWHERE
SQL + args.
TableCreate(
userID string,
table string,
data []map[string]any, // each row as map[col]→value
ctx map[string]any,
) ([]map[string]any, error)
- Return: slice of inserted rows (input echoed).
- If rolled back, still return input data for dry‑run scenarios.
TableUpdate(
userID string,
table string,
data map[string]any, // columns→new values
where map[string]any, // filter
ctx map[string]any,
) (int, error) // number of rows affected
- Return zero if rollback preference or no rows.
TableDelete(
userID string,
table string,
where map[string]any,
ctx map[string]any,
) (int, error)
- Similar to update; returns affected count.
CallFunction(
userID string,
funcName string,
data map[string]any, // RPC input args
ctx map[string]any,
) (any, error)
-
MySQL: stored procedures/functions discovered in
InitConnection
. -
PostgreSQL: functions from
information_schema.routines
. - Build call:
- MySQL:
CALL name(?,…)
orSELECT name(?,…)
. - Postgres:
SELECT * FROM name($1,…)
.
- MySQL:
- Scan results via
scanRows
/processRows
. - Handle:
- Scalar: single row/single column → return scalar.
- Composite: map of columns.
- SETOF: slice of maps.
GetSchema(ctx map[string]any) (any, error)
Return value must be a Swagger 2.0‑compatible JSON schema object:
{
"tables": {<tableName>: <JSON Schema>, …},
"views": {<viewName>: <JSON Schema>, …}, // SQLite/Postgres
"rpc": {<funcName>: [<inSchema>,<outSchema>], …}
}
-
Object type with:
-
"properties"
: map of field → schema{ type: "...", ... }
-
"required"
: array of required property names -
Field schema:
-
type
:"integer"
,"number"
,"string"
, or"boolean"
-
x-nullable
:true
if column allows null -
readOnly
:true
for primary keys / auto‑columns -
format
: e.g."byte"
for BLOB/base64 in SQLite
-
-
Implement caching either in SQL or Redis:
-
Table
easyrest_cache(key TEXT PRIMARY KEY, value TEXT, expires_at DATETIME)
-
Set:
INSERT … ON CONFLICT DO UPDATE …
-
Get:
SELECT value … WHERE expires_at > CURRENT_TIMESTAMP
- Cleanup: goroutine deletes expired entries every minute.
- Use
redis.Client.Set(ctx,key,value,ttl)
&Get(ctx,key)
. - On
Get
returnredis.Nil
if not found → core handles cache miss.
- Implement both plugin interfaces as needed.
-
Compile with correct naming in
$PATH
. -
Configure in EasyREST’s
config.yaml
:plugins: mydb: uri: "myscheme://conn-details" enable_cache: true cache_name: mydb_cache mydb_cache: uri: "myscheme://conn-details?autoCleanup=true"
-
Run
easyrest-server --config config.yaml
-
Hot‑reload with
kill -SIGHUP $(pidof easyrest-server)
🚀 You’re now ready to build and integrate powerful EasyREST plugins with full context-awareness, transaction control, caching, and schema introspection!