Skip to content

Commit 4341a1d

Browse files
committed
Appengine: provide a new API client
In the context of #33, rationalize and update the current API for interacting with Astarte Appengine. Use a type-safe request/response pattern. Signed-off-by: Arnaldo Cesco <arnaldo.cesco@secomind.com>
1 parent 3e8f8fb commit 4341a1d

30 files changed

+4464
-640
lines changed

client/appengine.go

Lines changed: 333 additions & 195 deletions
Large diffs are not rendered by default.

client/appengine_data.go

Lines changed: 511 additions & 0 deletions
Large diffs are not rendered by default.

client/appengine_data_paginator.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright © 2023 SECO Mind srl
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package client
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"net/http"
21+
"net/url"
22+
"time"
23+
24+
"github.com/astarte-platform/astarte-go/interfaces"
25+
"moul.io/http2curl"
26+
)
27+
28+
// ResultSetOrder represents the order of the samples.
29+
type ResultSetOrder int
30+
31+
const (
32+
// AscendingOrder means the Paginator will return results starting from the oldest.
33+
AscendingOrder ResultSetOrder = iota
34+
// DescendingOrder means the Paginator will return results starting from the oldest.
35+
DescendingOrder
36+
)
37+
38+
// DatastreamPaginator handles a paginated set of results. It provides a one-directional iterator to call onto
39+
// Astarte AppEngine API and handle potentially extremely large sets of results in chunk.
40+
type DatastreamPaginator struct {
41+
baseURL *url.URL
42+
since time.Time
43+
to time.Time
44+
firstPage bool
45+
nextQuery url.Values
46+
resultSetOrder ResultSetOrder
47+
pageSize int
48+
client *Client
49+
hasNextPage bool
50+
aggregation interfaces.AstarteInterfaceAggregation
51+
}
52+
53+
// Rewind rewinds the paginator to the first page. GetNextPage will then return the first page of the call.
54+
func (d *DatastreamPaginator) Rewind() {
55+
// Invalid time
56+
d.since = time.Time{}
57+
d.to = time.Time{}
58+
d.nextQuery = url.Values{}
59+
d.hasNextPage = true
60+
d.firstPage = true
61+
}
62+
63+
// HasNextPage returns whether this paginator can return more pages.
64+
func (d *DatastreamPaginator) HasNextPage() bool {
65+
return d.hasNextPage
66+
}
67+
68+
// GetPageSize returns the page size for this paginator.
69+
func (d *DatastreamPaginator) GetPageSize() int {
70+
return d.pageSize
71+
}
72+
73+
// GetResultSetOrder returns the order in which samples are returned for this paginator.
74+
func (d *DatastreamPaginator) GetResultSetOrder() ResultSetOrder {
75+
return d.resultSetOrder
76+
}
77+
78+
// GetNextPage returns a request to get the next result page from the paginator.
79+
// If no more results are available, HasNextPage will return false.
80+
// GetNextPage throws an error if no more pages are available or if an invalid parameter is specified.
81+
func (d *DatastreamPaginator) GetNextPage() (AstarteRequest, error) {
82+
if !d.hasNextPage {
83+
return nil, errors.New("No more pages available")
84+
}
85+
86+
callURL, err := d.setupCallURL()
87+
if err != nil {
88+
return Empty{}, err
89+
}
90+
req := d.client.makeHTTPrequest(http.MethodGet, callURL, nil)
91+
92+
return GetNextDatastreamPageRequest{req: req, expects: 200, paginator: d}, nil
93+
}
94+
95+
type GetNextDatastreamPageRequest struct {
96+
req *http.Request
97+
expects int
98+
paginator Paginator
99+
}
100+
101+
func (r GetNextDatastreamPageRequest) Run(c *Client) (AstarteResponse, error) {
102+
res, err := c.httpClient.Do(r.req)
103+
if err != nil {
104+
return Empty{}, err
105+
}
106+
if res.StatusCode != r.expects {
107+
return r.handleNextDatastreamPageFail(res)
108+
}
109+
return GetNextDatastreamPageResponse{res: res, paginator: &r.paginator}, nil
110+
}
111+
112+
func (r GetNextDatastreamPageRequest) handleNextDatastreamPageFail(res *http.Response) (AstarteResponse, error) {
113+
if res.Body == nil {
114+
return Empty{}, ErrDifferentStatusCode(r.expects, res.StatusCode)
115+
}
116+
// A quirky corner case:
117+
// when the size of Astarte data is a multiple of r.paginator.pageSize,
118+
// the last page will be too far in the future and the last request will fail.
119+
// Let's make sure everything works correctly.
120+
p, _ := r.paginator.(*DatastreamPaginator)
121+
if !p.firstPage {
122+
return GetNextDatastreamPageResponse{res: res, paginator: &r.paginator}, nil
123+
}
124+
// now that the corner case is handled, if we're here we must fail
125+
return Empty{}, errorFromJSONErrors(res.Body)
126+
}
127+
128+
func (r GetNextDatastreamPageRequest) ToCurl(c *Client) string {
129+
command, _ := http2curl.GetCurlCommand(r.req)
130+
return fmt.Sprint(command)
131+
}
132+
133+
func (d *DatastreamPaginator) setupCallURL() (*url.URL, error) {
134+
callURL, _ := url.Parse(d.baseURL.String())
135+
136+
query := d.nextQuery
137+
switch d.resultSetOrder {
138+
case AscendingOrder:
139+
// If no start is set, let's start from the beginnning of time
140+
if (d.since == time.Time{}) {
141+
fmt.Println("No start is set")
142+
d.since = time.Unix(0, 0)
143+
}
144+
// All data in the next page come from a time after 'since' (so we descend)
145+
if d.firstPage {
146+
// first page includes also the starting value
147+
query.Set("since", d.since.UTC().Format(time.RFC3339Nano))
148+
} else {
149+
// pages after the first must not include the starting value
150+
query.Set("since_after", d.since.UTC().Format(time.RFC3339Nano))
151+
query.Del("since")
152+
}
153+
if (d.to != time.Time{}) {
154+
// All data in the next page come from a time until 'to'
155+
query.Set("to", d.to.UTC().Format(time.RFC3339Nano))
156+
}
157+
if d.pageSize != 0 {
158+
query.Set("limit", fmt.Sprintf("%d", d.pageSize))
159+
}
160+
161+
case DescendingOrder:
162+
if d.pageSize == 0 {
163+
return &url.URL{}, fmt.Errorf("A limit parameter must be specified when using DescendingOrder")
164+
}
165+
if (d.since != time.Time{}) {
166+
return &url.URL{}, fmt.Errorf("A since parameter must not be specified when using DescendingOrder")
167+
}
168+
query.Set("limit", fmt.Sprintf("%d", d.pageSize))
169+
// if "to" doesn't exist, default behavior with only "limit" is descending
170+
if (d.to != time.Time{}) {
171+
// All data in the next page come from a time until 'to' (so we descend)
172+
query.Set("to", d.to.UTC().Format(time.RFC3339Nano))
173+
}
174+
}
175+
176+
callURL.RawQuery = query.Encode()
177+
178+
return callURL, nil
179+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright © 2023 SECO Mind srl
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package client
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"net/http"
21+
"net/url"
22+
23+
"moul.io/http2curl"
24+
)
25+
26+
// DeviceListPaginator handles a paginated set of results. It provides a one-directional iterator to call onto
27+
// Astarte AppEngine API and handle potentially extremely large sets of results in chunk. You should prefer
28+
// DeviceListPaginator rather than direct API calls if you expect your result set to be particularly large.
29+
type DeviceListPaginator struct {
30+
baseURL *url.URL
31+
nextQuery url.Values
32+
format DeviceResultFormat
33+
pageSize int
34+
client *Client
35+
hasNextPage bool
36+
}
37+
38+
// Rewind rewinds the simulator to the first page. GetNextPage will then return the first page of the call.
39+
func (d *DeviceListPaginator) Rewind() {
40+
d.nextQuery = url.Values{}
41+
d.hasNextPage = true
42+
}
43+
44+
// HasNextPage returns whether this paginator can return more pages
45+
func (d *DeviceListPaginator) HasNextPage() bool {
46+
return d.hasNextPage
47+
}
48+
49+
// GetPageSize returns the page size for this paginator
50+
func (d *DeviceListPaginator) GetPageSize() int {
51+
return d.pageSize
52+
}
53+
54+
type GetNextDeviceListPageRequest struct {
55+
req *http.Request
56+
expects int
57+
paginator Paginator
58+
}
59+
60+
// Performs a request to get the next page.
61+
// Returns either a response that can be parsed with Parse() or an error if the request failed.
62+
func (r GetNextDeviceListPageRequest) Run(c *Client) (AstarteResponse, error) {
63+
res, err := c.httpClient.Do(r.req)
64+
if err != nil {
65+
return Empty{}, err
66+
}
67+
if res.StatusCode != r.expects {
68+
if res.Body != nil {
69+
return Empty{}, errorFromJSONErrors(res.Body)
70+
}
71+
return Empty{}, ErrDifferentStatusCode(r.expects, res.StatusCode)
72+
}
73+
return GetNextDeviceListPageResponse{res: res, paginator: &r.paginator}, nil
74+
}
75+
76+
// Returns the curl command corresponding to the request to get the next page.
77+
func (r GetNextDeviceListPageRequest) ToCurl(c *Client) string {
78+
command, _ := http2curl.GetCurlCommand(r.req)
79+
return fmt.Sprint(command)
80+
}
81+
82+
// GetNextPage returns a request to get the next result page from the paginator.
83+
// If no more results are available, HasNextPage will return false.
84+
// GetNextPage throws an error if no more pages are available.
85+
func (d *DeviceListPaginator) GetNextPage() (AstarteRequest, error) {
86+
if !d.hasNextPage {
87+
return Empty{}, errors.New("No more pages available")
88+
}
89+
90+
callURL := d.setupCallURL()
91+
req := d.client.makeHTTPrequest(http.MethodGet, callURL, nil)
92+
93+
return GetNextDeviceListPageRequest{req: req, expects: 200, paginator: d}, nil
94+
}
95+
96+
func (d *DeviceListPaginator) setupCallURL() *url.URL {
97+
// TODO check err
98+
callURL, _ := url.Parse(d.baseURL.String())
99+
query := d.nextQuery
100+
switch d.format {
101+
case DeviceIDFormat:
102+
query.Set("details", "false")
103+
case DeviceDetailsFormat:
104+
query.Set("details", "true")
105+
}
106+
107+
callURL.RawQuery = query.Encode()
108+
109+
return callURL
110+
}

0 commit comments

Comments
 (0)