Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 75 additions & 3 deletions internal/sync/adapter/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,38 @@ type Node struct {
Encode func(any) ([]byte, error)
}

// PositionalIDReason marks the id of a resource that has no name of its own,
// such as the second unnamed agent of a file. The id is the position of the
// resource, so a selection by it is as fragile as an index.
const PositionalIDReason = "the id is the position of a resource without a name"

// MaxExactInteger is the largest integer that a float64 holds exactly. A
// number value is a float64, so a larger integer in a source stays read-only
// text instead of a rounded number.
const MaxExactInteger = int64(1) << 53

// ExactInteger reports whether a float64 holds an integer exactly.
func ExactInteger(integer int64) bool {
return integer >= -MaxExactInteger && integer <= MaxExactInteger
}

// PositionalID returns an id node that marks a positional id.
func PositionalID(id string) *Node {
return &Node{Value: id, Capability: CapabilityReadOnly, Reason: PositionalIDReason}
}

// IsPositionalID reports whether a resource has a positional id.
func IsPositionalID(resource *Node) bool {
if resource == nil || resource.Fields == nil {
return false
}
id := resource.Fields["id"]
if id == nil {
return false
}
return id.Reason == PositionalIDReason
}

// String returns a writable string leaf. encode writes a new string in the
// syntax of the source.
func String(value string, start, end int, encode func(string) ([]byte, error)) *Node {
Expand Down Expand Up @@ -95,12 +127,38 @@ func Collections(collections map[string][]*Node) *Node {
}

// Resource returns one logical resource with its id field set, so that a
// keyed selector such as agents[id="support"] can find it.
// keyed selector such as agents[id="support"] can find it. A resource whose
// definition is not a literal object has only the id.
func Resource(id string, fields map[string]*Node) *Node {
if fields == nil {
fields = make(map[string]*Node, 1)
}
fields["id"] = &Node{Value: id}
return &Node{Fields: fields}
}

// Clone returns a deep copy of the node tree, so that a change to one use of
// a constant does not change another. The encoders are shared.
func (node *Node) Clone() *Node {
if node == nil {
return nil
}
copied := *node
if node.Fields != nil {
copied.Fields = make(map[string]*Node, len(node.Fields))
for key, child := range node.Fields {
copied.Fields[key] = child.Clone()
}
}
if node.Items != nil {
copied.Items = make([]*Node, len(node.Items))
for index, child := range node.Items {
copied.Items[index] = child.Clone()
}
}
return &copied
}

// IsCollection reports whether a path can index the node.
func (node *Node) IsCollection() bool {
if node == nil {
Expand Down Expand Up @@ -182,10 +240,24 @@ func JSONEncoder(raw []byte) func(any) ([]byte, error) {
return func(next any) ([]byte, error) {
unit, prefix, multiline := jsonIndentation(raw)
if !multiline {
return json.Marshal(next)
return EncodeJSON(next, "", "")
}
return json.MarshalIndent(next, prefix, unit)
return EncodeJSON(next, prefix, unit)
}
}

// EncodeJSON writes a value as JSON without the HTML escapes of the standard
// encoder, so that <, >, and & in prompt text stay as they are. A non-empty
// indent writes the value over several lines.
func EncodeJSON(value any, prefix, indent string) ([]byte, error) {
var buffer bytes.Buffer
encoder := json.NewEncoder(&buffer)
encoder.SetEscapeHTML(false)
encoder.SetIndent(prefix, indent)
if err := encoder.Encode(value); err != nil {
return nil, err
}
return bytes.TrimRight(buffer.Bytes(), "\n"), nil
}

// jsonIndentation returns the indentation unit and the indentation of the
Expand Down
17 changes: 14 additions & 3 deletions internal/sync/adapter/reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package adapter
import (
"fmt"
"slices"
"strings"

syncdomain "github.com/launchdarkly/ldcli/internal/sync"
)
Expand Down Expand Up @@ -130,12 +131,22 @@ func fieldAllowed(kind syncdomain.Kind, field syncdomain.Field) bool {
}
}

// annotateProblem adds the resource and the file to the problem in an error.
// A wrapper such as `select "x": ...` names the part of the reference that
// failed, and the output prints the problem alone, so the problem takes that
// context into its message.
func annotateProblem(err error, id syncdomain.ResourceID, file string) error {
if err == nil {
return nil
}
if problem, ok := syncdomain.ProblemFrom(err); ok {
_ = problem.WithResource(id).WithFile(file, 0, 0)
problem, ok := syncdomain.ProblemFrom(err)
if !ok {
return err
}
return err
if err != error(problem) {
if prefix, found := strings.CutSuffix(err.Error(), ": "+problem.Error()); found && prefix != "" {
problem.Message = prefix + ": " + problem.Message
}
}
return problem.WithResource(id).WithFile(file, 0, 0)
}
Loading
Loading