package policy import ( "context" "database/sql" "errors" "fmt" "postern/internal/db" "go.starlark.net/starlark" ) type KVModule struct { db *db.DB } func NewKVModule(db *db.DB) *KVModule { return &KVModule{ db: db, } } func (k *KVModule) set(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { ctx, ok := thread.Local("context").(context.Context) if !ok || ctx == nil { ctx = context.Background() } if err := ctx.Err(); err != nil { return nil, err } var keyStr string var val starlark.Value if err := starlark.UnpackArgs("kv.set", args, kwargs, "key", &keyStr, "value", &val); err != nil { return nil, err } // Convert Starlark value to Go/SQLite compatible interface var goVal any switch v := val.(type) { case starlark.String: goVal = string(v) case starlark.Int: i, _ := v.Int64() goVal = i case starlark.Bool: goVal = bool(v) case starlark.Bytes: goVal = []byte(v) default: return nil, fmt.Errorf("kv.set: unsupported value type %s", val.Type()) } err := k.db.KVSet(ctx, keyStr, goVal) if err != nil { return nil, fmt.Errorf("kv.set: %w", err) } return starlark.None, nil } func (k *KVModule) get(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { ctx, ok := thread.Local("context").(context.Context) if !ok || ctx == nil { ctx = context.Background() } if err := ctx.Err(); err != nil { return nil, err } var keyStr string var defaultVal starlark.Value = starlark.None if err := starlark.UnpackArgs("kv.get", args, kwargs, "key", &keyStr, "default?", &defaultVal); err != nil { return nil, err } rawVal, err := k.db.KVGet(ctx, keyStr) if errors.Is(err, sql.ErrNoRows) { return defaultVal, nil } else if err != nil { return nil, err } return rawToStarlark(rawVal) } func (k *KVModule) inc(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { ctx, ok := thread.Local("context").(context.Context) if !ok || ctx == nil { ctx = context.Background() } if err := ctx.Err(); err != nil { return nil, err } var keyStr string var delta int64 = 1 if err := starlark.UnpackArgs("kv.inc", args, kwargs, "key", &keyStr, "delta?", &delta); err != nil { return nil, err } updatedVal, err := k.db.KVInc(ctx, keyStr, delta) if err != nil { return nil, err } return starlark.MakeInt64(updatedVal), nil } func (k *KVModule) pop(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { ctx, ok := thread.Local("context").(context.Context) if !ok || ctx == nil { ctx = context.Background() } if err := ctx.Err(); err != nil { return nil, err } var keyStr string if err := starlark.UnpackArgs("kv.pop", args, kwargs, "key", &keyStr); err != nil { return nil, err } rawVal, err := k.db.KVPop(ctx, keyStr) if errors.Is(err, sql.ErrNoRows) { return starlark.None, nil } else if err != nil { return nil, err } return rawToStarlark(rawVal) } func (k *KVModule) String() string { return "" } func (k *KVModule) Type() string { return "module" } func (k *KVModule) Freeze() {} func (k *KVModule) Truth() starlark.Bool { return true } func (k *KVModule) Hash() (uint32, error) { return 0, errors.New("unhashable") } func (k *KVModule) Attr(name string) (starlark.Value, error) { switch name { case "set": return starlark.NewBuiltin("set", k.set), nil case "get": return starlark.NewBuiltin("get", k.get), nil case "inc": return starlark.NewBuiltin("inc", k.inc), nil case "pop": return starlark.NewBuiltin("pop", k.pop), nil } return nil, nil } func (k *KVModule) AttrNames() []string { return []string{"get", "set", "inc", "pop"} } func rawToStarlark(rawVal any) (starlark.Value, error) { switch v := rawVal.(type) { case int64: return starlark.MakeInt64(v), nil case string: return starlark.String(v), nil case []byte: return starlark.Bytes(v), nil case bool: return starlark.Bool(v), nil default: return starlark.None, errors.New("kv.get: unsupported value from database") } }