feat(go): add galaxy-browse CLI subcommand (§4.6)
This commit is contained in:
@@ -121,6 +121,8 @@ func runWithIO(ctx context.Context, args []string, stdout, stderr io.Writer) err
|
||||
return runGalaxyDiscover(ctx, args[1:], stdout, stderr)
|
||||
case "galaxy-watch":
|
||||
return runGalaxyWatch(ctx, args[1:], stdout, stderr)
|
||||
case "galaxy-browse":
|
||||
return runGalaxyBrowse(ctx, args[1:], stdout, stderr)
|
||||
case "ping":
|
||||
return runPing(ctx, args[1:], stdout, stderr)
|
||||
case "batch":
|
||||
@@ -1244,7 +1246,7 @@ type protojsonMessage interface {
|
||||
}
|
||||
|
||||
func writeUsage(writer io.Writer) {
|
||||
fmt.Fprintln(writer, "usage: mxgw-go <version|open-session|close-session|ping|register|add-item|advise|subscribe-bulk|unsubscribe-bulk|read-bulk|write-bulk|write2-bulk|write-secured-bulk|write-secured2-bulk|bench-read-bulk|write|stream-events|stream-alarms|acknowledge-alarm|smoke|galaxy-test-connection|galaxy-last-deploy|galaxy-discover|galaxy-watch|batch>")
|
||||
fmt.Fprintln(writer, "usage: mxgw-go <version|open-session|close-session|ping|register|add-item|advise|subscribe-bulk|unsubscribe-bulk|read-bulk|write-bulk|write2-bulk|write-secured-bulk|write-secured2-bulk|bench-read-bulk|write|stream-events|stream-alarms|acknowledge-alarm|smoke|galaxy-test-connection|galaxy-last-deploy|galaxy-discover|galaxy-watch|galaxy-browse|batch>")
|
||||
}
|
||||
|
||||
// batchEOR is the end-of-result sentinel emitted to stdout after every command
|
||||
@@ -1507,6 +1509,231 @@ func runGalaxyWatch(ctx context.Context, args []string, stdout, stderr io.Writer
|
||||
}
|
||||
}
|
||||
|
||||
// runGalaxyBrowse drives the lazy-browse Galaxy helper from the CLI. Without
|
||||
// -parent it walks the root objects via GalaxyClient.Browse and eagerly expands
|
||||
// -depth further levels (each level reuses the same BrowseChildrenOptions, like
|
||||
// the library helper). With -parent it fetches exactly one level of children for
|
||||
// that gobject id via a parent-scoped BrowseChildren request; -depth is not
|
||||
// meaningful there and a warning is emitted if combined, mirroring the Rust CLI.
|
||||
//
|
||||
// Filter flags map onto BrowseChildrenOptions: -category-ids and
|
||||
// -template-contains are comma-separated lists (matching this CLI's other
|
||||
// list-valued flags), -tag-name-glob / -alarm-bearing-only / -historized-only
|
||||
// are scalar, and -include-attributes is a tri-state pointer (left nil unless
|
||||
// the flag is provided so the server default applies).
|
||||
func runGalaxyBrowse(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||
flags := flag.NewFlagSet("galaxy-browse", flag.ContinueOnError)
|
||||
flags.SetOutput(stderr)
|
||||
common := bindCommonFlags(flags)
|
||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
||||
parent := flags.Int("parent", -1, "parent gobject id whose children to browse; omit (or <0) for root objects")
|
||||
depth := flags.Int("depth", 0, "additional levels to eagerly expand beneath each root node; ignored with -parent")
|
||||
categoryIDs := flags.String("category-ids", "", "comma-separated Galaxy category ids to restrict results")
|
||||
templateContains := flags.String("template-contains", "", "comma-separated template tag names the chain must contain")
|
||||
tagNameGlob := flags.String("tag-name-glob", "", "restrict to objects whose tag name matches this glob")
|
||||
alarmBearingOnly := flags.Bool("alarm-bearing-only", false, "restrict to alarm-bearing objects")
|
||||
historizedOnly := flags.Bool("historized-only", false, "restrict to historized objects")
|
||||
includeAttributes := flags.Bool("include-attributes", false, "populate attributes on returned objects (overrides server default)")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
categoryList, err := parseInt32List(*categoryIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := &mxgateway.BrowseChildrenOptions{
|
||||
CategoryIds: categoryList,
|
||||
TemplateChainContains: parseStringList(*templateContains),
|
||||
TagNameGlob: *tagNameGlob,
|
||||
AlarmBearingOnly: *alarmBearingOnly,
|
||||
HistorizedOnly: *historizedOnly,
|
||||
}
|
||||
// Only override the server default when the flag was actually set; the
|
||||
// pointer form mirrors the proto's optional field.
|
||||
flags.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "include-attributes" {
|
||||
value := *includeAttributes
|
||||
opts.IncludeAttributes = &value
|
||||
}
|
||||
})
|
||||
|
||||
client, options, err := dialGalaxyForCommand(ctx, common)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// A specific parent → one level of children via the raw parent-scoped RPC.
|
||||
if *parent >= 0 {
|
||||
if *depth > 0 {
|
||||
fmt.Fprintln(stderr, "warning: -depth is ignored when -parent is specified")
|
||||
}
|
||||
return runGalaxyBrowseParent(ctx, client, int32(*parent), opts, stdout, *jsonOutput, options)
|
||||
}
|
||||
|
||||
// No parent → walk the lazy-browse tree from the root objects, eagerly
|
||||
// expanding -depth further levels so the print walks cached children
|
||||
// without re-issuing RPCs.
|
||||
nodes, err := client.Browse(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if err := expandToDepth(ctx, node, *depth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if *jsonOutput {
|
||||
jsonNodes := make([]map[string]any, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
jsonNodes = append(jsonNodes, lazyNodeToJSON(node))
|
||||
}
|
||||
return writeJSON(stdout, map[string]any{
|
||||
"command": "galaxy-browse",
|
||||
"options": options,
|
||||
"nodes": jsonNodes,
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Fprintln(stdout, len(nodes))
|
||||
for _, node := range nodes {
|
||||
printLazyNode(stdout, node, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runGalaxyBrowseParent fetches exactly one level of children for parentID via a
|
||||
// parent-scoped BrowseChildren request, paging until the server stops. It does
|
||||
// not lazily wrap the children in nodes; the single level is rendered directly.
|
||||
func runGalaxyBrowseParent(
|
||||
ctx context.Context,
|
||||
client *mxgateway.GalaxyClient,
|
||||
parentID int32,
|
||||
opts *mxgateway.BrowseChildrenOptions,
|
||||
stdout io.Writer,
|
||||
jsonOutput bool,
|
||||
options commonOptions,
|
||||
) error {
|
||||
var children []*mxgateway.GalaxyObject
|
||||
pageToken := ""
|
||||
seen := map[string]struct{}{}
|
||||
for {
|
||||
req := &mxgateway.BrowseChildrenRequest{
|
||||
PageSize: browseChildrenCLIPageSize,
|
||||
PageToken: pageToken,
|
||||
CategoryIds: opts.CategoryIds,
|
||||
TemplateChainContains: opts.TemplateChainContains,
|
||||
TagNameGlob: opts.TagNameGlob,
|
||||
AlarmBearingOnly: opts.AlarmBearingOnly,
|
||||
HistorizedOnly: opts.HistorizedOnly,
|
||||
IncludeAttributes: opts.IncludeAttributes,
|
||||
Parent: &mxgateway.BrowseChildrenRequest_ParentGobjectId{ParentGobjectId: parentID},
|
||||
}
|
||||
reply, err := client.BrowseChildrenRaw(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
children = append(children, reply.GetChildren()...)
|
||||
pageToken = reply.GetNextPageToken()
|
||||
if pageToken == "" {
|
||||
break
|
||||
}
|
||||
if _, dup := seen[pageToken]; dup {
|
||||
return fmt.Errorf("galaxy browse children returned repeated page token %q", pageToken)
|
||||
}
|
||||
seen[pageToken] = struct{}{}
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
jsonChildren := make([]map[string]any, 0, len(children))
|
||||
for _, child := range children {
|
||||
jsonChildren = append(jsonChildren, galaxyObjectToJSON(child))
|
||||
}
|
||||
return writeJSON(stdout, map[string]any{
|
||||
"command": "galaxy-browse",
|
||||
"options": options,
|
||||
"parentId": parentID,
|
||||
"children": jsonChildren,
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Fprintln(stdout, len(children))
|
||||
for _, child := range children {
|
||||
fmt.Fprintf(stdout, "%d\t%s\t%s\t(attrs=%d)\n", child.GetGobjectId(), child.GetTagName(), child.GetBrowseName(), len(child.GetAttributes()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// browseChildrenCLIPageSize is the per-request page size for the -parent
|
||||
// single-level walk. It mirrors the library's browseChildrenPageSize so the
|
||||
// CLI and the lazy-browse helper page identically.
|
||||
const browseChildrenCLIPageSize = 500
|
||||
|
||||
// expandToDepth eagerly expands node and remaining further levels beneath it so
|
||||
// a subsequent print walk reads cached children without re-issuing RPCs. A
|
||||
// remaining of 0 leaves the node unexpanded (only the requested level prints).
|
||||
func expandToDepth(ctx context.Context, node *mxgateway.LazyBrowseNode, remaining int) error {
|
||||
if remaining <= 0 {
|
||||
return nil
|
||||
}
|
||||
if err := node.Expand(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, child := range node.Children() {
|
||||
if err := expandToDepth(ctx, child, remaining-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// printLazyNode renders one node and its already-expanded children as an
|
||||
// indent-per-level tree. Only children loaded by a prior Expand are walked.
|
||||
func printLazyNode(stdout io.Writer, node *mxgateway.LazyBrowseNode, level int) {
|
||||
indent := strings.Repeat(" ", level)
|
||||
obj := node.Object()
|
||||
fmt.Fprintf(stdout, "%s%d\t%s\t%s\t(attrs=%d, hasChildren=%t)\n",
|
||||
indent, obj.GetGobjectId(), obj.GetTagName(), obj.GetBrowseName(), len(obj.GetAttributes()), node.HasChildrenHint())
|
||||
for _, child := range node.Children() {
|
||||
printLazyNode(stdout, child, level+1)
|
||||
}
|
||||
}
|
||||
|
||||
// lazyNodeToJSON renders one lazy node and its already-expanded children as a
|
||||
// nested JSON object.
|
||||
func lazyNodeToJSON(node *mxgateway.LazyBrowseNode) map[string]any {
|
||||
out := galaxyObjectToJSON(node.Object())
|
||||
out["hasChildren"] = node.HasChildrenHint()
|
||||
children := node.Children()
|
||||
jsonChildren := make([]map[string]any, 0, len(children))
|
||||
for _, child := range children {
|
||||
jsonChildren = append(jsonChildren, lazyNodeToJSON(child))
|
||||
}
|
||||
out["children"] = jsonChildren
|
||||
return out
|
||||
}
|
||||
|
||||
// galaxyObjectToJSON renders the scalar fields of a GalaxyObject for the
|
||||
// browse JSON output. Attributes are summarised by count to keep the tree
|
||||
// compact; -include-attributes still drives whether the server populates them.
|
||||
func galaxyObjectToJSON(obj *mxgateway.GalaxyObject) map[string]any {
|
||||
return map[string]any{
|
||||
"gobjectId": obj.GetGobjectId(),
|
||||
"tagName": obj.GetTagName(),
|
||||
"containedName": obj.GetContainedName(),
|
||||
"browseName": obj.GetBrowseName(),
|
||||
"parentGobjectId": obj.GetParentGobjectId(),
|
||||
"isArea": obj.GetIsArea(),
|
||||
"categoryId": obj.GetCategoryId(),
|
||||
"templateChain": obj.GetTemplateChain(),
|
||||
"attributeCount": len(obj.GetAttributes()),
|
||||
}
|
||||
}
|
||||
|
||||
func formatDeployEvent(event *mxgateway.DeployEvent) string {
|
||||
observed := ""
|
||||
if ts := event.GetObservedAt(); ts != nil {
|
||||
|
||||
Reference in New Issue
Block a user