Skip to content

Commit dc8e03e

Browse files
committed
fwserver: add ListResource method
1 parent d6776b3 commit dc8e03e

File tree

3 files changed

+278
-0
lines changed

3 files changed

+278
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fwserver
5+
6+
import (
7+
"context"
8+
"iter"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
12+
"github.com/hashicorp/terraform-plugin-framework/list"
13+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
14+
)
15+
16+
type ListResourceRequest struct {
17+
// ListResource is ...
18+
ListResource list.ListResource
19+
20+
Config tfsdk.Config
21+
22+
IncludeResourceObject bool
23+
}
24+
25+
type ListResourceStream struct {
26+
Results iter.Seq[ListResourceEvent]
27+
Diagnostics diag.Diagnostics
28+
}
29+
30+
type ListResourceEvent struct {
31+
// Identity is the identity of the managed resource instance.
32+
// A nil value will raise a diagnostic.
33+
Identity *tfsdk.ResourceIdentity
34+
35+
// ResourceObject is the provider's representation of the attributes of the
36+
// listed managed resource instance.
37+
// If ListResourceRequest.IncludeResourceObject is true, a nil value will raise
38+
// a warning diagnostic.
39+
ResourceObject *tfsdk.ResourceObject
40+
41+
// DisplayName is a provider-defined human-readable description of the
42+
// listed managed resource instance, intended for CLI and browser UIs.
43+
DisplayName string
44+
45+
// Diagnostics report errors or warnings related to the listed managed
46+
// resource instance. An empty slice indicates a successful operation with
47+
// no warnings or errors generated.
48+
Diagnostics diag.Diagnostics
49+
}
50+
51+
// ListResource implements the framework server ListResource RPC.
52+
func (s *Server) ListResource(ctx context.Context, fwReq *ListResourceRequest, fwStream *ListResourceStream) {
53+
listResource := fwReq.ListResource
54+
55+
req := list.ListResourceRequest{
56+
Config: fwReq.Config,
57+
IncludeResourceObject: fwReq.IncludeResourceObject,
58+
}
59+
60+
stream := &list.ListResourceResponse{} // Stream{}
61+
62+
logging.FrameworkTrace(ctx, "Calling provider defined ListResource")
63+
listResource.ListResource(ctx, req, stream)
64+
logging.FrameworkTrace(ctx, "Called provider defined ListResource")
65+
66+
if stream.Diagnostics.HasError() {
67+
// fwStream.Results = slices.Values([]ListResourceEvent{})
68+
fwStream.Diagnostics = stream.Diagnostics
69+
return
70+
}
71+
72+
if stream.Results == nil {
73+
// If the provider returned a nil results stream, we treat it as an empty stream.
74+
stream.Results = iter.Seq[list.ListResourceEvent](func(yield func(list.ListResourceEvent) bool) {})
75+
}
76+
77+
fwStream.Results = listResourceEventStreamAdapter(stream.Results)
78+
}
79+
80+
func listResourceEventStreamAdapter(stream iter.Seq[list.ListResourceEvent]) iter.Seq[ListResourceEvent] {
81+
return func(yield func(ListResourceEvent) bool) {
82+
for event := range stream {
83+
if !yield(ListResourceEvent(event)) {
84+
break
85+
}
86+
}
87+
}
88+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fwserver_test
5+
6+
import (
7+
"context"
8+
"slices"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
"github.com/hashicorp/terraform-plugin-framework/diag"
13+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
14+
"github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider"
15+
"github.com/hashicorp/terraform-plugin-framework/list"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
18+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
19+
"github.com/hashicorp/terraform-plugin-go/tftypes"
20+
)
21+
22+
func TestServerListResource(t *testing.T) {
23+
t.Parallel()
24+
25+
testSchema := schema.Schema{
26+
Attributes: map[string]schema.Attribute{
27+
"test_computed": schema.StringAttribute{
28+
Computed: true,
29+
},
30+
"test_required": schema.StringAttribute{
31+
Required: true,
32+
},
33+
},
34+
}
35+
36+
testType := tftypes.Object{
37+
AttributeTypes: map[string]tftypes.Type{
38+
"test_attribute": tftypes.String,
39+
},
40+
}
41+
42+
testResourceObjectValue1 := tftypes.NewValue(testType, map[string]tftypes.Value{
43+
"test_attribute": tftypes.NewValue(tftypes.String, "test-value-1"),
44+
})
45+
46+
testResourceObjectValue2 := tftypes.NewValue(testType, map[string]tftypes.Value{
47+
"test_attribute": tftypes.NewValue(tftypes.String, "test-value-2"),
48+
})
49+
50+
testIdentitySchema := identityschema.Schema{
51+
Attributes: map[string]identityschema.Attribute{
52+
"test_id": identityschema.StringAttribute{
53+
RequiredForImport: true,
54+
},
55+
},
56+
}
57+
58+
testIdentityType := tftypes.Object{
59+
AttributeTypes: map[string]tftypes.Type{
60+
"test_id": tftypes.String,
61+
},
62+
}
63+
64+
testIdentityValue1 := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{
65+
"test_id": tftypes.NewValue(tftypes.String, "new-id-123"),
66+
})
67+
68+
testIdentityValue2 := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{
69+
"test_id": tftypes.NewValue(tftypes.String, "new-id-456"),
70+
})
71+
72+
// nilIdentityValue := tftypes.NewValue(testIdentityType, nil)
73+
74+
testCases := map[string]struct {
75+
server *fwserver.Server
76+
request *fwserver.ListResourceRequest
77+
expectedStreamEvents []fwserver.ListResourceEvent
78+
}{
79+
"success-with-zero-results": {
80+
server: &fwserver.Server{
81+
Provider: &testprovider.Provider{},
82+
},
83+
request: &fwserver.ListResourceRequest{
84+
ListResource: &testprovider.ListResource{
85+
ListResourceMethod: func(ctx context.Context, req list.ListResourceRequest, resp *list.ListResourceResponse) { // TODO
86+
resp.Results = slices.Values([]list.ListResourceEvent{})
87+
},
88+
},
89+
},
90+
expectedStreamEvents: []fwserver.ListResourceEvent{},
91+
},
92+
"success-with-nil-results": {
93+
server: &fwserver.Server{
94+
Provider: &testprovider.Provider{},
95+
},
96+
request: &fwserver.ListResourceRequest{
97+
ListResource: &testprovider.ListResource{
98+
ListResourceMethod: func(ctx context.Context, req list.ListResourceRequest, resp *list.ListResourceResponse) { // TODO
99+
// Do nothing, so that resp.Results is nil
100+
},
101+
},
102+
},
103+
expectedStreamEvents: []fwserver.ListResourceEvent{},
104+
},
105+
106+
"success-with-multiple-results": {
107+
server: &fwserver.Server{
108+
Provider: &testprovider.Provider{},
109+
},
110+
request: &fwserver.ListResourceRequest{
111+
ListResource: &testprovider.ListResource{
112+
ListResourceMethod: func(ctx context.Context, req list.ListResourceRequest, resp *list.ListResourceResponse) { // TODO
113+
resp.Results = slices.Values([]list.ListResourceEvent{
114+
{
115+
Identity: &tfsdk.ResourceIdentity{
116+
Schema: testIdentitySchema,
117+
Raw: testIdentityValue1,
118+
},
119+
ResourceObject: &tfsdk.ResourceObject{
120+
Schema: testSchema,
121+
Raw: testResourceObjectValue1,
122+
},
123+
DisplayName: "Test Resource 1",
124+
Diagnostics: diag.Diagnostics{},
125+
},
126+
{
127+
Identity: &tfsdk.ResourceIdentity{
128+
Schema: testIdentitySchema,
129+
Raw: testIdentityValue2,
130+
},
131+
ResourceObject: &tfsdk.ResourceObject{
132+
Schema: testSchema,
133+
Raw: testResourceObjectValue2,
134+
},
135+
DisplayName: "Test Resource 2",
136+
Diagnostics: diag.Diagnostics{},
137+
},
138+
})
139+
},
140+
},
141+
},
142+
expectedStreamEvents: []fwserver.ListResourceEvent{
143+
{
144+
Identity: &tfsdk.ResourceIdentity{
145+
Schema: testIdentitySchema,
146+
Raw: testIdentityValue1,
147+
},
148+
ResourceObject: &tfsdk.ResourceObject{
149+
Schema: testSchema,
150+
Raw: testResourceObjectValue1,
151+
},
152+
DisplayName: "Test Resource 1",
153+
Diagnostics: diag.Diagnostics{},
154+
},
155+
{
156+
Identity: &tfsdk.ResourceIdentity{
157+
Schema: testIdentitySchema,
158+
Raw: testIdentityValue2,
159+
},
160+
ResourceObject: &tfsdk.ResourceObject{
161+
Schema: testSchema,
162+
Raw: testResourceObjectValue2,
163+
},
164+
DisplayName: "Test Resource 2",
165+
Diagnostics: diag.Diagnostics{},
166+
},
167+
},
168+
},
169+
}
170+
171+
for name, testCase := range testCases {
172+
t.Run(name, func(t *testing.T) {
173+
t.Parallel()
174+
175+
response := &fwserver.ListResourceStream{}
176+
testCase.server.ListResource(context.Background(), testCase.request, response)
177+
178+
events := slices.AppendSeq([]fwserver.ListResourceEvent{}, response.Results)
179+
if diff := cmp.Diff(events, testCase.expectedStreamEvents); diff != "" {
180+
t.Errorf("unexpected difference: %s", diff)
181+
}
182+
})
183+
}
184+
}

list/list_resource.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ type ListResultsStream struct {
112112
// Results is a function that emits ListResult values via its yield
113113
// function argument.
114114
Results iter.Seq[ListResult]
115+
116+
// Diagnostics report errors or warnings related to the list resource
117+
// operation. An empty slice indicates a successful operation with no
118+
// warnings or errors generated. Individual ListResourceEvents may have
119+
// specific diagnostics.
120+
Diagnostics diag.Diagnostics
115121
}
116122

117123
// ListResult represents a listed managed resource instance.

0 commit comments

Comments
 (0)