• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nautilus / gateway / 24917129850

24 Apr 2026 11:52PM UTC coverage: 92.011% (+0.7%) from 91.269%
24917129850

Pull #227

github

JohnStarich
Rename NonAuthoritative to Not... for clarity
Pull Request #227: Fix "node" field to stop returning non-null when it fails to resolve in remote schemas

333 of 361 new or added lines in 7 files covered. (92.24%)

7 existing lines in 3 files now uncovered.

2580 of 2804 relevant lines covered (92.01%)

367.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.38
/execute.go
1
package gateway
2

3
import (
4
        "context"
5
        "errors"
6
        "fmt"
7
        "strconv"
8
        "strings"
9
        "sync"
10

11
        "github.com/nautilus/gateway/internal/execresult"
12
        "github.com/nautilus/graphql"
13
        "github.com/vektah/gqlparser/v2/ast"
14
)
15

16
// Common type names for manipulating schemas
17
const (
18
        typeNameQuery        = "Query"
19
        typeNameMutation     = "Mutation"
20
        typeNameSubscription = "Subscription"
21
)
22

23
// Executor is responsible for executing a query plan against the remote
24
// schemas and returning the result
25
type Executor interface {
26
        Execute(ctx *ExecutionContext) (map[string]interface{}, error)
27
}
28

29
// ParallelExecutor executes the given query plan by starting at the root of the plan and
30
// walking down the path stitching the results together
31
type ParallelExecutor struct{}
32

33
type queryExecutionResult struct {
34
        InsertionPoint []string
35
        Result         *execresult.Object
36
        Err            error
37
}
38

39
// execution is broken up into two phases:
40
// - the first walks down the dependency graph execute the network request
41
// - the second strips the id fields from the response and  provides a
42
//   place for certain middlewares to fire
43

44
// ExecutionContext is a well-type alternative to context.Context and provides the context
45
// for a particular execution.
46
type ExecutionContext struct {
47
        logger             Logger
48
        Plan               *QueryPlan
49
        Variables          map[string]interface{}
50
        RequestContext     context.Context
51
        RequestMiddlewares []graphql.NetworkMiddleware
52
}
53

54
// Execute returns the result of the query plan
55
func (executor *ParallelExecutor) Execute(ctx *ExecutionContext) (map[string]interface{}, error) {
49✔
56
        // a place to store the result
49✔
57
        result := execresult.NewObject()
49✔
58

49✔
59
        // a channel to receive query results
49✔
60
        const maxResultBuffer = 10
49✔
61
        resultCh := make(chan *queryExecutionResult, maxResultBuffer)
49✔
62
        defer close(resultCh)
49✔
63

49✔
64
        // a wait group so we know when we're done with all of the steps
49✔
65
        stepWg := &sync.WaitGroup{}
49✔
66

49✔
67
        // and a channel for errors
49✔
68
        errMutex := &sync.Mutex{}
49✔
69
        errCh := make(chan error, maxResultBuffer)
49✔
70
        defer close(errCh)
49✔
71

49✔
72
        // a channel to close the goroutine
49✔
73
        closeCh := make(chan bool)
49✔
74
        defer close(closeCh)
49✔
75

49✔
76
        // if there are no steps after the root step, there is a problem
49✔
77
        if len(ctx.Plan.RootStep.Then) == 0 {
49✔
78
                return nil, errors.New("was given empty plan")
×
79
        }
×
80

81
        // the root step could have multiple steps that have to happen
82
        for _, step := range ctx.Plan.RootStep.Then {
99✔
83
                stepWg.Add(1)
50✔
84
                go executeStep(ctx, ctx.Plan, step, []string{}, ctx.Variables, resultCh, stepWg)
50✔
85
        }
50✔
86

87
        // the list of errors we have encountered while executing the plan
88
        errs := graphql.ErrorList{}
49✔
89

49✔
90
        // start a goroutine to add results to the list
49✔
91
        go func() {
98✔
92
                for {
200✔
93
                        select {
151✔
94
                        // we have a new result
95
                        case payload, ok := <-resultCh:
92✔
96
                                if !ok {
92✔
97
                                        return
×
98
                                }
×
99
                                ctx.logger.Debug("Inserting result into ", payload.InsertionPoint)
92✔
100
                                ctx.logger.Debug("Result: ", payload.Result)
92✔
101

92✔
102
                                // we have to grab the value in the result and write it to the appropriate spot in the
92✔
103
                                // acumulator.
92✔
104
                                insertErr := executorInsertObject(ctx, result, payload.InsertionPoint, payload.Result)
92✔
105

92✔
106
                                switch {
92✔
107
                                case payload.Err != nil: // response errors are the highest priority to return
10✔
108
                                        errCh <- payload.Err
10✔
109
                                case insertErr != nil:
×
110
                                        errCh <- insertErr
×
111
                                default:
82✔
112
                                        ctx.logger.Debug("Done. ", result)
82✔
113
                                        // one of the queries is done
82✔
114
                                        stepWg.Done()
82✔
115
                                }
116
                        case err := <-errCh:
10✔
117
                                if err != nil {
20✔
118
                                        errMutex.Lock()
10✔
119
                                        // if the error was a list
10✔
120
                                        var errList graphql.ErrorList
10✔
121
                                        if errors.As(err, &errList) {
15✔
122
                                                errs = append(errs, errList...)
5✔
123
                                        } else {
10✔
124
                                                ctx.logger.Warn("Unexpected error type executing query plan step: ", err)
5✔
125
                                                errs = append(errs, err)
5✔
126
                                        }
5✔
127
                                        errMutex.Unlock()
10✔
128
                                        stepWg.Done()
10✔
129
                                }
130
                        // we're done
131
                        case <-closeCh:
49✔
132
                                return
49✔
133
                        }
134
                }
135
        }()
136

137
        // when the wait group is finished
138
        stepWg.Wait()
49✔
139

49✔
140
        // if we encountered any errors
49✔
141
        errMutex.Lock()
49✔
142
        nErrs := len(errs)
49✔
143
        defer errMutex.Unlock()
49✔
144

49✔
145
        if nErrs > 0 {
58✔
146
                return result.ToMap(), errs
9✔
147
        }
9✔
148

149
        // we didn't encounter any errors
150
        return result.ToMap(), nil
40✔
151
}
152

153
// TODO: ugh... so... many... variables...
154
func executeStep(
155
        ctx *ExecutionContext,
156
        plan *QueryPlan,
157
        step *QueryPlanStep,
158
        insertionPoint []string,
159
        queryVariables map[string]interface{},
160
        resultCh chan *queryExecutionResult,
161
        stepWg *sync.WaitGroup,
162
) {
92✔
163
        queryResult, dependentSteps, queryErr := executeOneStep(ctx, plan, step, insertionPoint, queryVariables)
92✔
164
        // before publishing the current result, tell the wait-group about the dependent steps to wait for
92✔
165
        stepWg.Add(len(dependentSteps))
92✔
166
        ctx.logger.Debug("Pushing Result. Insertion point: ", insertionPoint, ". Value: ", queryResult)
92✔
167
        // send the result to be stitched in with our accumulator
92✔
168
        resultCh <- &queryExecutionResult{
92✔
169
                InsertionPoint: insertionPoint,
92✔
170
                Result:         queryResult,
92✔
171
                Err:            queryErr,
92✔
172
        }
92✔
173
        // We need to collect all the dependent steps and execute them after emitting the parent result in this function.
92✔
174
        // This avoids a race condition, where the result of a dependent request is published to the
92✔
175
        // result channel even before the result created in this iteration.
92✔
176
        // Execute dependent steps after the main step has been published.
92✔
177
        for _, sr := range dependentSteps {
134✔
178
                ctx.logger.Info("Spawn ", sr.insertionPoint)
42✔
179
                go executeStep(ctx, plan, sr.step, sr.insertionPoint, queryVariables, resultCh, stepWg)
42✔
180
        }
42✔
181
}
182

183
type dependentStepArgs struct {
184
        step           *QueryPlanStep
185
        insertionPoint []string
186
}
187

188
func executeOneStep(
189
        ctx *ExecutionContext,
190
        plan *QueryPlan,
191
        step *QueryPlanStep,
192
        insertionPoint []string,
193
        queryVariables map[string]interface{},
194
) (*execresult.Object, []dependentStepArgs, error) {
92✔
195
        ctx.logger.Debug("Executing step to be inserted in ", step.ParentType, ". Insertion point: ", insertionPoint)
92✔
196

92✔
197
        ctx.logger.Debug(step.SelectionSet)
92✔
198

92✔
199
        // log the query
92✔
200
        ctx.logger.QueryPlanStep(step)
92✔
201

92✔
202
        // the list of variables and their definitions that pertain to this query
92✔
203
        variables := map[string]interface{}{}
92✔
204

92✔
205
        // we need to grab the variable definitions and values for each variable in the step
92✔
206
        for variable := range step.Variables {
101✔
207
                // and the value if it exists
9✔
208
                if value, ok := queryVariables[variable]; ok {
17✔
209
                        variables[variable] = value
8✔
210
                }
8✔
211
        }
212

213
        // the id of the object we are query is defined by the last step in the realized insertion point
214
        if len(insertionPoint) > 0 {
133✔
215
                head := insertionPoint[max(len(insertionPoint)-1, 0)]
41✔
216

41✔
217
                // get the data of the point
41✔
218
                pointData, err := executorGetPointData(head)
41✔
219
                if err != nil {
41✔
220
                        return nil, nil, err
×
221
                }
×
222

223
                // if we dont have an id
224
                if pointData.ID == "" {
41✔
225
                        return nil, nil, fmt.Errorf("could not find id in path")
×
226
                }
×
227

228
                // save the id as a variable to the query
229
                variables["id"] = pointData.ID
41✔
230
        }
231

232
        // if there is no queryer
233
        if step.Queryer == nil {
92✔
234
                return nil, nil, errors.New(" could not find queryer for step")
×
235
        }
×
236

237
        queryer := step.Queryer
92✔
238

92✔
239
        // if we have middlewares
92✔
240
        if len(ctx.RequestMiddlewares) > 0 {
93✔
241
                // if the queryer is a network queryer
1✔
242
                if nQueryer, ok := queryer.(graphql.QueryerWithMiddlewares); ok {
2✔
243
                        queryer = nQueryer.WithMiddlewares(ctx.RequestMiddlewares)
1✔
244
                }
1✔
245
        }
246

247
        operationName := ""
92✔
248
        if plan != nil && plan.Operation != nil {
135✔
249
                operationName = plan.Operation.Name
43✔
250
        }
43✔
251

252
        var queryResult *execresult.Object
92✔
253
        var queryErr error
92✔
254
        { // fire the query
184✔
255
                var queryResultMap map[string]any
92✔
256
                queryErr = queryer.Query(ctx.RequestContext, &graphql.QueryInput{
92✔
257
                        Query:         step.QueryString,
92✔
258
                        QueryDocument: step.QueryDocument,
92✔
259
                        Variables:     variables,
92✔
260
                        OperationName: operationName,
92✔
261
                }, &queryResultMap)
92✔
262
                queryResult = execresult.NewObjectFromMap(queryResultMap)
92✔
263
        }
92✔
264

265
        // NOTE: this insertion point could point to a list of values. If it did, we have to have
266
        //       passed it to the this invocation of this function. It is safe to trust this
267
        //       InsertionPoint as the right place to insert this result.
268

269
        // if this is a query that falls underneath a `node(id: ???)` query then we only want to consider the object
270
        // underneath the `node` field as the result for the query
271
        stripNode := step.ParentType != typeNameQuery && step.ParentType != typeNameSubscription && step.ParentType != typeNameMutation
92✔
272
        if stripNode {
133✔
273
                ctx.logger.Debug("Should strip node")
41✔
274
                // get the result from the response that we have to stitch there
41✔
275
                extractedResult, err := executorExtractValue(ctx, queryResult, []string{"node"})
41✔
276
                if err != nil {
41✔
277
                        return nil, nil, err
×
278
                }
×
279
                queryResult = extractedResult
41✔
280
        }
281

282
        // if there are next steps
283
        var dependentSteps []dependentStepArgs
92✔
284
        if len(step.Then) > 0 {
120✔
285
                ctx.logger.Debug("Kicking off child queries")
28✔
286
                // we need to find the ids of the objects we are inserting into and then kick of the worker with the right
28✔
287
                // insertion point. For lists, insertion points look like: ["user", "friends:0", "catPhotos:0", "owner"]
28✔
288
                for _, dependent := range step.Then {
60✔
289
                        copiedInsertionPoint := make([]string, len(insertionPoint))
32✔
290
                        copy(copiedInsertionPoint, insertionPoint)
32✔
291
                        insertPoints, missingIDPoints, err := executorFindInsertionPoints(ctx, dependent.InsertionPoint, step.SelectionSet, queryResult, [][]string{copiedInsertionPoint}, step.FragmentDefinitions)
32✔
292
                        if err != nil {
32✔
293
                                return nil, nil, err
×
294
                        }
×
295
                        if len(missingIDPoints) > 0 {
32✔
NEW
296
                                return nil, nil, fmt.Errorf("could not find IDs for insertion points: %v", missingIDPoints)
×
NEW
297
                        }
×
298

299
                        // this dependent needs to fire for every object that the insertion point references
300
                        for _, insertionPoint := range insertPoints {
74✔
301
                                dependentSteps = append(dependentSteps, dependentStepArgs{
42✔
302
                                        step:           dependent,
42✔
303
                                        insertionPoint: insertionPoint,
42✔
304
                                })
42✔
305
                        }
42✔
306
                }
307
        }
308
        return queryResult, dependentSteps, queryErr
92✔
309
}
310

311
func findSelection(matchString string, selectionSet ast.SelectionSet, fragmentDefs ast.FragmentDefinitionList) (*ast.Field, error) {
73✔
312
        selectionSetFragments, err := graphql.ApplyFragments(selectionSet, fragmentDefs)
73✔
313
        if err != nil {
73✔
314
                return nil, err
×
315
        }
×
316

317
        for _, selection := range selectionSetFragments {
157✔
318
                selection, ok := selection.(*ast.Field)
84✔
319
                if ok && (selection.Alias == matchString || selection.Name == matchString) {
155✔
320
                        return selection, nil
71✔
321
                }
71✔
322
        }
323

324
        return nil, nil
2✔
325
}
326

327
// executorFindInsertionPoints returns the list of insertion points where this step should be executed.
328
func executorFindInsertionPoints(ctx *ExecutionContext, targetPoints []string, selectionSet ast.SelectionSet, result *execresult.Object, startingPoints [][]string, fragmentDefs ast.FragmentDefinitionList) (insertionPoints [][]string, missingIDPoints [][]string, err error) {
126✔
329
        ctx.logger.Debug("Looking for insertion points. target: ", targetPoints, " Starting from ", startingPoints)
126✔
330
        startingIndex := 0
126✔
331
        if len(startingPoints) > 0 {
249✔
332
                startingIndex = len(startingPoints[0])
123✔
333

123✔
334
                if len(targetPoints) == len(startingPoints[0]) {
176✔
335
                        return startingPoints, missingIDPoints, nil
53✔
336
                }
53✔
337
        }
338

339
        ctx.logger.Debug("traversing path point: ", targetPoints[startingIndex])
73✔
340

73✔
341
        // if our starting point is []string{"users:0"} then we know everything so far
73✔
342
        // is along the path of the steps insertion point
73✔
343
        point := targetPoints[startingIndex]
73✔
344
        isLastPoint := startingIndex == len(targetPoints)-1
73✔
345

73✔
346
        // find the selection node in the AST corresponding to the point
73✔
347
        foundSelection, err := findSelection(point, selectionSet, fragmentDefs)
73✔
348
        if err != nil {
73✔
NEW
349
                ctx.logger.Debug("Error looking for selection")
×
NEW
350
                return nil, nil, err
×
NEW
351
        }
×
352

353
        // if we didn't find a selection
354
        if foundSelection == nil {
75✔
355
                ctx.logger.Debug("No selection")
2✔
356
                return nil, missingIDPoints, nil
2✔
357
        }
2✔
358

359
        ctx.logger.Debug("Found Selection for: ", point)
71✔
360
        ctx.logger.Debug("Result Chunk: ", result)
71✔
361
        // make sure we are looking at the top of the selection set next time
71✔
362
        selectionSet = foundSelection.SelectionSet
71✔
363

71✔
364
        pointValue, ok := result.Get(point)
71✔
365
        if !ok {
72✔
366
                return nil, missingIDPoints, nil
1✔
367
        }
1✔
368

369
        // get the type of the object in question
370
        selectionType := foundSelection.Definition.Type
70✔
371

70✔
372
        if pointValue == nil {
70✔
NEW
373
                if selectionType.NonNull {
×
NEW
374
                        err := fmt.Errorf("received null for required field: %v", foundSelection.Name)
×
NEW
375
                        ctx.logger.Warn(err)
×
NEW
376
                        return nil, nil, err
×
NEW
377
                }
×
NEW
378
                return nil, missingIDPoints, nil
×
379
        }
380

381
        if selectionType.Elem != nil {
99✔
382
                ctx.logger.Debug("Selection should be a list")
29✔
383
                list, ok := pointValue.(*execresult.List)
29✔
384
                if !ok {
29✔
NEW
385
                        return nil, nil, fmt.Errorf("point value should be list, but was not: %v", pointValue)
×
UNCOV
386
                }
×
387

388
                // build up a new list of insertion points
389
                var newInsertionPoints [][]string
29✔
390

29✔
391
                // each value in the result contributes an insertion point
29✔
392
                for entryI, iEntry := range list.All() {
76✔
393
                        resultEntry, ok := iEntry.(*execresult.Object)
47✔
394
                        if !ok {
47✔
NEW
395
                                return nil, nil, errors.New("entry in result wasn't an object")
×
396
                        }
×
397

398
                        // the point we are going to add to the list
399
                        entryPoint := fmt.Sprintf("%s:%v", foundSelection.Name, entryI)
47✔
400
                        if foundSelection.Alias != "" {
61✔
401
                                entryPoint = fmt.Sprintf("%s:%v", foundSelection.Alias, entryI)
14✔
402
                        }
14✔
403
                        ctx.logger.Debug("Adding ", entryPoint, " to list")
47✔
404

47✔
405
                        var newBranchSet [][]string
47✔
406
                        for _, c := range startingPoints {
93✔
407
                                newBranchSet = append(newBranchSet, copyStrings(c))
46✔
408
                        }
46✔
409

410
                        // if we are adding to an existing branch
411
                        if len(newBranchSet) > 0 {
93✔
412
                                notFoundIndices := make(map[int]struct{})
46✔
413
                                // add the path to the end of this for the entry we just added
46✔
414
                                for i, newBranch := range newBranchSet {
92✔
415
                                        branchEntryPoint := entryPoint // avoid mutating shared list entrypoint
46✔
416
                                        // if we are looking at the last thing in the insertion list
46✔
417
                                        if isLastPoint {
67✔
418
                                                // look for an id
21✔
419
                                                id, ok := resultEntry.Get("id")
21✔
420
                                                if !ok {
21✔
NEW
421
                                                        notFoundIndices[i] = struct{}{}
×
422
                                                } else {
21✔
423
                                                        // add the id to the entry so that the executor can use it to form its query
21✔
424
                                                        branchEntryPoint = fmt.Sprintf("%s#%v", branchEntryPoint, id)
21✔
425
                                                }
21✔
426
                                        }
427
                                        newBranchSet[i] = append(newBranch, branchEntryPoint)
46✔
428
                                }
429
                                var deletedBranchSet [][]string
46✔
430
                                newBranchSet, deletedBranchSet = deleteIndices(newBranchSet, notFoundIndices)
46✔
431
                                missingIDPoints = append(missingIDPoints, deletedBranchSet...)
46✔
432
                        } else {
1✔
433
                                newBranchSet = append(newBranchSet, []string{entryPoint})
1✔
434
                        }
1✔
435

436
                        // compute the insertion points for that entry
437
                        entryInsertionPoints, missingEntryIDPoints, err := executorFindInsertionPoints(ctx, targetPoints, selectionSet, resultEntry, newBranchSet, fragmentDefs)
47✔
438
                        if err != nil {
47✔
NEW
439
                                return nil, nil, err
×
NEW
440
                        }
×
441

442
                        // add the list of insertion points to the acumulator
443
                        newInsertionPoints = append(newInsertionPoints, entryInsertionPoints...)
47✔
444
                        missingIDPoints = append(missingIDPoints, missingEntryIDPoints...)
47✔
445
                }
446

447
                // return the flat list of insertion points created by our children
448
                return newInsertionPoints, missingIDPoints, nil
29✔
449
        }
450

451
        // traverse down the resultChunk for the next iteration
452
        if pointValueObj, ok := pointValue.(*execresult.Object); ok {
74✔
453
                result = pointValueObj
33✔
454
        }
33✔
455

456
        // we are encountering something that isn't a list so it must be an object or a scalar
457
        // regardless, we just need to add the point to the end of each list
458
        for i, points := range startingPoints {
82✔
459
                startingPoints[i] = append(points, point)
41✔
460
        }
41✔
461

462
        if isLastPoint {
73✔
463
                notFoundIndices := make(map[int]struct{})
32✔
464
                if list, ok := pointValue.(*execresult.List); ok {
40✔
465
                        for i := range startingPoints {
16✔
466
                                entry, ok := list.GetObjectAtIndex(i)
8✔
467
                                if !ok {
8✔
NEW
468
                                        return nil, nil, errors.New("item in list isn't an object")
×
469
                                }
×
470

471
                                // look up the id of the object
472
                                id, ok := entry.Get("id")
8✔
473
                                if !ok {
8✔
NEW
474
                                        notFoundIndices[i] = struct{}{}
×
NEW
475
                                }
×
476
                                startingPoints[i][startingIndex] = fmt.Sprintf("%s:%v#%v", startingPoints[i][startingIndex], i, id)
8✔
477
                        }
478
                } else {
24✔
479
                        obj, ok := pointValue.(*execresult.Object)
24✔
480
                        if !ok {
24✔
NEW
481
                                return nil, nil, fmt.Errorf("point value was not an object. Point: %v Value: %v", point, pointValue)
×
NEW
482
                        }
×
483
                        for i := range startingPoints {
48✔
484
                                // look up the id of the object
24✔
485
                                id, ok := obj.Get("id")
24✔
486
                                if !ok {
25✔
487
                                        notFoundIndices[i] = struct{}{}
1✔
488
                                }
1✔
489
                                startingPoints[i][startingIndex] = fmt.Sprintf("%s#%v", startingPoints[i][startingIndex], id)
24✔
490
                        }
491
                }
492
                var deletedStartingPoints [][]string
32✔
493
                startingPoints, deletedStartingPoints = deleteIndices(startingPoints, notFoundIndices)
32✔
494
                missingIDPoints = append(missingIDPoints, deletedStartingPoints...)
32✔
495
        }
496
        insertionPoints, missingSubIDPoints, err := executorFindInsertionPoints(ctx, targetPoints, selectionSet, result, startingPoints, fragmentDefs)
41✔
497
        return insertionPoints, append(missingIDPoints, missingSubIDPoints...), err
41✔
498
}
499

500
func isListElement(path string) bool {
150✔
501
        if hashLocation := strings.Index(path, "#"); hashLocation > 0 {
205✔
502
                path = path[:hashLocation]
55✔
503
        }
55✔
504
        return strings.Contains(path, ":")
150✔
505
}
506

507
func executorExtractValue(ctx *ExecutionContext, source *execresult.Object, path []string) (*execresult.Object, error) {
140✔
508
        // a pointer to the objects we are modifying
140✔
509
        recent := source
140✔
510
        ctx.logger.Debug("Pulling ", path, " from ", source)
140✔
511

140✔
512
        for _, point := range path {
290✔
513
                // if the point designates an element in the list
150✔
514
                if isListElement(point) {
217✔
515
                        pointData, err := executorGetPointData(point)
67✔
516
                        if err != nil {
67✔
517
                                return nil, err
×
518
                        }
×
519

520
                        list, ok := recent.EnsureList(pointData.Field)
67✔
521
                        if !ok {
67✔
NEW
522
                                value, _ := recent.Get(pointData.Field)
×
NEW
523
                                return nil, fmt.Errorf("unexpected type at list insertion point %q: %T %v", pointData.Field, value, value)
×
UNCOV
524
                        }
×
525
                        obj, ok := list.EnsureObjectAtIndex(pointData.Index)
67✔
526
                        if !ok {
67✔
NEW
527
                                value, _ := list.Get(pointData.Index)
×
NEW
528
                                return nil, fmt.Errorf("unexpected type at list item insertion point %q: %T %v", pointData.Field, value, value)
×
UNCOV
529
                        }
×
530
                        recent = obj
67✔
531
                } else {
83✔
532
                        // it's possible that there's an id
83✔
533
                        pointData, err := executorGetPointData(point)
83✔
534
                        if err != nil {
83✔
535
                                return nil, err
×
536
                        }
×
537
                        obj, ok := recent.EnsureObject(pointData.Field)
83✔
538
                        if !ok {
88✔
539
                                value, exists := recent.Get(pointData.Field)
5✔
540
                                if exists && value == nil { // 'recent' is a strong object and field is already present and set to 'null'
10✔
541
                                        weakObj := execresult.NewObject()
5✔
542
                                        weakObj.SetWeak()
5✔
543
                                        return weakObj, nil
5✔
544
                                }
5✔
NEW
545
                                return nil, fmt.Errorf("target is non-null but not an object: %v, %T %v", pointData.Field, value, value)
×
546
                        }
547
                        recent = obj
78✔
548
                }
549
        }
550

551
        return recent, nil
135✔
552
}
553

554
func executorInsertObject(ctx *ExecutionContext, target *execresult.Object, path []string, value *execresult.Object) error {
94✔
555
        obj, err := executorExtractValue(ctx, target, path)
94✔
556
        if err != nil {
94✔
NEW
557
                return err
×
UNCOV
558
        }
×
559
        obj.MergeOverrides(value)
94✔
560
        return nil
94✔
561
}
562

563
type extractorPointData struct {
564
        Field string
565
        Index int
566
        ID    string
567
}
568

569
func executorGetPointData(point string) (*extractorPointData, error) {
196✔
570
        field := point
196✔
571
        index := -1
196✔
572
        id := ""
196✔
573

196✔
574
        // points come in the form <field>:<index>#<id> and each of index or id is optional
196✔
575
        if strings.Contains(point, "#") {
296✔
576
                idData := strings.Split(point, "#")
100✔
577
                const longIDParts = 2
100✔
578
                if len(idData) == longIDParts {
200✔
579
                        id = idData[1]
100✔
580
                }
100✔
581

582
                // use the index data without the id
583
                field = idData[0]
100✔
584
        }
585

586
        if strings.Contains(field, ":") {
288✔
587
                indexData := strings.Split(field, ":")
92✔
588
                indexValue, err := strconv.Atoi(indexData[1])
92✔
589
                if err != nil {
92✔
590
                        return nil, err
×
591
                }
×
592

593
                index = indexValue
92✔
594
                field = indexData[0]
92✔
595
        }
596

597
        return &extractorPointData{
196✔
598
                Field: field,
196✔
599
                Index: index,
196✔
600
                ID:    id,
196✔
601
        }, nil
196✔
602
}
603

604
// ExecutorFunc wraps a function to be used as an executor.
605
type ExecutorFunc func(ctx *ExecutionContext) (map[string]interface{}, error)
606

607
// Execute invokes and returns the internal function
608
func (e ExecutorFunc) Execute(ctx *ExecutionContext) (map[string]interface{}, error) {
17✔
609
        return e(ctx)
17✔
610
}
17✔
611

612
// ErrExecutor always returnes the internal error.
613
type ErrExecutor struct {
614
        Error error
615
}
616

617
// Execute returns the internet error
618
func (e *ErrExecutor) Execute(_ *ExecutionContext) (map[string]interface{}, error) {
×
619
        return nil, e.Error
×
620
}
×
621

622
// MockExecutor always returns a success with the provided value
623
type MockExecutor struct {
624
        Value map[string]interface{}
625
}
626

627
// Execute returns the provided value
628
func (e *MockExecutor) Execute(_ *ExecutionContext) (map[string]interface{}, error) {
×
629
        return e.Value, nil
×
630
}
×
631

632
func copyStrings(s []string) []string {
802✔
633
        var result []string
802✔
634
        result = append(result, s...)
802✔
635
        return result
802✔
636
}
802✔
637

638
func deleteIndices[Value any](values []Value, indices map[int]struct{}) (newValues, deletedValues []Value) {
78✔
639
        for index, value := range values {
156✔
640
                if _, shouldDelete := indices[index]; shouldDelete {
79✔
641
                        deletedValues = append(deletedValues, value)
1✔
642
                } else {
78✔
643
                        newValues = append(newValues, value)
77✔
644
                }
77✔
645
        }
646
        return
78✔
647
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc