feat(jsonrpc): NDJSON rpc implementation
This commit is contained in:
parent
c160bff2f8
commit
12d0e44efd
7 changed files with 116 additions and 89 deletions
17
cmd/main.go
17
cmd/main.go
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, ¶ms); err != nil {
|
if err := json.Unmarshal(raw, ¶ms); 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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
83
core/rpc/server.go
Normal 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
BIN
dendrite
Binary file not shown.
BIN
index.db
Normal file
BIN
index.db
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue