Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee88f9d647 | |||
| 9dcd4baff2 | |||
| 7a0743496f | |||
| bcfbd1cfc8 | |||
| 8e3b0c1c4a |
@@ -0,0 +1,55 @@
|
|||||||
|
# Go Client
|
||||||
|
|
||||||
|
The Go client module contains the generated MXAccess Gateway protobuf bindings,
|
||||||
|
a small handwritten `mxgateway` package, and the `mxgw-go` test CLI scaffold.
|
||||||
|
The module uses the shared proto inputs documented in
|
||||||
|
`../../docs/client-proto-generation.md` so gateway and client contracts stay in
|
||||||
|
sync.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
clients/go/
|
||||||
|
go.mod
|
||||||
|
generate-proto.ps1
|
||||||
|
internal/generated/
|
||||||
|
mxgateway/
|
||||||
|
cmd/mxgw-go/
|
||||||
|
```
|
||||||
|
|
||||||
|
`internal/generated` contains code produced by `protoc`, `protoc-gen-go`, and
|
||||||
|
`protoc-gen-go-grpc`. Do not edit generated files by hand.
|
||||||
|
|
||||||
|
## Regenerating Protobuf Bindings
|
||||||
|
|
||||||
|
Run generation after the shared `.proto` files or the Go output path changes:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./generate-proto.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The script uses the tool paths recorded in `../../docs/toolchain-links.md`.
|
||||||
|
|
||||||
|
## Build And Test
|
||||||
|
|
||||||
|
Run the Go module checks from `clients/go`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go test ./...
|
||||||
|
go build ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
The scaffold tests parse the shared golden JSON fixtures with the generated Go
|
||||||
|
types. Later client implementation tests add fake gRPC services, auth metadata,
|
||||||
|
streaming, value conversion, and CLI behavior.
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
The scaffold CLI exposes version information:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go run ./cmd/mxgw-go version -json
|
||||||
|
```
|
||||||
|
|
||||||
|
Additional commands are implemented with the client/session wrapper work.
|
||||||
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway"
|
||||||
|
)
|
||||||
|
|
||||||
|
type versionOutput struct {
|
||||||
|
ClientVersion string `json:"clientVersion"`
|
||||||
|
GatewayProtocolVersion uint32 `json:"gatewayProtocolVersion"`
|
||||||
|
WorkerProtocolVersion uint32 `json:"workerProtocolVersion"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(os.Args[1:]); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("usage: mxgw-go version [-json]")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
case "version":
|
||||||
|
return runVersion(args[1:])
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown command %q", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVersion(args []string) error {
|
||||||
|
flags := flag.NewFlagSet("version", flag.ContinueOnError)
|
||||||
|
flags.SetOutput(os.Stderr)
|
||||||
|
jsonOutput := flags.Bool("json", false, "write JSON output")
|
||||||
|
|
||||||
|
if err := flags.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
output := versionOutput{
|
||||||
|
ClientVersion: mxgateway.ClientVersion,
|
||||||
|
GatewayProtocolVersion: mxgateway.GatewayProtocolVersion,
|
||||||
|
WorkerProtocolVersion: mxgateway.WorkerProtocolVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
if *jsonOutput {
|
||||||
|
encoder := json.NewEncoder(os.Stdout)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
return encoder.Encode(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stdout, "mxgw-go %s\n", output.ClientVersion)
|
||||||
|
fmt.Fprintf(os.Stdout, "gateway protocol %d\n", output.GatewayProtocolVersion)
|
||||||
|
fmt.Fprintf(os.Stdout, "worker protocol %d\n", output.WorkerProtocolVersion)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
||||||
|
$protoRoot = Join-Path $repoRoot 'src\MxGateway.Contracts\Protos'
|
||||||
|
$outputRoot = Join-Path $PSScriptRoot 'internal\generated'
|
||||||
|
$modulePath = 'gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated'
|
||||||
|
$protoc = 'C:\Users\dohertj2\AppData\Local\Microsoft\WinGet\Packages\Google.Protobuf_Microsoft.Winget.Source_8wekyb3d8bbwe\bin\protoc.exe'
|
||||||
|
$goPluginPath = 'C:\Users\dohertj2\go\bin'
|
||||||
|
|
||||||
|
if (-not (Test-Path $protoc)) {
|
||||||
|
throw "protoc was not found at $protoc. See docs/toolchain-links.md."
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pluginName in @('protoc-gen-go.exe', 'protoc-gen-go-grpc.exe')) {
|
||||||
|
$pluginPath = Join-Path $goPluginPath $pluginName
|
||||||
|
if (-not (Test-Path $pluginPath)) {
|
||||||
|
throw "$pluginName was not found at $pluginPath. See docs/toolchain-links.md."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null
|
||||||
|
Get-ChildItem -Path $outputRoot -Filter '*.pb.go' -File | Remove-Item
|
||||||
|
|
||||||
|
$env:Path = "$goPluginPath;$env:Path"
|
||||||
|
|
||||||
|
& $protoc `
|
||||||
|
--proto_path=$protoRoot `
|
||||||
|
--go_out=$outputRoot `
|
||||||
|
--go_opt=paths=source_relative `
|
||||||
|
"--go_opt=Mmxaccess_gateway.proto=$modulePath;generated" `
|
||||||
|
"--go_opt=Mmxaccess_worker.proto=$modulePath;generated" `
|
||||||
|
mxaccess_gateway.proto `
|
||||||
|
mxaccess_worker.proto
|
||||||
|
|
||||||
|
& $protoc `
|
||||||
|
--proto_path=$protoRoot `
|
||||||
|
--go-grpc_out=$outputRoot `
|
||||||
|
--go-grpc_opt=paths=source_relative `
|
||||||
|
"--go-grpc_opt=Mmxaccess_gateway.proto=$modulePath;generated" `
|
||||||
|
mxaccess_gateway.proto
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
module gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go
|
||||||
|
|
||||||
|
go 1.26
|
||||||
|
|
||||||
|
require (
|
||||||
|
google.golang.org/grpc v1.80.0
|
||||||
|
google.golang.org/protobuf v1.36.11
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/net v0.49.0 // indirect
|
||||||
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
|
golang.org/x/text v0.33.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,243 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
|
// - protoc v7.34.1
|
||||||
|
// source: mxaccess_gateway.proto
|
||||||
|
|
||||||
|
package generated
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
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 (
|
||||||
|
MxAccessGateway_OpenSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/OpenSession"
|
||||||
|
MxAccessGateway_CloseSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/CloseSession"
|
||||||
|
MxAccessGateway_Invoke_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/Invoke"
|
||||||
|
MxAccessGateway_StreamEvents_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/StreamEvents"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MxAccessGatewayClient is the client API for MxAccessGateway 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.
|
||||||
|
//
|
||||||
|
// Public client API for MXAccess sessions hosted by the gateway.
|
||||||
|
type MxAccessGatewayClient interface {
|
||||||
|
OpenSession(ctx context.Context, in *OpenSessionRequest, opts ...grpc.CallOption) (*OpenSessionReply, error)
|
||||||
|
CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*CloseSessionReply, error)
|
||||||
|
Invoke(ctx context.Context, in *MxCommandRequest, opts ...grpc.CallOption) (*MxCommandReply, error)
|
||||||
|
StreamEvents(ctx context.Context, in *StreamEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[MxEvent], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mxAccessGatewayClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMxAccessGatewayClient(cc grpc.ClientConnInterface) MxAccessGatewayClient {
|
||||||
|
return &mxAccessGatewayClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mxAccessGatewayClient) OpenSession(ctx context.Context, in *OpenSessionRequest, opts ...grpc.CallOption) (*OpenSessionReply, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(OpenSessionReply)
|
||||||
|
err := c.cc.Invoke(ctx, MxAccessGateway_OpenSession_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mxAccessGatewayClient) CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*CloseSessionReply, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CloseSessionReply)
|
||||||
|
err := c.cc.Invoke(ctx, MxAccessGateway_CloseSession_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mxAccessGatewayClient) Invoke(ctx context.Context, in *MxCommandRequest, opts ...grpc.CallOption) (*MxCommandReply, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(MxCommandReply)
|
||||||
|
err := c.cc.Invoke(ctx, MxAccessGateway_Invoke_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mxAccessGatewayClient) StreamEvents(ctx context.Context, in *StreamEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[MxEvent], error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
stream, err := c.cc.NewStream(ctx, &MxAccessGateway_ServiceDesc.Streams[0], MxAccessGateway_StreamEvents_FullMethodName, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &grpc.GenericClientStream[StreamEventsRequest, MxEvent]{ClientStream: stream}
|
||||||
|
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := x.ClientStream.CloseSend(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
|
type MxAccessGateway_StreamEventsClient = grpc.ServerStreamingClient[MxEvent]
|
||||||
|
|
||||||
|
// MxAccessGatewayServer is the server API for MxAccessGateway service.
|
||||||
|
// All implementations must embed UnimplementedMxAccessGatewayServer
|
||||||
|
// for forward compatibility.
|
||||||
|
//
|
||||||
|
// Public client API for MXAccess sessions hosted by the gateway.
|
||||||
|
type MxAccessGatewayServer interface {
|
||||||
|
OpenSession(context.Context, *OpenSessionRequest) (*OpenSessionReply, error)
|
||||||
|
CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionReply, error)
|
||||||
|
Invoke(context.Context, *MxCommandRequest) (*MxCommandReply, error)
|
||||||
|
StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error
|
||||||
|
mustEmbedUnimplementedMxAccessGatewayServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedMxAccessGatewayServer must 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 UnimplementedMxAccessGatewayServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedMxAccessGatewayServer) OpenSession(context.Context, *OpenSessionRequest) (*OpenSessionReply, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method OpenSession not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedMxAccessGatewayServer) CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionReply, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method CloseSession not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedMxAccessGatewayServer) Invoke(context.Context, *MxCommandRequest) (*MxCommandReply, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Invoke not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedMxAccessGatewayServer) StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error {
|
||||||
|
return status.Error(codes.Unimplemented, "method StreamEvents not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedMxAccessGatewayServer) mustEmbedUnimplementedMxAccessGatewayServer() {}
|
||||||
|
func (UnimplementedMxAccessGatewayServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeMxAccessGatewayServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to MxAccessGatewayServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeMxAccessGatewayServer interface {
|
||||||
|
mustEmbedUnimplementedMxAccessGatewayServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterMxAccessGatewayServer(s grpc.ServiceRegistrar, srv MxAccessGatewayServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedMxAccessGatewayServer 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(&MxAccessGateway_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _MxAccessGateway_OpenSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(OpenSessionRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MxAccessGatewayServer).OpenSession(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: MxAccessGateway_OpenSession_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MxAccessGatewayServer).OpenSession(ctx, req.(*OpenSessionRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _MxAccessGateway_CloseSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CloseSessionRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MxAccessGatewayServer).CloseSession(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: MxAccessGateway_CloseSession_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MxAccessGatewayServer).CloseSession(ctx, req.(*CloseSessionRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _MxAccessGateway_Invoke_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(MxCommandRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MxAccessGatewayServer).Invoke(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: MxAccessGateway_Invoke_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MxAccessGatewayServer).Invoke(ctx, req.(*MxCommandRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _MxAccessGateway_StreamEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
m := new(StreamEventsRequest)
|
||||||
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.(MxAccessGatewayServer).StreamEvents(m, &grpc.GenericServerStream[StreamEventsRequest, MxEvent]{ServerStream: stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
|
type MxAccessGateway_StreamEventsServer = grpc.ServerStreamingServer[MxEvent]
|
||||||
|
|
||||||
|
// MxAccessGateway_ServiceDesc is the grpc.ServiceDesc for MxAccessGateway service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var MxAccessGateway_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "mxaccess_gateway.v1.MxAccessGateway",
|
||||||
|
HandlerType: (*MxAccessGatewayServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "OpenSession",
|
||||||
|
Handler: _MxAccessGateway_OpenSession_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CloseSession",
|
||||||
|
Handler: _MxAccessGateway_CloseSession_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Invoke",
|
||||||
|
Handler: _MxAccessGateway_Invoke_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{
|
||||||
|
{
|
||||||
|
StreamName: "StreamEvents",
|
||||||
|
Handler: _MxAccessGateway_StreamEvents_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: "mxaccess_gateway.proto",
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
|||||||
|
package mxgateway
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// Options configures future gateway connections.
|
||||||
|
type Options struct {
|
||||||
|
Endpoint string
|
||||||
|
APIKey string
|
||||||
|
Plaintext bool
|
||||||
|
CACertFile string
|
||||||
|
ServerNameOverride string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedactedAPIKey returns a display-safe representation of the configured API
|
||||||
|
// key for diagnostics and CLI output.
|
||||||
|
func (o Options) RedactedAPIKey() string {
|
||||||
|
return RedactAPIKey(o.APIKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedactAPIKey hides credential material while keeping enough shape for
|
||||||
|
// troubleshooting whether a key was supplied.
|
||||||
|
func RedactAPIKey(apiKey string) string {
|
||||||
|
if apiKey == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apiKey) <= 8 {
|
||||||
|
return "<redacted>"
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix, suffix := apiKey[:4], apiKey[len(apiKey)-4:]
|
||||||
|
return prefix + strings.Repeat("*", len(apiKey)-8) + suffix
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package mxgateway
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestRedactAPIKey(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
apiKey string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "empty", apiKey: "", want: ""},
|
||||||
|
{name: "short", apiKey: "mxgw_1", want: "<redacted>"},
|
||||||
|
{name: "long", apiKey: "mxgw_key_secret", want: "mxgw*******cret"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := RedactAPIKey(tt.apiKey); got != tt.want {
|
||||||
|
t.Fatalf("RedactAPIKey() = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package mxgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGeneratedGoldenFixturesParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
msg proto.Message
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "open session reply",
|
||||||
|
path: filepath.Join("..", "..", "proto", "fixtures", "golden", "open-session-reply.ok.json"),
|
||||||
|
msg: &pb.OpenSessionReply{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "register command request",
|
||||||
|
path: filepath.Join("..", "..", "proto", "fixtures", "golden", "register-command-request.json"),
|
||||||
|
msg: &pb.MxCommandRequest{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "on data change event",
|
||||||
|
path: filepath.Join("..", "..", "proto", "fixtures", "golden", "on-data-change-event.json"),
|
||||||
|
msg: &pb.MxEvent{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
unmarshal := protojson.UnmarshalOptions{DiscardUnknown: false}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
data, err := os.ReadFile(tt.path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := unmarshal.Unmarshal(data, tt.msg); err != nil {
|
||||||
|
t.Fatalf("parse fixture: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenSessionFixtureProtocolVersions(t *testing.T) {
|
||||||
|
data, err := os.ReadFile(filepath.Join("..", "..", "proto", "fixtures", "golden", "open-session-reply.ok.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply pb.OpenSessionReply
|
||||||
|
if err := protojson.Unmarshal(data, &reply); err != nil {
|
||||||
|
t.Fatalf("parse fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply.GetGatewayProtocolVersion() != GatewayProtocolVersion {
|
||||||
|
t.Fatalf("gateway protocol = %d, want %d", reply.GetGatewayProtocolVersion(), GatewayProtocolVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply.GetWorkerProtocolVersion() != WorkerProtocolVersion {
|
||||||
|
t.Fatalf("worker protocol = %d, want %d", reply.GetWorkerProtocolVersion(), WorkerProtocolVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package mxgateway
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ClientVersion identifies this Go client scaffold before package releases
|
||||||
|
// assign semantic versions.
|
||||||
|
ClientVersion = "0.1.0-dev"
|
||||||
|
|
||||||
|
// GatewayProtocolVersion matches GatewayContractInfo.GatewayProtocolVersion
|
||||||
|
// in the shared .NET contracts.
|
||||||
|
GatewayProtocolVersion uint32 = 1
|
||||||
|
|
||||||
|
// WorkerProtocolVersion matches GatewayContractInfo.WorkerProtocolVersion
|
||||||
|
// and is exposed for fake-worker and parity tests.
|
||||||
|
WorkerProtocolVersion uint32 = 1
|
||||||
|
)
|
||||||
Generated
+1308
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
|||||||
|
[package]
|
||||||
|
name = "mxgateway-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["crates/mxgw-cli"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
clap = { version = "4.5.53", features = ["derive"] }
|
||||||
|
prost = "0.13.5"
|
||||||
|
prost-types = "0.13.5"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.145"
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
tonic = { version = "0.13.1", features = ["transport"] }
|
||||||
|
tonic-build = "0.13.1"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
prost = { workspace = true }
|
||||||
|
prost-types = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tonic = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tonic-build = { workspace = true }
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Rust Client Workspace
|
||||||
|
|
||||||
|
The Rust client workspace contains the MXAccess Gateway client library, a
|
||||||
|
test CLI, and scaffold tests for generated contract wiring. The library uses
|
||||||
|
the shared protobuf inputs documented in
|
||||||
|
`../../docs/client-proto-generation.md` so the Rust bindings compile against
|
||||||
|
the same public gateway and worker contracts as the server.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
clients/rust/
|
||||||
|
Cargo.toml
|
||||||
|
build.rs
|
||||||
|
src/
|
||||||
|
tests/
|
||||||
|
crates/mxgw-cli/
|
||||||
|
```
|
||||||
|
|
||||||
|
`build.rs` reads the `.proto` files from
|
||||||
|
`../../src/MxGateway.Contracts/Protos` and generates `tonic`/`prost` bindings
|
||||||
|
into Cargo build output. `src/generated.rs` declares the Rust modules that
|
||||||
|
include those generated files. `src/generated` remains reserved for checked-in
|
||||||
|
generator output if the crate later changes to source-tree generation.
|
||||||
|
|
||||||
|
## Build And Test
|
||||||
|
|
||||||
|
Run the Rust workspace checks from `clients/rust`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cargo fmt --all --check
|
||||||
|
cargo test --workspace
|
||||||
|
cargo check --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
The build script uses `protoc` from `PATH` or the Windows path recorded in
|
||||||
|
`../../docs/toolchain-links.md`.
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
The scaffold CLI exposes version information:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cargo run -p mxgw-cli -- version --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Additional commands are implemented with the client/session wrapper work.
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
||||||
|
- [Rust Client Detailed Design](../../docs/clients-rust-design.md)
|
||||||
|
- [Rust Style Guide](../../docs/style-guides/RustStyleGuide.md)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
configure_protoc();
|
||||||
|
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
|
||||||
|
let repo_root = manifest_dir
|
||||||
|
.parent()
|
||||||
|
.and_then(Path::parent)
|
||||||
|
.ok_or("clients/rust must live two levels below the repository root")?;
|
||||||
|
let proto_root = repo_root.join("src/MxGateway.Contracts/Protos");
|
||||||
|
let gateway_proto = proto_root.join("mxaccess_gateway.proto");
|
||||||
|
let worker_proto = proto_root.join("mxaccess_worker.proto");
|
||||||
|
let descriptor_path = PathBuf::from(env::var("OUT_DIR")?).join("mxaccessgw-client-v1.protoset");
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed={}", gateway_proto.display());
|
||||||
|
println!("cargo:rerun-if-changed={}", worker_proto.display());
|
||||||
|
|
||||||
|
tonic_build::configure()
|
||||||
|
.build_server(false)
|
||||||
|
.build_client(true)
|
||||||
|
.file_descriptor_set_path(descriptor_path)
|
||||||
|
.compile_protos(
|
||||||
|
&[gateway_proto.as_path(), worker_proto.as_path()],
|
||||||
|
&[proto_root.as_path()],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_protoc() {
|
||||||
|
if env::var_os("PROTOC").is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for candidate in protoc_candidates() {
|
||||||
|
if candidate.is_file() {
|
||||||
|
env::set_var("PROTOC", candidate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn protoc_candidates() -> Vec<PathBuf> {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
|
||||||
|
if cfg!(windows) {
|
||||||
|
if let Some(local_app_data) = env::var_os("LOCALAPPDATA") {
|
||||||
|
candidates.push(PathBuf::from(local_app_data).join(
|
||||||
|
"Microsoft/WinGet/Packages/Google.Protobuf_Microsoft.Winget.Source_8wekyb3d8bbwe/bin/protoc.exe",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.push(PathBuf::from("protoc"));
|
||||||
|
candidates
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "mxgw-cli"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "mxgw"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { workspace = true }
|
||||||
|
mxgateway-client = { path = "../.." }
|
||||||
|
serde_json = { workspace = true }
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use mxgateway_client::{CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(name = "mxgw")]
|
||||||
|
#[command(about = "MXAccess Gateway Rust test CLI")]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum Command {
|
||||||
|
Version {
|
||||||
|
#[arg(long)]
|
||||||
|
json: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
run(cli);
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(cli: Cli) {
|
||||||
|
match cli.command {
|
||||||
|
Command::Version { json } => print_version(json),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_version(use_json: bool) {
|
||||||
|
if use_json {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
json!({
|
||||||
|
"clientVersion": CLIENT_VERSION,
|
||||||
|
"gatewayProtocolVersion": GATEWAY_PROTOCOL_VERSION,
|
||||||
|
"workerProtocolVersion": WORKER_PROTOCOL_VERSION,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("mxgw {CLIENT_VERSION}");
|
||||||
|
println!("gateway protocol {GATEWAY_PROTOCOL_VERSION}");
|
||||||
|
println!("worker protocol {WORKER_PROTOCOL_VERSION}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use super::Cli;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_version_json_command() {
|
||||||
|
let parsed = Cli::try_parse_from(["mxgw", "version", "--json"]);
|
||||||
|
assert!(parsed.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// API key wrapper that avoids exposing raw credentials in formatted output.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct ApiKey(String);
|
||||||
|
|
||||||
|
impl ApiKey {
|
||||||
|
pub fn new(value: impl Into<String>) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expose_secret(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for ApiKey {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter
|
||||||
|
.debug_tuple("ApiKey")
|
||||||
|
.field(&"<redacted>")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ApiKey {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter.write_str("<redacted>")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::generated::mxaccess_gateway::v1::mx_access_gateway_client::MxAccessGatewayClient;
|
||||||
|
use crate::options::ClientOptions;
|
||||||
|
|
||||||
|
/// Thin owner for the generated gateway client.
|
||||||
|
pub struct GatewayClient {
|
||||||
|
inner: MxAccessGatewayClient<Channel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GatewayClient {
|
||||||
|
pub async fn connect(options: ClientOptions) -> Result<Self, Error> {
|
||||||
|
let endpoint = Channel::from_shared(options.endpoint().to_owned()).map_err(|source| {
|
||||||
|
Error::InvalidEndpoint {
|
||||||
|
endpoint: options.endpoint().to_owned(),
|
||||||
|
detail: source.to_string(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let channel = endpoint.connect().await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
inner: MxAccessGatewayClient::new(channel),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> MxAccessGatewayClient<Channel> {
|
||||||
|
self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use thiserror::Error as ThisError;
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("invalid gateway endpoint `{endpoint}`: {detail}")]
|
||||||
|
InvalidEndpoint { endpoint: String, detail: String },
|
||||||
|
|
||||||
|
#[error("gateway transport error: {0}")]
|
||||||
|
Transport(#[from] tonic::transport::Error),
|
||||||
|
|
||||||
|
#[error("gateway status error: {0}")]
|
||||||
|
Status(#[from] tonic::Status),
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
pub mod mxaccess_gateway {
|
||||||
|
pub mod v1 {
|
||||||
|
#![allow(clippy::large_enum_variant)]
|
||||||
|
|
||||||
|
tonic::include_proto!("mxaccess_gateway.v1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod mxaccess_worker {
|
||||||
|
pub mod v1 {
|
||||||
|
#![allow(clippy::large_enum_variant)]
|
||||||
|
|
||||||
|
tonic::include_proto!("mxaccess_worker.v1");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//! Rust client scaffold for MXAccess Gateway.
|
||||||
|
//!
|
||||||
|
//! The crate compiles generated `tonic` bindings from the shared gateway
|
||||||
|
//! protobuf contracts and exposes a small handwritten surface for future client
|
||||||
|
//! implementation work.
|
||||||
|
|
||||||
|
pub mod auth;
|
||||||
|
pub mod client;
|
||||||
|
pub mod error;
|
||||||
|
pub mod generated;
|
||||||
|
pub mod options;
|
||||||
|
pub mod session;
|
||||||
|
pub mod value;
|
||||||
|
pub mod version;
|
||||||
|
|
||||||
|
pub use auth::ApiKey;
|
||||||
|
pub use client::GatewayClient;
|
||||||
|
pub use error::Error;
|
||||||
|
pub use options::ClientOptions;
|
||||||
|
pub use session::Session;
|
||||||
|
pub use version::{CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::auth::ApiKey;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ClientOptions {
|
||||||
|
endpoint: String,
|
||||||
|
api_key: Option<ApiKey>,
|
||||||
|
plaintext: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientOptions {
|
||||||
|
pub fn new(endpoint: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
endpoint: endpoint.into(),
|
||||||
|
api_key: None,
|
||||||
|
plaintext: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_api_key(mut self, api_key: ApiKey) -> Self {
|
||||||
|
self.api_key = Some(api_key);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn endpoint(&self) -> &str {
|
||||||
|
&self.endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn api_key(&self) -> Option<&ApiKey> {
|
||||||
|
self.api_key.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plaintext(&self) -> bool {
|
||||||
|
self.plaintext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ClientOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new("http://127.0.0.1:5000")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for ClientOptions {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter
|
||||||
|
.debug_struct("ClientOptions")
|
||||||
|
.field("endpoint", &self.endpoint)
|
||||||
|
.field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
|
||||||
|
.field("plaintext", &self.plaintext)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/// Session identifier returned by the gateway.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct Session {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
Self { id: id.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
use crate::generated::mxaccess_gateway::v1::MxValue;
|
||||||
|
|
||||||
|
pub fn int32_value(value: i32) -> MxValue {
|
||||||
|
MxValue {
|
||||||
|
data_type: crate::generated::mxaccess_gateway::v1::MxDataType::Integer as i32,
|
||||||
|
kind: Some(crate::generated::mxaccess_gateway::v1::mx_value::Kind::Int32Value(value)),
|
||||||
|
..MxValue::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub const CLIENT_VERSION: &str = "0.1.0-dev";
|
||||||
|
pub const GATEWAY_PROTOCOL_VERSION: u32 = 1;
|
||||||
|
pub const WORKER_PROTOCOL_VERSION: u32 = 1;
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use mxgateway_client::generated::mxaccess_gateway::v1::{
|
||||||
|
mx_command, mx_value, MxCommand, MxCommandKind, MxCommandRequest, MxDataType, MxEvent,
|
||||||
|
MxEventFamily, MxValue, OpenSessionReply, ProtocolStatusCode, RegisterCommand,
|
||||||
|
};
|
||||||
|
use mxgateway_client::{GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_golden_fixtures_are_available() {
|
||||||
|
for fixture_name in [
|
||||||
|
"open-session-reply.ok.json",
|
||||||
|
"register-command-request.json",
|
||||||
|
"on-data-change-event.json",
|
||||||
|
] {
|
||||||
|
let fixture = read_fixture(fixture_name);
|
||||||
|
assert!(
|
||||||
|
fixture.is_object(),
|
||||||
|
"{fixture_name} must remain a protobuf JSON object"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_session_fixture_matches_protocol_versions() {
|
||||||
|
let fixture = read_fixture("open-session-reply.ok.json");
|
||||||
|
let reply = OpenSessionReply {
|
||||||
|
session_id: string_field(&fixture, "sessionId"),
|
||||||
|
backend_name: string_field(&fixture, "backendName"),
|
||||||
|
worker_process_id: i32_field(&fixture, "workerProcessId"),
|
||||||
|
worker_protocol_version: u32_field(&fixture, "workerProtocolVersion"),
|
||||||
|
gateway_protocol_version: u32_field(&fixture, "gatewayProtocolVersion"),
|
||||||
|
protocol_status: Some(
|
||||||
|
mxgateway_client::generated::mxaccess_gateway::v1::ProtocolStatus {
|
||||||
|
code: ProtocolStatusCode::Ok as i32,
|
||||||
|
message: string_field(&fixture["protocolStatus"], "message"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
..OpenSessionReply::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(reply.gateway_protocol_version, GATEWAY_PROTOCOL_VERSION);
|
||||||
|
assert_eq!(reply.worker_protocol_version, WORKER_PROTOCOL_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn register_fixture_can_build_generated_request() {
|
||||||
|
let fixture = read_fixture("register-command-request.json");
|
||||||
|
let command = &fixture["command"];
|
||||||
|
let request = MxCommandRequest {
|
||||||
|
session_id: string_field(&fixture, "sessionId"),
|
||||||
|
client_correlation_id: string_field(&fixture, "clientCorrelationId"),
|
||||||
|
command: Some(MxCommand {
|
||||||
|
kind: MxCommandKind::Register as i32,
|
||||||
|
payload: Some(mx_command::Payload::Register(RegisterCommand {
|
||||||
|
client_name: string_field(&command["register"], "clientName"),
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(request.session_id, "session-fixture");
|
||||||
|
assert_eq!(
|
||||||
|
request.command.unwrap().kind,
|
||||||
|
MxCommandKind::Register as i32
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn on_data_change_fixture_can_build_generated_event() {
|
||||||
|
let fixture = read_fixture("on-data-change-event.json");
|
||||||
|
let event = MxEvent {
|
||||||
|
family: MxEventFamily::OnDataChange as i32,
|
||||||
|
session_id: string_field(&fixture, "sessionId"),
|
||||||
|
server_handle: i32_field(&fixture, "serverHandle"),
|
||||||
|
item_handle: i32_field(&fixture, "itemHandle"),
|
||||||
|
value: Some(MxValue {
|
||||||
|
data_type: MxDataType::Integer as i32,
|
||||||
|
variant_type: string_field(&fixture["value"], "variantType"),
|
||||||
|
kind: Some(mx_value::Kind::Int32Value(i32_field(
|
||||||
|
&fixture["value"],
|
||||||
|
"int32Value",
|
||||||
|
))),
|
||||||
|
..MxValue::default()
|
||||||
|
}),
|
||||||
|
quality: i32_field(&fixture, "quality"),
|
||||||
|
worker_sequence: u64_field(&fixture, "workerSequence"),
|
||||||
|
..MxEvent::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(event.family, MxEventFamily::OnDataChange as i32);
|
||||||
|
assert_eq!(event.value.unwrap().data_type, MxDataType::Integer as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_fixture(name: &str) -> Value {
|
||||||
|
let path = fixture_root().join(name);
|
||||||
|
let data = fs::read_to_string(&path).unwrap_or_else(|error| {
|
||||||
|
panic!("failed to read fixture {}: {error}", path.display());
|
||||||
|
});
|
||||||
|
|
||||||
|
serde_json::from_str(&data).unwrap_or_else(|error| {
|
||||||
|
panic!("failed to parse fixture {}: {error}", path.display());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_root() -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto/fixtures/golden")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_field(value: &Value, name: &str) -> String {
|
||||||
|
value[name]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_else(|| panic!("missing string field {name}"))
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn i32_field(value: &Value, name: &str) -> i32 {
|
||||||
|
value[name]
|
||||||
|
.as_i64()
|
||||||
|
.unwrap_or_else(|| panic!("missing i32 field {name}"))
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or_else(|_| panic!("field {name} does not fit in i32"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u32_field(value: &Value, name: &str) -> u32 {
|
||||||
|
value[name]
|
||||||
|
.as_u64()
|
||||||
|
.unwrap_or_else(|| panic!("missing u32 field {name}"))
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or_else(|_| panic!("field {name} does not fit in u32"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u64_field(value: &Value, name: &str) -> u64 {
|
||||||
|
if let Some(number) = value[name].as_u64() {
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
value[name]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_else(|| panic!("missing u64 field {name}"))
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_else(|_| panic!("field {name} does not parse as u64"))
|
||||||
|
}
|
||||||
@@ -100,10 +100,32 @@ Go clients should generate `mxaccess_gateway.proto` and
|
|||||||
`protoc-gen-go` and `protoc-gen-go-grpc`. Keep generated packages internal
|
`protoc-gen-go` and `protoc-gen-go-grpc`. Keep generated packages internal
|
||||||
unless the wrapper API intentionally exposes raw protobuf messages.
|
unless the wrapper API intentionally exposes raw protobuf messages.
|
||||||
|
|
||||||
Rust clients should use `tonic-build` or the selected protobuf generator from
|
The Go scaffold provides a repo-local generation script:
|
||||||
the Rust client build script, with generated modules placed under
|
|
||||||
`clients/rust/src/generated` or included from the build output according to the
|
```powershell
|
||||||
client crate design.
|
clients/go/generate-proto.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The script maps both proto files into the internal Go package
|
||||||
|
`gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated` because
|
||||||
|
the source `.proto` files do not carry Go-specific `go_package` options. This
|
||||||
|
keeps language-specific packaging outside the public contract files.
|
||||||
|
|
||||||
|
Rust clients use `tonic-build` from `clients/rust/build.rs`. The build script
|
||||||
|
reads the shared `.proto` files and emits generated `tonic`/`prost` modules
|
||||||
|
into Cargo build output. `clients/rust/src/generated.rs` contains the module
|
||||||
|
declarations that include those generated files. `clients/rust/src/generated`
|
||||||
|
remains reserved for checked-in generator output if the crate later changes to
|
||||||
|
source-tree generation, and handwritten wrapper code stays outside that
|
||||||
|
directory.
|
||||||
|
|
||||||
|
Run the Rust workspace checks from `clients/rust`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cargo fmt --all --check
|
||||||
|
cargo test --workspace
|
||||||
|
cargo check --workspace
|
||||||
|
```
|
||||||
|
|
||||||
Python clients should use `grpc_tools.protoc` and write generated modules under
|
Python clients should use `grpc_tools.protoc` and write generated modules under
|
||||||
`clients/python/src/mxgateway/generated` so imports stay separate from
|
`clients/python/src/mxgateway/generated` so imports stay separate from
|
||||||
|
|||||||
Reference in New Issue
Block a user