feat(gl): scaffold general ledger grpc service

This commit is contained in:
2026-08-14 18:52:30 +03:30
parent 1863de1f3b
commit b1e0b01598
19 changed files with 3065 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
.PHONY: build generate test
generate:
buf generate ../proto --template ./buf.gen.yaml --path ../proto/base/v1 --path ../proto/ledger/v1
build: generate
go build ./...
test:
go test ./...
+10
View File
@@ -5,3 +5,13 @@ committed wallet value movement so financial state can be reconstructed when a
blockchain or provider is unavailable. blockchain or provider is unavailable.
The implementation contract and invariants are defined in [DESIGN.md](DESIGN.md). The implementation contract and invariants are defined in [DESIGN.md](DESIGN.md).
Generate protobufs, test, and build with:
```bash
make generate
make test
make build
```
Run locally with `go run ./cmd/gl -conf ./gl.cfg.toml`.
+29
View File
@@ -0,0 +1,29 @@
// Package health provides the service readiness use case.
package health
import "context"
type Database interface {
Ping(context.Context) error
}
type Status struct {
Serving bool
DatabaseReady bool
}
type Service struct {
database Database
}
func NewService(database Database) *Service {
return &Service{database: database}
}
func (s *Service) Check(ctx context.Context) Status {
status := Status{Serving: true}
if s.database != nil {
status.DatabaseReady = s.database.Ping(ctx) == nil
}
return status
}
+30
View File
@@ -0,0 +1,30 @@
package health
import (
"context"
"errors"
"testing"
)
type databaseStub struct{ err error }
func (s databaseStub) Ping(context.Context) error { return s.err }
func TestCheckReportsDatabaseReadiness(t *testing.T) {
for _, tc := range []struct {
name string
db Database
ready bool
}{
{name: "not connected", db: nil, ready: false},
{name: "ready", db: databaseStub{}, ready: true},
{name: "unavailable", db: databaseStub{err: errors.New("down")}, ready: false},
} {
t.Run(tc.name, func(t *testing.T) {
got := NewService(tc.db).Check(context.Background())
if !got.Serving || got.DatabaseReady != tc.ready {
t.Fatalf("unexpected status: %+v", got)
}
})
}
}
+16
View File
@@ -0,0 +1,16 @@
version: v2
clean: true
managed:
enabled: true
override:
- file_option: go_package_prefix
value: gl/gen
plugins:
- local: protoc-gen-go
out: gen
opt: paths=source_relative
- local: protoc-gen-go-grpc
out: gen
opt:
- paths=source_relative
- require_unimplemented_servers=false
+40
View File
@@ -0,0 +1,40 @@
package main
import (
"context"
"flag"
"log/slog"
"os"
"os/signal"
"syscall"
"gl/application/health"
"gl/infrastructure/config"
grpcadapter "gl/interface/grpc"
)
func main() {
configPath := flag.String("conf", "./gl.cfg.toml", "path to the TOML configuration file")
flag.Parse()
cfg, err := config.Load(*configPath)
if err != nil {
slog.Error("load configuration", "error", err)
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
handler := grpcadapter.NewHealthHandler(health.NewService(nil))
slog.Info("starting GL gRPC service", "host", cfg.GRPC.Host, "port", cfg.GRPC.Port)
serverConfig := grpcadapter.ServerConfig{
Host: cfg.GRPC.Host,
Port: cfg.GRPC.Port,
ShutdownTimeout: cfg.GRPC.ShutdownTimeout,
}
if err := grpcadapter.Run(ctx, serverConfig, handler); err != nil {
slog.Error("GL service stopped", "error", err)
os.Exit(1)
}
}
+368
View File
@@ -0,0 +1,368 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: base/v1/msg.proto
package basev1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Empty struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Empty) Reset() {
*x = Empty{}
mi := &file_base_v1_msg_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Empty) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Empty) ProtoMessage() {}
func (x *Empty) ProtoReflect() protoreflect.Message {
mi := &file_base_v1_msg_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Empty.ProtoReflect.Descriptor instead.
func (*Empty) Descriptor() ([]byte, []int) {
return file_base_v1_msg_proto_rawDescGZIP(), []int{0}
}
type StatusRes struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StatusRes) Reset() {
*x = StatusRes{}
mi := &file_base_v1_msg_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StatusRes) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StatusRes) ProtoMessage() {}
func (x *StatusRes) ProtoReflect() protoreflect.Message {
mi := &file_base_v1_msg_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StatusRes.ProtoReflect.Descriptor instead.
func (*StatusRes) Descriptor() ([]byte, []int) {
return file_base_v1_msg_proto_rawDescGZIP(), []int{1}
}
func (x *StatusRes) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
type IdRes struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *IdRes) Reset() {
*x = IdRes{}
mi := &file_base_v1_msg_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IdRes) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IdRes) ProtoMessage() {}
func (x *IdRes) ProtoReflect() protoreflect.Message {
mi := &file_base_v1_msg_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IdRes.ProtoReflect.Descriptor instead.
func (*IdRes) Descriptor() ([]byte, []int) {
return file_base_v1_msg_proto_rawDescGZIP(), []int{2}
}
func (x *IdRes) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
type IdReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *IdReq) Reset() {
*x = IdReq{}
mi := &file_base_v1_msg_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IdReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IdReq) ProtoMessage() {}
func (x *IdReq) ProtoReflect() protoreflect.Message {
mi := &file_base_v1_msg_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IdReq.ProtoReflect.Descriptor instead.
func (*IdReq) Descriptor() ([]byte, []int) {
return file_base_v1_msg_proto_rawDescGZIP(), []int{3}
}
func (x *IdReq) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
type YesNoRes struct {
state protoimpl.MessageState `protogen:"open.v1"`
Yes bool `protobuf:"varint,1,opt,name=yes,proto3" json:"yes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *YesNoRes) Reset() {
*x = YesNoRes{}
mi := &file_base_v1_msg_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *YesNoRes) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*YesNoRes) ProtoMessage() {}
func (x *YesNoRes) ProtoReflect() protoreflect.Message {
mi := &file_base_v1_msg_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use YesNoRes.ProtoReflect.Descriptor instead.
func (*YesNoRes) Descriptor() ([]byte, []int) {
return file_base_v1_msg_proto_rawDescGZIP(), []int{4}
}
func (x *YesNoRes) GetYes() bool {
if x != nil {
return x.Yes
}
return false
}
type PaginationRespSample struct {
state protoimpl.MessageState `protogen:"open.v1"`
PageNo uint32 `protobuf:"varint,2,opt,name=page_no,json=pageNo,proto3" json:"page_no,omitempty"`
PageSize uint32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
TotalCount uint32 `protobuf:"varint,4,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PaginationRespSample) Reset() {
*x = PaginationRespSample{}
mi := &file_base_v1_msg_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PaginationRespSample) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PaginationRespSample) ProtoMessage() {}
func (x *PaginationRespSample) ProtoReflect() protoreflect.Message {
mi := &file_base_v1_msg_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PaginationRespSample.ProtoReflect.Descriptor instead.
func (*PaginationRespSample) Descriptor() ([]byte, []int) {
return file_base_v1_msg_proto_rawDescGZIP(), []int{5}
}
func (x *PaginationRespSample) GetPageNo() uint32 {
if x != nil {
return x.PageNo
}
return 0
}
func (x *PaginationRespSample) GetPageSize() uint32 {
if x != nil {
return x.PageSize
}
return 0
}
func (x *PaginationRespSample) GetTotalCount() uint32 {
if x != nil {
return x.TotalCount
}
return 0
}
var File_base_v1_msg_proto protoreflect.FileDescriptor
const file_base_v1_msg_proto_rawDesc = "" +
"\n" +
"\x11base/v1/msg.proto\x12\abase.v1\"\a\n" +
"\x05Empty\"%\n" +
"\tStatusRes\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\"\x17\n" +
"\x05IdRes\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\"\x17\n" +
"\x05IdReq\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\"\x1c\n" +
"\bYesNoRes\x12\x10\n" +
"\x03yes\x18\x01 \x01(\bR\x03yes\"m\n" +
"\x14PaginationRespSample\x12\x17\n" +
"\apage_no\x18\x02 \x01(\rR\x06pageNo\x12\x1b\n" +
"\tpage_size\x18\x03 \x01(\rR\bpageSize\x12\x1f\n" +
"\vtotal_count\x18\x04 \x01(\rR\n" +
"totalCountBk\n" +
"\vcom.base.v1B\bMsgProtoP\x01Z\x15gl/gen/base/v1;basev1\xa2\x02\x03BXX\xaa\x02\aBase.V1\xca\x02\aBase\\V1\xe2\x02\x13Base\\V1\\GPBMetadata\xea\x02\bBase::V1b\x06proto3"
var (
file_base_v1_msg_proto_rawDescOnce sync.Once
file_base_v1_msg_proto_rawDescData []byte
)
func file_base_v1_msg_proto_rawDescGZIP() []byte {
file_base_v1_msg_proto_rawDescOnce.Do(func() {
file_base_v1_msg_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_base_v1_msg_proto_rawDesc), len(file_base_v1_msg_proto_rawDesc)))
})
return file_base_v1_msg_proto_rawDescData
}
var file_base_v1_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_base_v1_msg_proto_goTypes = []any{
(*Empty)(nil), // 0: base.v1.Empty
(*StatusRes)(nil), // 1: base.v1.StatusRes
(*IdRes)(nil), // 2: base.v1.IdRes
(*IdReq)(nil), // 3: base.v1.IdReq
(*YesNoRes)(nil), // 4: base.v1.YesNoRes
(*PaginationRespSample)(nil), // 5: base.v1.PaginationRespSample
}
var file_base_v1_msg_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_base_v1_msg_proto_init() }
func file_base_v1_msg_proto_init() {
if File_base_v1_msg_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_base_v1_msg_proto_rawDesc), len(file_base_v1_msg_proto_rawDesc)),
NumEnums: 0,
NumMessages: 6,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_base_v1_msg_proto_goTypes,
DependencyIndexes: file_base_v1_msg_proto_depIdxs,
MessageInfos: file_base_v1_msg_proto_msgTypes,
}.Build()
File_base_v1_msg_proto = out.File
file_base_v1_msg_proto_goTypes = nil
file_base_v1_msg_proto_depIdxs = nil
}
File diff suppressed because it is too large Load Diff
+102
View File
@@ -0,0 +1,102 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: ledger/v1/srv.proto
package ledgerv1
import (
v1 "gl/gen/base/v1"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
var File_ledger_v1_srv_proto protoreflect.FileDescriptor
const file_ledger_v1_srv_proto_rawDesc = "" +
"\n" +
"\x13ledger/v1/srv.proto\x12\tledger.v1\x1a\x11base/v1/msg.proto\x1a\x13ledger/v1/msg.proto2\xbe\x04\n" +
"\x14GeneralLedgerService\x123\n" +
"\x06Health\x12\x0e.base.v1.Empty\x1a\x19.ledger.v1.HealthResponse\x12R\n" +
"\rAppendJournal\x12\x1f.ledger.v1.AppendJournalRequest\x1a .ledger.v1.AppendJournalResponse\x12m\n" +
"\x16AppendTransactionEvent\x12(.ledger.v1.AppendTransactionEventRequest\x1a).ledger.v1.AppendTransactionEventResponse\x12>\n" +
"\n" +
"GetJournal\x12\x1c.ledger.v1.GetJournalRequest\x1a\x12.ledger.v1.Journal\x12L\n" +
"\vListEntries\x12\x1d.ledger.v1.ListEntriesRequest\x1a\x1e.ledger.v1.ListEntriesResponse\x12I\n" +
"\n" +
"GetBalance\x12\x1c.ledger.v1.GetBalanceRequest\x1a\x1d.ledger.v1.GetBalanceResponse\x12U\n" +
"\x0eReplayJournals\x12 .ledger.v1.ReplayJournalsRequest\x1a!.ledger.v1.ReplayJournalsResponseBy\n" +
"\rcom.ledger.v1B\bSrvProtoP\x01Z\x19gl/gen/ledger/v1;ledgerv1\xa2\x02\x03LXX\xaa\x02\tLedger.V1\xca\x02\tLedger\\V1\xe2\x02\x15Ledger\\V1\\GPBMetadata\xea\x02\n" +
"Ledger::V1b\x06proto3"
var file_ledger_v1_srv_proto_goTypes = []any{
(*v1.Empty)(nil), // 0: base.v1.Empty
(*AppendJournalRequest)(nil), // 1: ledger.v1.AppendJournalRequest
(*AppendTransactionEventRequest)(nil), // 2: ledger.v1.AppendTransactionEventRequest
(*GetJournalRequest)(nil), // 3: ledger.v1.GetJournalRequest
(*ListEntriesRequest)(nil), // 4: ledger.v1.ListEntriesRequest
(*GetBalanceRequest)(nil), // 5: ledger.v1.GetBalanceRequest
(*ReplayJournalsRequest)(nil), // 6: ledger.v1.ReplayJournalsRequest
(*HealthResponse)(nil), // 7: ledger.v1.HealthResponse
(*AppendJournalResponse)(nil), // 8: ledger.v1.AppendJournalResponse
(*AppendTransactionEventResponse)(nil), // 9: ledger.v1.AppendTransactionEventResponse
(*Journal)(nil), // 10: ledger.v1.Journal
(*ListEntriesResponse)(nil), // 11: ledger.v1.ListEntriesResponse
(*GetBalanceResponse)(nil), // 12: ledger.v1.GetBalanceResponse
(*ReplayJournalsResponse)(nil), // 13: ledger.v1.ReplayJournalsResponse
}
var file_ledger_v1_srv_proto_depIdxs = []int32{
0, // 0: ledger.v1.GeneralLedgerService.Health:input_type -> base.v1.Empty
1, // 1: ledger.v1.GeneralLedgerService.AppendJournal:input_type -> ledger.v1.AppendJournalRequest
2, // 2: ledger.v1.GeneralLedgerService.AppendTransactionEvent:input_type -> ledger.v1.AppendTransactionEventRequest
3, // 3: ledger.v1.GeneralLedgerService.GetJournal:input_type -> ledger.v1.GetJournalRequest
4, // 4: ledger.v1.GeneralLedgerService.ListEntries:input_type -> ledger.v1.ListEntriesRequest
5, // 5: ledger.v1.GeneralLedgerService.GetBalance:input_type -> ledger.v1.GetBalanceRequest
6, // 6: ledger.v1.GeneralLedgerService.ReplayJournals:input_type -> ledger.v1.ReplayJournalsRequest
7, // 7: ledger.v1.GeneralLedgerService.Health:output_type -> ledger.v1.HealthResponse
8, // 8: ledger.v1.GeneralLedgerService.AppendJournal:output_type -> ledger.v1.AppendJournalResponse
9, // 9: ledger.v1.GeneralLedgerService.AppendTransactionEvent:output_type -> ledger.v1.AppendTransactionEventResponse
10, // 10: ledger.v1.GeneralLedgerService.GetJournal:output_type -> ledger.v1.Journal
11, // 11: ledger.v1.GeneralLedgerService.ListEntries:output_type -> ledger.v1.ListEntriesResponse
12, // 12: ledger.v1.GeneralLedgerService.GetBalance:output_type -> ledger.v1.GetBalanceResponse
13, // 13: ledger.v1.GeneralLedgerService.ReplayJournals:output_type -> ledger.v1.ReplayJournalsResponse
7, // [7:14] is the sub-list for method output_type
0, // [0:7] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_ledger_v1_srv_proto_init() }
func file_ledger_v1_srv_proto_init() {
if File_ledger_v1_srv_proto != nil {
return
}
file_ledger_v1_msg_proto_init()
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_ledger_v1_srv_proto_rawDesc), len(file_ledger_v1_srv_proto_rawDesc)),
NumEnums: 0,
NumMessages: 0,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_ledger_v1_srv_proto_goTypes,
DependencyIndexes: file_ledger_v1_srv_proto_depIdxs,
}.Build()
File_ledger_v1_srv_proto = out.File
file_ledger_v1_srv_proto_goTypes = nil
file_ledger_v1_srv_proto_depIdxs = nil
}
+348
View File
@@ -0,0 +1,348 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// source: ledger/v1/srv.proto
package ledgerv1
import (
context "context"
v1 "gl/gen/base/v1"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
GeneralLedgerService_Health_FullMethodName = "/ledger.v1.GeneralLedgerService/Health"
GeneralLedgerService_AppendJournal_FullMethodName = "/ledger.v1.GeneralLedgerService/AppendJournal"
GeneralLedgerService_AppendTransactionEvent_FullMethodName = "/ledger.v1.GeneralLedgerService/AppendTransactionEvent"
GeneralLedgerService_GetJournal_FullMethodName = "/ledger.v1.GeneralLedgerService/GetJournal"
GeneralLedgerService_ListEntries_FullMethodName = "/ledger.v1.GeneralLedgerService/ListEntries"
GeneralLedgerService_GetBalance_FullMethodName = "/ledger.v1.GeneralLedgerService/GetBalance"
GeneralLedgerService_ReplayJournals_FullMethodName = "/ledger.v1.GeneralLedgerService/ReplayJournals"
)
// GeneralLedgerServiceClient is the client API for GeneralLedgerService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type GeneralLedgerServiceClient interface {
Health(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*HealthResponse, error)
AppendJournal(ctx context.Context, in *AppendJournalRequest, opts ...grpc.CallOption) (*AppendJournalResponse, error)
AppendTransactionEvent(ctx context.Context, in *AppendTransactionEventRequest, opts ...grpc.CallOption) (*AppendTransactionEventResponse, error)
GetJournal(ctx context.Context, in *GetJournalRequest, opts ...grpc.CallOption) (*Journal, error)
ListEntries(ctx context.Context, in *ListEntriesRequest, opts ...grpc.CallOption) (*ListEntriesResponse, error)
GetBalance(ctx context.Context, in *GetBalanceRequest, opts ...grpc.CallOption) (*GetBalanceResponse, error)
ReplayJournals(ctx context.Context, in *ReplayJournalsRequest, opts ...grpc.CallOption) (*ReplayJournalsResponse, error)
}
type generalLedgerServiceClient struct {
cc grpc.ClientConnInterface
}
func NewGeneralLedgerServiceClient(cc grpc.ClientConnInterface) GeneralLedgerServiceClient {
return &generalLedgerServiceClient{cc}
}
func (c *generalLedgerServiceClient) Health(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*HealthResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(HealthResponse)
err := c.cc.Invoke(ctx, GeneralLedgerService_Health_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *generalLedgerServiceClient) AppendJournal(ctx context.Context, in *AppendJournalRequest, opts ...grpc.CallOption) (*AppendJournalResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AppendJournalResponse)
err := c.cc.Invoke(ctx, GeneralLedgerService_AppendJournal_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *generalLedgerServiceClient) AppendTransactionEvent(ctx context.Context, in *AppendTransactionEventRequest, opts ...grpc.CallOption) (*AppendTransactionEventResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AppendTransactionEventResponse)
err := c.cc.Invoke(ctx, GeneralLedgerService_AppendTransactionEvent_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *generalLedgerServiceClient) GetJournal(ctx context.Context, in *GetJournalRequest, opts ...grpc.CallOption) (*Journal, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Journal)
err := c.cc.Invoke(ctx, GeneralLedgerService_GetJournal_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *generalLedgerServiceClient) ListEntries(ctx context.Context, in *ListEntriesRequest, opts ...grpc.CallOption) (*ListEntriesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListEntriesResponse)
err := c.cc.Invoke(ctx, GeneralLedgerService_ListEntries_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *generalLedgerServiceClient) GetBalance(ctx context.Context, in *GetBalanceRequest, opts ...grpc.CallOption) (*GetBalanceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetBalanceResponse)
err := c.cc.Invoke(ctx, GeneralLedgerService_GetBalance_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *generalLedgerServiceClient) ReplayJournals(ctx context.Context, in *ReplayJournalsRequest, opts ...grpc.CallOption) (*ReplayJournalsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ReplayJournalsResponse)
err := c.cc.Invoke(ctx, GeneralLedgerService_ReplayJournals_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// GeneralLedgerServiceServer is the server API for GeneralLedgerService service.
// All implementations should embed UnimplementedGeneralLedgerServiceServer
// for forward compatibility.
type GeneralLedgerServiceServer interface {
Health(context.Context, *v1.Empty) (*HealthResponse, error)
AppendJournal(context.Context, *AppendJournalRequest) (*AppendJournalResponse, error)
AppendTransactionEvent(context.Context, *AppendTransactionEventRequest) (*AppendTransactionEventResponse, error)
GetJournal(context.Context, *GetJournalRequest) (*Journal, error)
ListEntries(context.Context, *ListEntriesRequest) (*ListEntriesResponse, error)
GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error)
ReplayJournals(context.Context, *ReplayJournalsRequest) (*ReplayJournalsResponse, error)
}
// UnimplementedGeneralLedgerServiceServer should be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedGeneralLedgerServiceServer struct{}
func (UnimplementedGeneralLedgerServiceServer) Health(context.Context, *v1.Empty) (*HealthResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Health not implemented")
}
func (UnimplementedGeneralLedgerServiceServer) AppendJournal(context.Context, *AppendJournalRequest) (*AppendJournalResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AppendJournal not implemented")
}
func (UnimplementedGeneralLedgerServiceServer) AppendTransactionEvent(context.Context, *AppendTransactionEventRequest) (*AppendTransactionEventResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AppendTransactionEvent not implemented")
}
func (UnimplementedGeneralLedgerServiceServer) GetJournal(context.Context, *GetJournalRequest) (*Journal, error) {
return nil, status.Error(codes.Unimplemented, "method GetJournal not implemented")
}
func (UnimplementedGeneralLedgerServiceServer) ListEntries(context.Context, *ListEntriesRequest) (*ListEntriesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListEntries not implemented")
}
func (UnimplementedGeneralLedgerServiceServer) GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetBalance not implemented")
}
func (UnimplementedGeneralLedgerServiceServer) ReplayJournals(context.Context, *ReplayJournalsRequest) (*ReplayJournalsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ReplayJournals not implemented")
}
func (UnimplementedGeneralLedgerServiceServer) testEmbeddedByValue() {}
// UnsafeGeneralLedgerServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to GeneralLedgerServiceServer will
// result in compilation errors.
type UnsafeGeneralLedgerServiceServer interface {
mustEmbedUnimplementedGeneralLedgerServiceServer()
}
func RegisterGeneralLedgerServiceServer(s grpc.ServiceRegistrar, srv GeneralLedgerServiceServer) {
// If the following call panics, it indicates UnimplementedGeneralLedgerServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&GeneralLedgerService_ServiceDesc, srv)
}
func _GeneralLedgerService_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(v1.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeneralLedgerServiceServer).Health(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GeneralLedgerService_Health_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeneralLedgerServiceServer).Health(ctx, req.(*v1.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _GeneralLedgerService_AppendJournal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AppendJournalRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeneralLedgerServiceServer).AppendJournal(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GeneralLedgerService_AppendJournal_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeneralLedgerServiceServer).AppendJournal(ctx, req.(*AppendJournalRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GeneralLedgerService_AppendTransactionEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AppendTransactionEventRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeneralLedgerServiceServer).AppendTransactionEvent(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GeneralLedgerService_AppendTransactionEvent_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeneralLedgerServiceServer).AppendTransactionEvent(ctx, req.(*AppendTransactionEventRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GeneralLedgerService_GetJournal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetJournalRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeneralLedgerServiceServer).GetJournal(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GeneralLedgerService_GetJournal_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeneralLedgerServiceServer).GetJournal(ctx, req.(*GetJournalRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GeneralLedgerService_ListEntries_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListEntriesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeneralLedgerServiceServer).ListEntries(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GeneralLedgerService_ListEntries_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeneralLedgerServiceServer).ListEntries(ctx, req.(*ListEntriesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GeneralLedgerService_GetBalance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetBalanceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeneralLedgerServiceServer).GetBalance(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GeneralLedgerService_GetBalance_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeneralLedgerServiceServer).GetBalance(ctx, req.(*GetBalanceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GeneralLedgerService_ReplayJournals_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReplayJournalsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeneralLedgerServiceServer).ReplayJournals(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GeneralLedgerService_ReplayJournals_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeneralLedgerServiceServer).ReplayJournals(ctx, req.(*ReplayJournalsRequest))
}
return interceptor(ctx, in, info, handler)
}
// GeneralLedgerService_ServiceDesc is the grpc.ServiceDesc for GeneralLedgerService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var GeneralLedgerService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "ledger.v1.GeneralLedgerService",
HandlerType: (*GeneralLedgerServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Health",
Handler: _GeneralLedgerService_Health_Handler,
},
{
MethodName: "AppendJournal",
Handler: _GeneralLedgerService_AppendJournal_Handler,
},
{
MethodName: "AppendTransactionEvent",
Handler: _GeneralLedgerService_AppendTransactionEvent_Handler,
},
{
MethodName: "GetJournal",
Handler: _GeneralLedgerService_GetJournal_Handler,
},
{
MethodName: "ListEntries",
Handler: _GeneralLedgerService_ListEntries_Handler,
},
{
MethodName: "GetBalance",
Handler: _GeneralLedgerService_GetBalance_Handler,
},
{
MethodName: "ReplayJournals",
Handler: _GeneralLedgerService_ReplayJournals_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "ledger/v1/srv.proto",
}
+14
View File
@@ -0,0 +1,14 @@
environment = "local"
[grpc]
host = "0.0.0.0"
port = 8500
shutdown-timeout = "10s"
[database]
host = "127.0.0.1"
port = 5432
name = "gl_db"
user = "postgres"
password = ""
ssl-mode = "disable"
+24
View File
@@ -0,0 +1,24 @@
module gl
go 1.24
require (
github.com/knadh/koanf/parsers/toml v0.1.0
github.com/knadh/koanf/providers/file v1.2.1
github.com/knadh/koanf/v2 v2.3.4
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.36.6
)
require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
)
+40
View File
@@ -0,0 +1,40 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI=
github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18=
github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM=
github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
github.com/knadh/koanf/v2 v2.3.4 h1:fnynNSDlujWE+v83hAp8wKr/cdoxHLO0629SN+U8Urc=
github.com/knadh/koanf/v2 v2.3.4/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+81
View File
@@ -0,0 +1,81 @@
// Package config owns GL configuration loading and validation.
package config
import (
"fmt"
"time"
"github.com/knadh/koanf/parsers/toml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
)
type Config struct {
Environment string `koanf:"environment"`
GRPC GRPCConfig `koanf:"grpc"`
Database DatabaseConfig `koanf:"database"`
}
type GRPCConfig struct {
Host string `koanf:"host"`
Port int `koanf:"port"`
ShutdownTimeout time.Duration `koanf:"shutdown-timeout"`
}
type DatabaseConfig struct {
Host string `koanf:"host"`
Port int `koanf:"port"`
Name string `koanf:"name"`
User string `koanf:"user"`
Password string `koanf:"password"`
SSLMode string `koanf:"ssl-mode"`
}
func Load(path string) (*Config, error) {
cfg := &Config{
Environment: "local",
GRPC: GRPCConfig{
Host: "0.0.0.0",
Port: 8500,
ShutdownTimeout: 10 * time.Second,
},
Database: DatabaseConfig{
Host: "127.0.0.1",
Port: 5432,
Name: "gl_db",
User: "postgres",
SSLMode: "disable",
},
}
k := koanf.New(".")
if err := k.Load(file.Provider(path), toml.Parser()); err != nil {
return nil, fmt.Errorf("load config: %w", err)
}
if err := k.Unmarshal("", cfg); err != nil {
return nil, fmt.Errorf("decode config: %w", err)
}
if err := cfg.Validate(); err != nil {
return nil, err
}
return cfg, nil
}
func (c *Config) Validate() error {
if c.GRPC.Host == "" {
return fmt.Errorf("grpc host is required")
}
if c.GRPC.Port < 0 || c.GRPC.Port > 65535 {
return fmt.Errorf("grpc port must be between 0 and 65535")
}
if c.GRPC.ShutdownTimeout <= 0 {
return fmt.Errorf("grpc shutdown timeout must be positive")
}
if c.Database.Host == "" || c.Database.Name == "" || c.Database.User == "" {
return fmt.Errorf("database host, name, and user are required")
}
if c.Database.Port < 1 || c.Database.Port > 65535 {
return fmt.Errorf("database port must be between 1 and 65535")
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package config
import (
"os"
"path/filepath"
"testing"
"time"
)
func TestLoadUsesDefaultsAndOverrides(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "gl.toml")
contents := []byte("[grpc]\nport = 0\nshutdown-timeout = \"3s\"\n")
if err := os.WriteFile(path, contents, 0o600); err != nil {
t.Fatal(err)
}
cfg, err := Load(path)
if err != nil {
t.Fatal(err)
}
if cfg.GRPC.Port != 0 || cfg.GRPC.ShutdownTimeout != 3*time.Second {
t.Fatalf("unexpected grpc config: %+v", cfg.GRPC)
}
if cfg.Database.Name != "gl_db" || cfg.Database.Port != 5432 {
t.Fatalf("unexpected database defaults: %+v", cfg.Database)
}
}
func TestLoadRejectsInvalidConfiguration(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "gl.toml")
if err := os.WriteFile(path, []byte("[grpc]\nshutdown-timeout = \"0s\"\n"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := Load(path); err == nil {
t.Fatal("expected validation error")
}
}
func TestLoadReturnsMissingFileError(t *testing.T) {
if _, err := Load(filepath.Join(t.TempDir(), "missing.toml")); err == nil {
t.Fatal("expected missing file error")
}
}
+26
View File
@@ -0,0 +1,26 @@
package grpcadapter
import (
"context"
"gl/application/health"
basev1 "gl/gen/base/v1"
ledgerv1 "gl/gen/ledger/v1"
)
type HealthHandler struct {
ledgerv1.UnimplementedGeneralLedgerServiceServer
service *health.Service
}
func NewHealthHandler(service *health.Service) *HealthHandler {
return &HealthHandler{service: service}
}
func (h *HealthHandler) Health(ctx context.Context, _ *basev1.Empty) (*ledgerv1.HealthResponse, error) {
result := h.service.Check(ctx)
return &ledgerv1.HealthResponse{
Serving: result.Serving,
DatabaseReady: result.DatabaseReady,
}, nil
}
+19
View File
@@ -0,0 +1,19 @@
package grpcadapter
import (
"context"
"testing"
"gl/application/health"
basev1 "gl/gen/base/v1"
)
func TestHealth(t *testing.T) {
response, err := NewHealthHandler(health.NewService(nil)).Health(context.Background(), &basev1.Empty{})
if err != nil {
t.Fatal(err)
}
if !response.Serving || response.DatabaseReady {
t.Fatalf("unexpected response: %+v", response)
}
}
+69
View File
@@ -0,0 +1,69 @@
package grpcadapter
import (
"context"
"errors"
"fmt"
"net"
"time"
ledgerv1 "gl/gen/ledger/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
type ServerConfig struct {
Host string
Port int
ShutdownTimeout time.Duration
}
func Run(ctx context.Context, cfg ServerConfig, handler ledgerv1.GeneralLedgerServiceServer) error {
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
if err != nil {
return fmt.Errorf("listen for grpc: %w", err)
}
return runWithListener(ctx, cfg.ShutdownTimeout, listener, handler)
}
func runWithListener(ctx context.Context, shutdownTimeout time.Duration, listener net.Listener, handler ledgerv1.GeneralLedgerServiceServer) error {
defer listener.Close()
server := grpc.NewServer()
ledgerv1.RegisterGeneralLedgerServiceServer(server, handler)
reflection.Register(server)
serveErr := make(chan error, 1)
go func() { serveErr <- server.Serve(listener) }()
select {
case err := <-serveErr:
if errors.Is(err, grpc.ErrServerStopped) {
return nil
}
return fmt.Errorf("serve grpc: %w", err)
case <-ctx.Done():
}
stopped := make(chan struct{})
go func() {
server.GracefulStop()
close(stopped)
}()
timer := time.NewTimer(shutdownTimeout)
defer timer.Stop()
select {
case <-stopped:
case <-timer.C:
server.Stop()
<-stopped
}
err := <-serveErr
if err != nil && !errors.Is(err, grpc.ErrServerStopped) {
return fmt.Errorf("serve grpc: %w", err)
}
return nil
}
+26
View File
@@ -0,0 +1,26 @@
package grpcadapter
import (
"context"
"testing"
"time"
"gl/application/health"
"google.golang.org/grpc/test/bufconn"
)
func TestRunStopsWhenContextIsCancelled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := runWithListener(
ctx,
time.Second,
bufconn.Listen(1024),
NewHealthHandler(health.NewService(nil)),
)
if err != nil {
t.Fatal(err)
}
}