Skip to content

Commit ef70c17

Browse files
Provide a ListOpts helper
With `query.ListOpts`, it should be easier to list resources based on repeated properties. For example: get information about multiple ports by ID with a single call. ListOpts is currently implemented for three Network resources: * ports * networks * subnets
1 parent de873b9 commit ef70c17

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed

query/errors.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package query
2+
3+
// As we can't use Go v1.20 in this module yet, this is an adaptation of
4+
// `errors.Join`.
5+
// https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/errors/join.go
6+
7+
func joinErrors(errs ...error) error {
8+
n := 0
9+
for _, err := range errs {
10+
if err != nil {
11+
n++
12+
}
13+
}
14+
if n == 0 {
15+
return nil
16+
}
17+
e := &joinError{
18+
errs: make([]error, 0, n),
19+
}
20+
for _, err := range errs {
21+
if err != nil {
22+
e.errs = append(e.errs, err)
23+
}
24+
}
25+
return e
26+
}
27+
28+
29+
type joinError struct {
30+
errs []error
31+
}
32+
33+
func (e *joinError) Error() string {
34+
var b []byte
35+
for i, err := range e.errs {
36+
if i > 0 {
37+
b = append(b, '\n')
38+
}
39+
b = append(b, err.Error()...)
40+
}
41+
return string(b)
42+
}

query/list.go

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package query
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"reflect"
7+
8+
"github.com/gophercloud/gophercloud"
9+
)
10+
11+
func New(listOpts interface{}) *ListOpts {
12+
availableFields := make(map[string]string)
13+
{
14+
t := reflect.TypeOf(listOpts)
15+
for i := 0; i < t.NumField(); i++ {
16+
if tag := t.Field(i).Tag.Get("q"); tag != "" {
17+
availableFields[tag] = t.Field(i).Name
18+
}
19+
}
20+
}
21+
22+
queryString, err := gophercloud.BuildQueryString(listOpts)
23+
24+
return &ListOpts{
25+
allowedFields: availableFields,
26+
query: queryString.Query(),
27+
errs: joinErrors(err),
28+
}
29+
}
30+
31+
// ListOpts can be used to list multiple resources.
32+
type ListOpts struct {
33+
allowedFields map[string]string
34+
query url.Values
35+
errs error
36+
}
37+
38+
// And adds an arbitrary number of permutations of a single property to filter
39+
// in. When a single ListOpts is called multiple times with the same property
40+
// name, the resulting query contains the resulting intersection (AND). Note
41+
// that how these properties are combined in OpenStack depend on the property.
42+
// For example: passing multiple "id" behaves like an OR. Instead, passing
43+
// multiple "tags" will only return resources that have ALL those tags. This
44+
// helper function only combines the parameters in the most straightforward
45+
// way; please refer to the OpenStack documented behaviour to know how these
46+
// parameters are treated.
47+
//
48+
// ListOpts is currently implemented for three Network resources:
49+
//
50+
// * ports
51+
// * networks
52+
// * subnets
53+
func (o *ListOpts) And(property string, values ...interface{}) *ListOpts {
54+
if existingValues, ok := o.query[property]; ok {
55+
// There already are values of the same property: we AND them
56+
// with the new ones. We only keep the values that exist in
57+
// both `o.query` AND in `values`.
58+
59+
// First, to avoid nested loops, we build a hashmap with the
60+
// new values.
61+
newValuesSet := make(map[string]struct{})
62+
for _, newValue := range values {
63+
newValuesSet[fmt.Sprint(newValue)] = struct{}{}
64+
}
65+
66+
// intersectedValues is a slice which will contain the values
67+
// that we want to keep. They will be at most as many as what
68+
// we already have; that's what we set the slice capacity to.
69+
intersectedValues := make([]string, 0, len(existingValues))
70+
71+
// We add each existing value to intersectedValues if and only
72+
// if it's also present in the new set.
73+
for _, existingValue := range existingValues {
74+
if _, ok := newValuesSet[existingValue]; ok {
75+
intersectedValues = append(intersectedValues, existingValue)
76+
}
77+
}
78+
o.query[property] = intersectedValues
79+
return o
80+
}
81+
82+
if _, ok := o.allowedFields[property]; !ok {
83+
o.errs = joinErrors(o.errs, fmt.Errorf("invalid property for the filter: %q", property))
84+
return o
85+
}
86+
87+
for _, v := range values {
88+
o.query.Add(property, fmt.Sprint(v))
89+
}
90+
91+
return o
92+
}
93+
94+
func (o ListOpts) String() string {
95+
return "?" + o.query.Encode()
96+
}
97+
98+
func (o ListOpts) ToPortListQuery() (string, error) {
99+
return o.String(), o.errs
100+
}
101+
102+
func (o ListOpts) ToNetworkListQuery() (string, error) {
103+
return o.String(), o.errs
104+
}
105+
106+
func (o ListOpts) ToSubnetListQuery() (string, error) {
107+
return o.String(), o.errs
108+
}

query/list_test.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package query_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
8+
"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
9+
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
10+
"github.com/gophercloud/utils/query"
11+
)
12+
13+
var _ networks.ListOptsBuilder = (*query.ListOpts)(nil)
14+
var _ ports.ListOptsBuilder = (*query.ListOpts)(nil)
15+
var _ subnets.ListOptsBuilder = (*query.ListOpts)(nil)
16+
17+
func ExampleListOpts_And_by_id() {
18+
q := query.New(ports.ListOpts{
19+
Name: "Jules",
20+
}).And("id", "123", "321", "12345")
21+
fmt.Println(q)
22+
//Output: ?id=123&id=321&id=12345&name=Jules
23+
}
24+
25+
func ExampleListOpts_And_by_name() {
26+
q := query.New(ports.ListOpts{}).
27+
And("name", "port-1", "port-&321", "the-other-port")
28+
fmt.Println(q)
29+
//Output: ?name=port-1&name=port-%26321&name=the-other-port
30+
}
31+
32+
func ExampleListOpts_And_by_Name_and_tag() {
33+
q := query.New(ports.ListOpts{}).
34+
And("name", "port-1", "port-3").
35+
And("tags", "my-tag")
36+
fmt.Println(q)
37+
//Output: ?name=port-1&name=port-3&tags=my-tag
38+
}
39+
40+
func ExampleListOpts_And_by_id_twice() {
41+
q := query.New(ports.ListOpts{}).
42+
And("id", "1", "2", "3").
43+
And("id", "2", "3", "4")
44+
fmt.Println(q)
45+
//Output: ?id=2&id=3
46+
}
47+
48+
func ExampleListOpts_And_by_id_twice_plus_ListOpts() {
49+
q := query.New(ports.ListOpts{ID: "3"}).
50+
And("id", "1", "2", "3").
51+
And("id", "3", "4", "5")
52+
fmt.Println(q)
53+
//Output: ?id=3
54+
}
55+
56+
func TestToPortListQuery(t *testing.T) {
57+
for _, tc := range [...]struct {
58+
name string
59+
base interface{}
60+
andProperty string
61+
andItems []interface{}
62+
expected string
63+
expectedError bool
64+
}{
65+
{
66+
"valid",
67+
ports.ListOpts{},
68+
"name",
69+
[]interface{}{"port-1"},
70+
"?name=port-1",
71+
false,
72+
},
73+
{
74+
"invalid_field",
75+
ports.ListOpts{},
76+
"door",
77+
[]interface{}{"pod bay"},
78+
"?",
79+
true,
80+
},
81+
} {
82+
t.Run(tc.name, func(t *testing.T) {
83+
q, err := query.New(tc.base).And(tc.andProperty, tc.andItems...).ToPortListQuery()
84+
if q != tc.expected {
85+
t.Errorf("expected query %q, got %q", tc.expected, q)
86+
}
87+
if (err != nil) != tc.expectedError {
88+
if err != nil {
89+
t.Errorf("unexpected error: %v", err)
90+
} else {
91+
t.Errorf("expected error, got nil")
92+
}
93+
}
94+
})
95+
}
96+
}

0 commit comments

Comments
 (0)