feat(jsonrpc): NDJSON rpc implementation

This commit is contained in:
Kristian Borgwarth 2026-03-22 21:46:41 +01:00
parent c160bff2f8
commit 12d0e44efd
7 changed files with 116 additions and 89 deletions

View file

@ -1,13 +1,20 @@
package main package main
import ( import (
"github.com/KristianJBorgwarth/dendrite.daemon/core/http" "log/slog"
"github.com/KristianJBorgwarth/dendrite.daemon/core/logging" "os"
"github.com/KristianJBorgwarth/dendrite.daemon/core/rpc"
"github.com/KristianJBorgwarth/dendrite.daemon/core/handlers"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
func main() { func main() {
logging.Init() server := rpc.NewServer()
srv := server.New(":6969")
srv.Start() server.Register("initialize", handlers.InitializeHandler{})
if err := server.Serve(os.Stdin, os.Stdout); err != nil {
slog.Error("server error", "error", err)
}
} }

View file

@ -5,6 +5,7 @@ import (
"log/slog" "log/slog"
"github.com/KristianJBorgwarth/dendrite.daemon/config" "github.com/KristianJBorgwarth/dendrite.daemon/config"
"github.com/KristianJBorgwarth/dendrite.daemon/core/rpc"
"github.com/KristianJBorgwarth/dendrite.daemon/persistence" "github.com/KristianJBorgwarth/dendrite.daemon/persistence"
) )
@ -24,12 +25,17 @@ type initializeCommand struct {
} `json:"dailyNote"` } `json:"dailyNote"`
} }
func Initialize(raw json.RawMessage) (*config.Config, error) { type InitializeHandler struct{}
func (h InitializeHandler) Handle(raw json.RawMessage) (any, *rpc.Error) {
var params initializeCommand var params initializeCommand
slog.Info("initializing with params", "params", string(raw)) slog.Info("initializing with params", "params", string(raw))
if err := json.Unmarshal(raw, &params); err != nil { if err := json.Unmarshal(raw, &params); err != nil {
return nil, err return nil, &rpc.Error{
Code: -32602,
Message: "invalid params: " + err.Error(),
}
} }
cfg := &config.Config{ cfg := &config.Config{
@ -50,7 +56,10 @@ func Initialize(raw json.RawMessage) (*config.Config, error) {
err := persistence.InitializeIndex(cfg.VaultPath) err := persistence.InitializeIndex(cfg.VaultPath)
if err != nil { if err != nil {
return nil, err return nil, &rpc.Error{
Code: -1,
Message: "failed to initialize index: " + err.Error(),
}
} }
return cfg, nil return cfg, nil

View file

@ -1,76 +0,0 @@
// Package server provides the implementation of the server component of the application.
package server
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/KristianJBorgwarth/dendrite.daemon/config"
"github.com/KristianJBorgwarth/dendrite.daemon/core/handlers"
"github.com/KristianJBorgwarth/dendrite.daemon/core/rpc"
)
type Server struct {
addr string
config *config.Config
}
func New(addr string) *Server {
return &Server{
addr: addr,
}
}
func (s *Server) Start() error {
slog.Info("server started")
mux := http.NewServeMux()
mux.HandleFunc("/rpc", s.RPCHandler)
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
return http.ListenAndServe(s.addr, mux)
}
func (s *Server) RPCHandler(w http.ResponseWriter, r *http.Request) {
var req rpc.Request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
resp := rpc.Response{
Jsonrpc: "2.0",
ID: req.ID,
}
switch req.Method {
case "initialize":
cfg, err := handlers.Initialize(req.Params)
if err != nil {
resp.Error = map[string]any{
"code": -1,
"message": err.Error(),
}
} else {
s.config = cfg
resp.Result = map[string]any{"status": "ok"}
}
default:
slog.Warn("Unknown method", "method", req.Method)
resp.Error = map[string]any{
"code": -32601,
"message": "method not found",
}
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
slog.Error("failed to write response", "error", err)
}
}

View file

@ -7,16 +7,16 @@ import (
type Request struct { type Request struct {
Jsonrpc string `json:"jsonrpc"` Jsonrpc string `json:"jsonrpc"`
ID int `json:"id"` ID *int `json:"id"`
Method string `json:"method"` Method string `json:"method"`
Params json.RawMessage `json:"params"` Params json.RawMessage `json:"params"`
} }
type Response struct { type Response struct {
Jsonrpc string `json:"jsonrpc"` Jsonrpc string `json:"jsonrpc"`
ID int `json:"id"` ID *int `json:"id"`
Result any `json:"result,omitempty"` Result any `json:"result,omitempty"`
Error any `json:"error,omitempty"` Error *Error `json:"error,omitempty"`
} }
type Notification struct { type Notification struct {
@ -25,3 +25,7 @@ type Notification struct {
Params json.RawMessage `json:"params,omitempty"` Params json.RawMessage `json:"params,omitempty"`
} }
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
}

83
core/rpc/server.go Normal file
View file

@ -0,0 +1,83 @@
package rpc
import (
"bufio"
"encoding/json"
"fmt"
"io"
)
type Handler interface {
Handle(json.RawMessage) (any, *Error)
}
type Server struct {
handlers map[string]Handler
}
func NewServer() *Server {
return &Server{
handlers: make(map[string]Handler),
}
}
func (s *Server) Register(method string, handler Handler) {
s.handlers[method] = handler
}
func (s *Server) Serve(r io.Reader, w io.Writer) error {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Bytes()
var req Request
if err := json.Unmarshal(line, &req); err != nil {
s.write(w, Response{
Jsonrpc: "2.0",
Error: &Error{Code: -32700, Message: "parse error"},
})
continue
}
s.handle(w, req)
}
return scanner.Err()
}
func (s *Server) handle(w io.Writer, req Request) {
handler, ok := s.handlers[req.Method]
if !ok {
s.respond(w, req.ID, nil, &Error{
Code: -32601,
Message: "method not found",
})
return
}
result, err := handler.Handle(req.Params)
if req.ID == nil {
return
}
s.respond(w, req.ID, result, err)
}
func (s *Server) respond(w io.Writer, id *int, result any, err *Error) {
resp := Response{
Jsonrpc: "2.0",
ID: id,
Result: result,
Error: err,
}
s.write(w, resp)
}
func (s *Server) write(w io.Writer, resp Response) {
data, _ := json.Marshal(resp)
fmt.Fprintln(w, string(data))
}

BIN
dendrite

Binary file not shown.

BIN
index.db Normal file

Binary file not shown.