aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/mirum-server/api_cli.go')
-rw-r--r--cmd/mirum-server/api_cli.go463+0 −463
1 files changed, 0 insertions, 463 deletions
diff --git a/cmd/mirum-server/api_cli.go b/cmd/mirum-server/api_cli.go
deleted file mode 100644
--- a/cmd/mirum-server/api_cli.go
+++ /dev/null
@@ -1,463 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Nikolay Govorov
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package main
-
-// Console CLI is generated from api.proto at startup via protoreflect.
-// RPC name is camelCase-split into a cobra path: UserCreate -> "user create",
-// OrgMemberAdd -> "org member add". Flags come from request fields, dispatch
-// goes through reflect on apipbconnect.ConsoleClient.
-
-import (
- "context"
- "crypto/ed25519"
- "crypto/x509"
- "encoding/base64"
- "fmt"
- "os"
- "reflect"
- "strings"
- "unicode"
-
- "github.com/spf13/cobra"
- "github.com/spf13/pflag"
- "google.golang.org/protobuf/proto"
- "google.golang.org/protobuf/reflect/protoreflect"
- "google.golang.org/protobuf/reflect/protoregistry"
- "google.golang.org/protobuf/types/known/timestamppb"
-
- "dimidiumlabs/mirum/cmd/mirum-server/apipb"
- "dimidiumlabs/mirum/cmd/mirum-server/apipb/apipbconnect"
-)
-
-// mkClient is called per-invocation so persistent flags (e.g. --socket) are
-// already parsed by the time it runs.
-func buildConsoleCLI(root *cobra.Command, mkClient func() apipbconnect.ConsoleClient) {
- methods := apipb.File_api_proto.Services().ByName("Console").Methods()
- for i := 0; i < methods.Len(); i++ {
- md := methods.Get(i)
- path := splitCamel(string(md.Name()))
- parent := ensureGroups(root, path[:len(path)-1])
- parent.AddCommand(buildMethodCmd(md, path[len(path)-1], mkClient))
- }
-}
-
-// "OrgMemberAdd" -> ["org","member","add"].
-func splitCamel(s string) []string {
- var parts []string
- start := 0
- for i := 1; i < len(s); i++ {
- if unicode.IsUpper(rune(s[i])) {
- parts = append(parts, strings.ToLower(s[start:i]))
- start = i
- }
- }
- return append(parts, strings.ToLower(s[start:]))
-}
-
-func ensureGroups(root *cobra.Command, path []string) *cobra.Command {
- parent := root
- for _, name := range path {
- var next *cobra.Command
- for _, c := range parent.Commands() {
- if c.Name() == name {
- next = c
- break
- }
- }
- if next == nil {
- next = &cobra.Command{Use: name, Short: "Manage " + name}
- parent.AddCommand(next)
- }
- parent = next
- }
- return parent
-}
-
-// fieldSetter writes one flag value into the request message.
-type fieldSetter func(*pflag.FlagSet, protoreflect.Message) error
-
-func buildMethodCmd(md protoreflect.MethodDescriptor, leaf string, mkClient func() apipbconnect.ConsoleClient) *cobra.Command {
- reqDesc := md.Input()
- rpcName := string(md.Name())
- cmd := &cobra.Command{
- Use: leaf,
- Short: rpcName,
- }
- setters := registerRequestFlags(cmd, reqDesc)
- cmd.Run = func(c *cobra.Command, _ []string) {
- req, err := buildRequest(reqDesc, c.Flags(), setters)
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
- resp, err := dispatchConsole(mkClient(), rpcName, req)
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
- printResponse(resp)
- }
- return cmd
-}
-
-func registerRequestFlags(cmd *cobra.Command, desc protoreflect.MessageDescriptor) []fieldSetter {
- var setters []fieldSetter
- fields := desc.Fields()
- for i := 0; i < fields.Len(); i++ {
- if s := registerField(cmd, fields.Get(i)); s != nil {
- setters = append(setters, s)
- }
- }
- return setters
-}
-
-func registerField(cmd *cobra.Command, fd protoreflect.FieldDescriptor) fieldSetter {
- flagName := strings.ReplaceAll(string(fd.Name()), "_", "-")
- // Required iff non-optional in the schema; bools default to false, so
- // marking them required makes no sense.
- required := !fd.HasOptionalKeyword() && fd.Kind() != protoreflect.BoolKind
- flags := cmd.Flags()
-
- markRequired := func() {
- if required {
- _ = cmd.MarkFlagRequired(flagName)
- }
- }
-
- switch fd.Kind() {
- case protoreflect.StringKind:
- flags.String(flagName, "", string(fd.Name()))
- markRequired()
- return func(fs *pflag.FlagSet, m protoreflect.Message) error {
- v, _ := fs.GetString(flagName)
- if v == "" {
- return nil
- }
- m.Set(fd, protoreflect.ValueOfString(v))
- return nil
- }
-
- case protoreflect.BoolKind:
- flags.Bool(flagName, false, string(fd.Name()))
- return func(fs *pflag.FlagSet, m protoreflect.Message) error {
- // Preserve "unset" vs "false" for optional bools.
- if fd.HasOptionalKeyword() && !fs.Changed(flagName) {
- return nil
- }
- v, _ := fs.GetBool(flagName)
- m.Set(fd, protoreflect.ValueOfBool(v))
- return nil
- }
-
- case protoreflect.BytesKind:
- flags.String(flagName, "", string(fd.Name()))
- markRequired()
- return func(fs *pflag.FlagSet, m protoreflect.Message) error {
- v, _ := fs.GetString(flagName)
- if v == "" {
- return nil
- }
- b, err := parseBytesFlag(string(fd.Name()), v)
- if err != nil {
- return fmt.Errorf("--%s: %w", flagName, err)
- }
- m.Set(fd, protoreflect.ValueOfBytes(b))
- return nil
- }
-
- case protoreflect.EnumKind:
- flags.String(flagName, "", string(fd.Name()))
- markRequired()
- return func(fs *pflag.FlagSet, m protoreflect.Message) error {
- v, _ := fs.GetString(flagName)
- if v == "" {
- return nil
- }
- n, err := parseEnumFlag(fd.Enum(), v)
- if err != nil {
- return fmt.Errorf("--%s: %w", flagName, err)
- }
- m.Set(fd, protoreflect.ValueOfEnum(n))
- return nil
- }
-
- case protoreflect.MessageKind:
- return registerMessageField(cmd, fd, flagName, required)
- }
-
- if required {
- panic(fmt.Sprintf("consolecli: unhandled required field %s (kind=%s)", fd.FullName(), fd.Kind()))
- }
- return nil
-}
-
-// Only UserRef/OrgRef are flattened (to the string arm of their oneof);
-// PageRequest is skipped; anything else required panics at startup so
-// schema changes can't silently send malformed requests.
-func registerMessageField(cmd *cobra.Command, fd protoreflect.FieldDescriptor, flagName string, required bool) fieldSetter {
- flags := cmd.Flags()
- markRequired := func() {
- if required {
- _ = cmd.MarkFlagRequired(flagName)
- }
- }
-
- switch fd.Message().FullName() {
- case "mirum.api.UserRef":
- flags.String(flagName, "", "user email")
- markRequired()
- return func(fs *pflag.FlagSet, m protoreflect.Message) error {
- v, _ := fs.GetString(flagName)
- if v == "" {
- return nil
- }
- ref := &apipb.UserRef{Ref: &apipb.UserRef_Email{Email: v}}
- m.Set(fd, protoreflect.ValueOfMessage(ref.ProtoReflect()))
- return nil
- }
-
- case "mirum.api.OrgRef":
- flags.String(flagName, "", "org slug")
- markRequired()
- return func(fs *pflag.FlagSet, m protoreflect.Message) error {
- v, _ := fs.GetString(flagName)
- if v == "" {
- return nil
- }
- ref := &apipb.OrgRef{Ref: &apipb.OrgRef_Slug{Slug: v}}
- m.Set(fd, protoreflect.ValueOfMessage(ref.ProtoReflect()))
- return nil
- }
-
- case "mirum.api.PageRequest":
- return nil
-
- case "mirum.api.Locale":
- langFlag := flagName + "-language"
- dfFlag := flagName + "-date-format"
- flags.String(langFlag, "", "locale language (e.g. en)")
- flags.String(dfFlag, "", "date format (DMY, MDY, YMD)")
- return func(fs *pflag.FlagSet, m protoreflect.Message) error {
- lang, _ := fs.GetString(langFlag)
- dfStr, _ := fs.GetString(dfFlag)
- if lang == "" && dfStr == "" {
- return nil
- }
- loc := &apipb.Locale{}
- if lang != "" {
- loc.Language = &lang
- }
- if dfStr != "" {
- want := strings.ToUpper(dfStr)
- enumVals := loc.ProtoReflect().Descriptor().Fields().ByName("date_format").Enum().Values()
- var n protoreflect.EnumNumber
- var found bool
- for i := 0; i < enumVals.Len(); i++ {
- ev := enumVals.Get(i)
- name := string(ev.Name())
- if idx := strings.LastIndexByte(name, '_'); idx >= 0 && strings.ToUpper(name[idx+1:]) == want {
- n = ev.Number()
- found = true
- break
- }
- }
- if !found {
- return fmt.Errorf("--%s: unknown value %q (valid: DMY, MDY, YMD)", dfFlag, dfStr)
- }
- df := apipb.DateFormat(n)
- loc.DateFormat = &df
- }
- m.Set(fd, protoreflect.ValueOfMessage(loc.ProtoReflect()))
- return nil
- }
- }
-
- if required {
- panic(fmt.Sprintf("consolecli: unhandled required message field %s (type=%s)", fd.FullName(), fd.Message().FullName()))
- }
- return nil
-}
-
-// Console schema uses bytes only for UUIDs (id / *_id) and ed25519 PKIX keys.
-func parseBytesFlag(fieldName, v string) ([]byte, error) {
- switch {
- case fieldName == "id" || strings.HasSuffix(fieldName, "_id"):
- id, err := ParseAnyID(v)
- if err != nil {
- return nil, fmt.Errorf("invalid id: %w", err)
- }
- return id[:], nil
- case strings.Contains(fieldName, "key"):
- der, err := base64.StdEncoding.DecodeString(v)
- if err != nil {
- return nil, fmt.Errorf("invalid base64: %w", err)
- }
- pub, err := x509.ParsePKIXPublicKey(der)
- if err != nil {
- return nil, fmt.Errorf("invalid public key: %w", err)
- }
- ed, ok := pub.(ed25519.PublicKey)
- if !ok {
- return nil, fmt.Errorf("not an ed25519 key")
- }
- return ed, nil
- }
- return nil, fmt.Errorf("unsupported bytes field %q", fieldName)
-}
-
-// Accepts both short ("admin") and full ("ROLE_ADMIN") forms.
-func parseEnumFlag(ed protoreflect.EnumDescriptor, v string) (protoreflect.EnumNumber, error) {
- want := strings.ToUpper(v)
- values := ed.Values()
- if ev := values.ByName(protoreflect.Name(want)); ev != nil {
- return ev.Number(), nil
- }
- prefix := strings.ToUpper(string(ed.Name())) + "_"
- if ev := values.ByName(protoreflect.Name(prefix + want)); ev != nil {
- return ev.Number(), nil
- }
- return 0, fmt.Errorf("unknown %s value %q", ed.Name(), v)
-}
-
-func buildRequest(desc protoreflect.MessageDescriptor, flags *pflag.FlagSet, setters []fieldSetter) (proto.Message, error) {
- mt, err := protoregistry.GlobalTypes.FindMessageByName(desc.FullName())
- if err != nil {
- return nil, fmt.Errorf("find message %s: %w", desc.FullName(), err)
- }
- m := mt.New()
- for _, set := range setters {
- if err := set(flags, m); err != nil {
- return nil, err
- }
- }
- return m.Interface(), nil
-}
-
-// reflect.New on *connect.Request[T] is equivalent to connect.NewRequest(req):
-// Msg is the only public field, the rest are initialised lazily at send time.
-func dispatchConsole(client apipbconnect.ConsoleClient, name string, req proto.Message) (proto.Message, error) {
- cv := reflect.ValueOf(client)
- method := cv.MethodByName(name)
- if !method.IsValid() {
- return nil, fmt.Errorf("unknown console method %q", name)
- }
- // method signature:
- // func(context.Context, *connect.Request[T]) (*connect.Response[U], error)
- reqPtrType := method.Type().In(1) // *connect.Request[T]
- reqWrap := reflect.New(reqPtrType.Elem())
- reqWrap.Elem().FieldByName("Msg").Set(reflect.ValueOf(req))
-
- out := method.Call([]reflect.Value{
- reflect.ValueOf(context.Background()),
- reqWrap,
- })
- if errV := out[1]; !errV.IsNil() {
- return nil, errV.Interface().(error)
- }
- return out[0].Elem().FieldByName("Msg").Interface().(proto.Message), nil
-}
-
-// Shape-driven printer: empty → "ok", single bytes id → UUID, single message
-// → TSV of scalars, repeated → one TSV line per element, anything else → TSV
-// of top-level scalars. PageResponse metadata is ignored.
-func printResponse(resp proto.Message) {
- m := resp.ProtoReflect()
- fields := m.Descriptor().Fields()
-
- var meaningful []protoreflect.FieldDescriptor
- for i := 0; i < fields.Len(); i++ {
- f := fields.Get(i)
- if f.Kind() == protoreflect.MessageKind && f.Message().FullName() == "mirum.api.PageResponse" {
- continue
- }
- meaningful = append(meaningful, f)
- }
-
- if len(meaningful) == 0 {
- fmt.Println("ok")
- return
- }
- if len(meaningful) == 1 {
- f := meaningful[0]
- v := m.Get(f)
- switch {
- case f.IsList():
- list := v.List()
- for j := 0; j < list.Len(); j++ {
- item := list.Get(j)
- if f.Kind() == protoreflect.MessageKind {
- fmt.Println(formatMessageTSV(item.Message()))
- } else {
- fmt.Println(formatScalar(f, item))
- }
- }
- case f.Kind() == protoreflect.MessageKind:
- fmt.Println(formatMessageTSV(v.Message()))
- default:
- fmt.Println(formatScalar(f, v))
- }
- return
- }
- fmt.Println(formatMessageTSV(m))
-}
-
-func formatMessageTSV(m protoreflect.Message) string {
- var parts []string
- fields := m.Descriptor().Fields()
- for i := 0; i < fields.Len(); i++ {
- f := fields.Get(i)
- if f.IsList() || f.IsMap() {
- continue
- }
- if f.HasOptionalKeyword() && !m.Has(f) {
- continue
- }
- if f.Kind() == protoreflect.MessageKind {
- if f.Message().FullName() == "google.protobuf.Timestamp" {
- ts := m.Get(f).Message().Interface().(*timestamppb.Timestamp)
- parts = append(parts, ts.AsTime().Format("2006-01-02"))
- continue
- }
- nested := m.Get(f).Message()
- if nested.IsValid() {
- parts = append(parts, formatMessageTSV(nested))
- }
- continue
- }
- parts = append(parts, formatScalar(f, m.Get(f)))
- }
- return strings.Join(parts, "\t")
-}
-
-func formatScalar(fd protoreflect.FieldDescriptor, v protoreflect.Value) string {
- switch fd.Kind() {
- case protoreflect.StringKind:
- return v.String()
- case protoreflect.BoolKind:
- if v.Bool() {
- return "true"
- }
- return "false"
- case protoreflect.BytesKind:
- return formatBytes(v.Bytes())
- case protoreflect.EnumKind:
- ev := fd.Enum().Values().ByNumber(v.Enum())
- if ev == nil {
- return fmt.Sprintf("%d", v.Enum())
- }
- name := string(ev.Name())
- if idx := strings.IndexByte(name, '_'); idx >= 0 {
- name = name[idx+1:]
- }
- return strings.ToLower(name)
- }
- return v.String()
-}
-
-func formatBytes(b []byte) string {
- if len(b) == 16 {
- return FormatAnyID(b)
- }
- return base64.StdEncoding.EncodeToString(b)
-}