Skip to content

Commit f312915

Browse files
authored
Add OperationLocationResultPath to NewPollerOptions[T] (Azure#23733)
* Add OperationLocationResultPath to NewPollerOptions[T] The result path must be configurable instead of hard-coded to result. * update test
1 parent 3154354 commit f312915

File tree

5 files changed

+53
-24
lines changed

5 files changed

+53
-24
lines changed

sdk/azcore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
* Added field `OperationLocationResultPath` to `runtime.NewPollerOptions[T]` for LROs that use the `Operation-Location` pattern.
8+
79
### Breaking Changes
810

911
### Bugs Fixed

sdk/azcore/internal/pollers/op/op.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ type Poller[T any] struct {
4040
OrigURL string `json:"origURL"`
4141
Method string `json:"method"`
4242
FinalState pollers.FinalStateVia `json:"finalState"`
43+
ResultPath string `json:"resultPath"`
4344
CurState string `json:"state"`
4445
}
4546

4647
// New creates a new Poller from the provided initial response.
4748
// Pass nil for response to create an empty Poller for rehydration.
48-
func New[T any](pl exported.Pipeline, resp *http.Response, finalState pollers.FinalStateVia) (*Poller[T], error) {
49+
func New[T any](pl exported.Pipeline, resp *http.Response, finalState pollers.FinalStateVia, resultPath string) (*Poller[T], error) {
4950
if resp == nil {
5051
log.Write(log.EventLRO, "Resuming Operation-Location poller.")
5152
return &Poller[T]{pl: pl}, nil
@@ -82,6 +83,7 @@ func New[T any](pl exported.Pipeline, resp *http.Response, finalState pollers.Fi
8283
OrigURL: resp.Request.URL.String(),
8384
Method: resp.Request.Method,
8485
FinalState: finalState,
86+
ResultPath: resultPath,
8587
CurState: curState,
8688
}, nil
8789
}
@@ -116,10 +118,6 @@ func (p *Poller[T]) Result(ctx context.Context, out *T) error {
116118
var req *exported.Request
117119
var err error
118120

119-
// when the payload is included with the status monitor on
120-
// terminal success it's in the "result" JSON property
121-
payloadPath := "result"
122-
123121
if p.FinalState == pollers.FinalStateViaLocation && p.LocURL != "" {
124122
req, err = exported.NewRequest(ctx, http.MethodGet, p.LocURL)
125123
} else if rl, rlErr := poller.GetResourceLocation(p.resp); rlErr != nil && !errors.Is(rlErr, poller.ErrNoBody) {
@@ -138,13 +136,13 @@ func (p *Poller[T]) Result(ctx context.Context, out *T) error {
138136
// if a final GET request has been created, execute it
139137
if req != nil {
140138
// no JSON path when making a final GET request
141-
payloadPath = ""
139+
p.ResultPath = ""
142140
resp, err := p.pl.Do(req)
143141
if err != nil {
144142
return err
145143
}
146144
p.resp = resp
147145
}
148146

149-
return pollers.ResultHelper(p.resp, poller.Failed(p.CurState), payloadPath, out)
147+
return pollers.ResultHelper(p.resp, poller.Failed(p.CurState), p.ResultPath, out)
150148
}

sdk/azcore/internal/pollers/op/op_test.go

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,30 +57,30 @@ func TestCanResume(t *testing.T) {
5757
}
5858

5959
func TestNew(t *testing.T) {
60-
poller, err := New[struct{}](exported.Pipeline{}, nil, "")
60+
poller, err := New[struct{}](exported.Pipeline{}, nil, "", "")
6161
require.NoError(t, err)
6262
require.Empty(t, poller.CurState)
6363

64-
poller, err = New[struct{}](exported.Pipeline{}, &http.Response{Header: http.Header{}}, "")
64+
poller, err = New[struct{}](exported.Pipeline{}, &http.Response{Header: http.Header{}}, "", "")
6565
require.Error(t, err)
6666
require.Nil(t, poller)
6767

6868
resp := initialResponse(http.MethodPut, http.NoBody)
6969
resp.Header.Set(shared.HeaderOperationLocation, "this is an invalid polling URL")
70-
poller, err = New[struct{}](exported.Pipeline{}, resp, "")
70+
poller, err = New[struct{}](exported.Pipeline{}, resp, "", "")
7171
require.Error(t, err)
7272
require.Nil(t, poller)
7373

7474
resp = initialResponse(http.MethodPut, http.NoBody)
7575
resp.Header.Set(shared.HeaderOperationLocation, fakePollingURL)
7676
resp.Header.Set(shared.HeaderLocation, "this is an invalid polling URL")
77-
poller, err = New[struct{}](exported.Pipeline{}, resp, "")
77+
poller, err = New[struct{}](exported.Pipeline{}, resp, "", "")
7878
require.Error(t, err)
7979
require.Nil(t, poller)
8080

8181
resp = initialResponse(http.MethodPut, strings.NewReader(`{ "status": "Updating" }`))
8282
resp.Header.Set(shared.HeaderOperationLocation, fakePollingURL)
83-
poller, err = New[struct{}](exported.Pipeline{}, resp, "")
83+
poller, err = New[struct{}](exported.Pipeline{}, resp, "", "")
8484
require.NoError(t, err)
8585
require.Equal(t, "Updating", poller.CurState)
8686
require.False(t, poller.Done())
@@ -108,7 +108,7 @@ func TestFinalStateViaLocation(t *testing.T) {
108108
} else {
109109
return nil, fmt.Errorf("test bug, unhandled URL %s", surl)
110110
}
111-
})), resp, pollers.FinalStateViaLocation)
111+
})), resp, pollers.FinalStateViaLocation, "")
112112
require.NoError(t, err)
113113
require.False(t, poller.Done())
114114
resp, err = poller.Poll(context.Background())
@@ -129,7 +129,28 @@ func TestFinalStateViaOperationLocationWithPost(t *testing.T) {
129129
StatusCode: http.StatusOK,
130130
Body: io.NopCloser(strings.NewReader(`{ "status": "succeeded", "result": { "shape": "rhombus" } }`)),
131131
}, nil
132-
})), resp, pollers.FinalStateViaOpLocation)
132+
})), resp, pollers.FinalStateViaOpLocation, "result")
133+
require.NoError(t, err)
134+
require.False(t, poller.Done())
135+
resp, err = poller.Poll(context.Background())
136+
require.NoError(t, err)
137+
require.Equal(t, http.StatusOK, resp.StatusCode)
138+
require.True(t, poller.Done())
139+
var result widget
140+
err = poller.Result(context.Background(), &result)
141+
require.NoError(t, err)
142+
require.Equal(t, "rhombus", result.Shape)
143+
}
144+
145+
func TestFinalStateViaOperationLocationWithPostNoResultPath(t *testing.T) {
146+
resp := initialResponse(http.MethodPost, strings.NewReader(`{ "status": "Updating" }`))
147+
resp.Header.Set(shared.HeaderOperationLocation, fakePollingURL)
148+
poller, err := New[widget](exported.NewPipeline(shared.TransportFunc(func(req *http.Request) (*http.Response, error) {
149+
return &http.Response{
150+
StatusCode: http.StatusOK,
151+
Body: io.NopCloser(strings.NewReader(`{ "status": "succeeded", "shape": "rhombus" }`)),
152+
}, nil
153+
})), resp, pollers.FinalStateViaOpLocation, "")
133154
require.NoError(t, err)
134155
require.False(t, poller.Done())
135156
resp, err = poller.Poll(context.Background())
@@ -159,7 +180,7 @@ func TestFinalStateViaResourceLocation(t *testing.T) {
159180
} else {
160181
return nil, fmt.Errorf("test bug, unhandled URL %s", surl)
161182
}
162-
})), resp, pollers.FinalStateViaLocation)
183+
})), resp, pollers.FinalStateViaLocation, "")
163184
require.NoError(t, err)
164185
require.False(t, poller.Done())
165186
resp, err = poller.Poll(context.Background())
@@ -189,7 +210,7 @@ func TestResultForPatch(t *testing.T) {
189210
} else {
190211
return nil, fmt.Errorf("test bug, unhandled URL %s", surl)
191212
}
192-
})), resp, "")
213+
})), resp, "", "")
193214
require.NoError(t, err)
194215
require.False(t, poller.Done())
195216
resp, err = poller.Poll(context.Background())
@@ -220,7 +241,7 @@ func TestPostWithLocation(t *testing.T) {
220241
} else {
221242
return nil, fmt.Errorf("test bug, unhandled URL %s", surl)
222243
}
223-
})), resp, "")
244+
})), resp, "", "")
224245
require.NoError(t, err)
225246
require.False(t, poller.Done())
226247
resp, err = poller.Poll(context.Background())
@@ -241,7 +262,7 @@ func TestOperationFailed(t *testing.T) {
241262
StatusCode: http.StatusOK,
242263
Body: io.NopCloser(strings.NewReader(`{ "status": "Failed", "error": { "code": "InvalidSomething" } }`)),
243264
}, nil
244-
})), resp, pollers.FinalStateViaLocation)
265+
})), resp, pollers.FinalStateViaLocation, "")
245266
require.NoError(t, err)
246267
require.False(t, poller.Done())
247268
resp, err = poller.Poll(context.Background())
@@ -261,7 +282,7 @@ func TestPollFailed(t *testing.T) {
261282
resp.Header.Set(shared.HeaderOperationLocation, fakePollingURL)
262283
poller, err := New[widget](exported.NewPipeline(shared.TransportFunc(func(req *http.Request) (*http.Response, error) {
263284
return nil, errors.New("failed")
264-
})), resp, pollers.FinalStateViaLocation)
285+
})), resp, pollers.FinalStateViaLocation, "")
265286
require.NoError(t, err)
266287
require.False(t, poller.Done())
267288
resp, err = poller.Poll(context.Background())
@@ -279,7 +300,7 @@ func TestPollError(t *testing.T) {
279300
Header: http.Header{},
280301
Body: io.NopCloser(strings.NewReader(`{ "error": { "code": "NotFound", "message": "the item doesn't exist" } }`)),
281302
}, nil
282-
})), resp, pollers.FinalStateViaLocation)
303+
})), resp, pollers.FinalStateViaLocation, "")
283304
require.NoError(t, err)
284305
require.False(t, poller.Done())
285306
resp, err = poller.Poll(context.Background())
@@ -299,7 +320,7 @@ func TestMissingStatus(t *testing.T) {
299320
StatusCode: http.StatusOK,
300321
Body: io.NopCloser(strings.NewReader(`{ "shape": "square" }`)),
301322
}, nil
302-
})), resp, "")
323+
})), resp, "", "")
303324
require.NoError(t, err)
304325
require.False(t, poller.Done())
305326
resp, err = poller.Poll(context.Background())

sdk/azcore/runtime/poller.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,14 @@ const (
5050
// NewPollerOptions contains the optional parameters for NewPoller.
5151
type NewPollerOptions[T any] struct {
5252
// FinalStateVia contains the final-state-via value for the LRO.
53+
// NOTE: used only for Azure-AsyncOperation and Operation-Location LROs.
5354
FinalStateVia FinalStateVia
5455

56+
// OperationLocationResultPath contains the JSON path to the result's
57+
// payload when it's included with the terminal success response.
58+
// NOTE: only used for Operation-Location LROs.
59+
OperationLocationResultPath string
60+
5561
// Response contains a preconstructed response type.
5662
// The final payload will be unmarshaled into it and returned.
5763
Response *T
@@ -98,7 +104,7 @@ func NewPoller[T any](resp *http.Response, pl exported.Pipeline, options *NewPol
98104
opr, err = async.New[T](pl, resp, options.FinalStateVia)
99105
} else if op.Applicable(resp) {
100106
// op poller must be checked before loc as it can also have a location header
101-
opr, err = op.New[T](pl, resp, options.FinalStateVia)
107+
opr, err = op.New[T](pl, resp, options.FinalStateVia, options.OperationLocationResultPath)
102108
} else if loc.Applicable(resp) {
103109
opr, err = loc.New[T](pl, resp)
104110
} else if body.Applicable(resp) {
@@ -172,7 +178,7 @@ func NewPollerFromResumeToken[T any](token string, pl exported.Pipeline, options
172178
} else if loc.CanResume(asJSON) {
173179
opr, _ = loc.New[T](pl, nil)
174180
} else if op.CanResume(asJSON) {
175-
opr, _ = op.New[T](pl, nil, "")
181+
opr, _ = op.New[T](pl, nil, "", "")
176182
} else {
177183
return nil, fmt.Errorf("unhandled poller token %s", string(raw))
178184
}

sdk/azcore/runtime/poller_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,9 @@ func TestOpPollerWithWidgetPOST(t *testing.T) {
514514
},
515515
}
516516
pl := newTestPipeline(&policy.ClientOptions{Transport: srv})
517-
lro, err := NewPoller[widget](firstResp, pl, nil)
517+
lro, err := NewPoller(firstResp, pl, &NewPollerOptions[widget]{
518+
OperationLocationResultPath: "result",
519+
})
518520
if err != nil {
519521
t.Fatal(err)
520522
}

0 commit comments

Comments
 (0)