-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
325 lines (290 loc) · 8.52 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
package verhist
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/kenshaw/httplog"
)
// https://versionhistory.googleapis.com/v1/chrome/platforms/all/channels/all/versions/
// https://versionhistory.googleapis.com/v1/chrome/platforms/all/channels/all/versions/all/releases?filter=endtime%3E2023-01-01T00:00:00Z
// https://developer.chrome.com/docs/web-platform/versionhistory/guide
// https://developer.chrome.com/docs/web-platform/versionhistory/reference
// https://developer.chrome.com/docs/web-platform/versionhistory/examples
// DefaultTransport is the default transport.
var DefaultTransport = http.DefaultTransport
// BaseURL is the base URL.
var BaseURL = "https://versionhistory.googleapis.com"
// Client is a version history client.
type Client struct {
Transport http.RoundTripper
}
// New creates a new version history client.
func New(opts ...Option) *Client {
cl := &Client{
Transport: DefaultTransport,
}
for _, o := range opts {
o(cl)
}
return cl
}
// Platforms returns platforms.
func (cl *Client) Platforms(ctx context.Context) ([]Platform, error) {
res := new(PlatformsResponse)
if err := grab(ctx, BaseURL+"/v1/chrome/platforms/", cl.Transport, res); err != nil {
return nil, err
}
return res.Platforms, nil
}
// Channels returns channels for the platform type.
func (cl *Client) Channels(ctx context.Context, typ PlatformType) ([]Channel, error) {
res := new(ChannelsResponse)
if err := grab(ctx, BaseURL+"/v1/chrome/platforms/"+typ.String()+"/channels", cl.Transport, res); err != nil {
return nil, err
}
return res.Channels, nil
}
// All returns all channels.
func (cl *Client) All(ctx context.Context) ([]Channel, error) {
return cl.Channels(ctx, All)
}
// Versions returns the versions for the platform, channel.
func (cl *Client) Versions(ctx context.Context, platform, channel string, q ...string) ([]Version, error) {
if len(q) == 0 {
q = []string{
"order_by", "version desc",
}
}
res := new(VersionsResponse)
if err := grab(ctx, BaseURL+"/v1/chrome/platforms/"+platform+"/channels/"+channel+"/versions", cl.Transport, res, q...); err != nil {
return nil, err
}
return res.Versions, nil
}
// Latest returns the latest version for the platform, channel.
func (cl *Client) Latest(ctx context.Context, platform, channel string) (Version, error) {
versions, err := cl.Versions(ctx, platform, channel)
switch {
case err != nil:
return Version{}, err
case len(versions) == 0:
return Version{}, ErrNoVersionsAvailable
}
return versions[0], nil
}
// UserAgent builds the user agent for the platform, channel.
func (cl *Client) UserAgent(ctx context.Context, platform, channel string) (string, error) {
latest, err := cl.Latest(ctx, platform, channel)
if err != nil {
return "", err
}
return latest.UserAgent(platform), nil
}
// PlatformsResponse wraps the platforms API response.
type PlatformsResponse struct {
Platforms []Platform `json:"platforms,omitempty"`
NextPageToken string `json:"nextPageToken,omitempty"`
}
// Platform contains information about a chrome platform.
type Platform struct {
Name string `json:"name,omitempty"`
PlatformType PlatformType `json:"platformType,omitempty"`
}
// PlatformType is a platform type.
type PlatformType string
// Platform types.
const (
All PlatformType = "all"
Android PlatformType = "android"
ChromeOS PlatformType = "chromeos"
Fuchsia PlatformType = "fuchsia"
IOS PlatformType = "ios"
LacrosARM32 PlatformType = "lacros_arm32"
LacrosARM64 PlatformType = "lacros_arm64"
Lacros PlatformType = "lacros"
Linux PlatformType = "linux"
MacARM64 PlatformType = "mac_arm64"
Mac PlatformType = "mac"
Webview PlatformType = "webview"
Windows64 PlatformType = "win64"
WindowsARM64 PlatformType = "win_arm64"
Windows PlatformType = "win"
)
// String satisfies the [fmt.Stinger] interface.
func (typ PlatformType) String() string {
return strings.ToLower(string(typ))
}
// MarshalText satisfies the [encoding.TextMarshaler] interface.
func (typ PlatformType) MarshalText() ([]byte, error) {
return []byte(typ.String()), nil
}
// UnmarshalText satisfies the [encoding.TextUnmarshaler] interface.
func (typ *PlatformType) UnmarshalText(buf []byte) error {
switch PlatformType(strings.ToLower(string(buf))) {
case All:
*typ = All
case Android:
*typ = Android
case ChromeOS:
*typ = ChromeOS
case Fuchsia:
*typ = Fuchsia
case IOS:
*typ = IOS
case LacrosARM32:
*typ = LacrosARM32
case LacrosARM64:
*typ = LacrosARM64
case Lacros:
*typ = Lacros
case Linux:
*typ = Linux
case MacARM64:
*typ = MacARM64
case Mac:
*typ = Mac
case Webview:
*typ = Webview
case Windows64:
*typ = Windows64
case WindowsARM64:
*typ = WindowsARM64
case Windows:
*typ = Windows
default:
return ErrInvalidPlatformType
}
return nil
}
// ChannelsResponse wraps the channels API response.
type ChannelsResponse struct {
Channels []Channel `json:"channels,omitempty"`
NextPageToken string `json:"nextPageToken,omitempty"`
}
// Channel contains information about a chrome channel.
type Channel struct {
Name string `json:"name,omitempty"`
ChannelType ChannelType `json:"channelType,omitempty"`
}
// ChannelType is a channel type.
type ChannelType string
// Channel types.
const (
Beta ChannelType = "beta"
CanaryASAN ChannelType = "canary_asan"
Canary ChannelType = "canary"
Dev ChannelType = "dev"
Extended ChannelType = "extended"
LTS ChannelType = "lts"
LTC ChannelType = "ltc"
Stable ChannelType = "stable"
)
// String satisfies the [fmt.Stinger] interface.
func (typ ChannelType) String() string {
return strings.ToLower(string(typ))
}
// MarshalText satisfies the [encoding.TextMarshaler] interface.
func (typ ChannelType) MarshalText() ([]byte, error) {
return []byte(typ.String()), nil
}
// UnmarshalText satisfies the [encoding.TextUnmarshaler] interface.
func (typ *ChannelType) UnmarshalText(buf []byte) error {
switch ChannelType(strings.ToLower(string(buf))) {
case Beta:
*typ = Beta
case CanaryASAN:
*typ = CanaryASAN
case Canary:
*typ = Canary
case Dev:
*typ = Dev
case Extended:
*typ = Extended
case LTS:
*typ = LTS
case LTC:
*typ = LTC
case Stable:
*typ = Stable
default:
return ErrInvalidChannelType
}
return nil
}
// VersionsResponse wraps the versions API response.
type VersionsResponse struct {
Versions []Version `json:"versions,omitempty"`
NextPageToken string `json:"nextPageToken,omitempty"`
}
// Version contains information about a chrome release.
type Version struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
}
// UserAgent builds a user agent for the platform.
func (ver Version) UserAgent(platform string) string {
typ, extra := "Windows NT 10.0; Win64; x64", ""
switch strings.ToLower(platform) {
case "linux":
typ = "X11; Linux x86_64"
case "mac", "mac_arm64":
typ = "Macintosh; Intel Mac OS X 10_15_7"
case "android":
typ, extra = "Linux; Android 10; K", " Mobile"
}
v := "120.0.0.0"
if i := strings.Index(ver.Version, "."); i != -1 {
v = ver.Version[:i] + ".0.0.0"
}
return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s%s Safari/537.36", typ, v, extra)
}
// Option is a version history client option.
type Option func(*Client)
// WithTransport is a version history client option to set the http transport.
func WithTransport(transport http.RoundTripper) Option {
return func(cl *Client) {
cl.Transport = transport
}
}
// WithLogf is a version history client option to set a log handler for HTTP
// requests and responses.
func WithLogf(logf interface{}, opts ...httplog.Option) Option {
return func(cl *Client) {
cl.Transport = httplog.NewPrefixedRoundTripLogger(cl.Transport, logf, opts...)
}
}
// grab grabs the url and json decodes it.
func grab(ctx context.Context, urlstr string, transport http.RoundTripper, v interface{}, q ...string) error {
if len(q)%2 != 0 {
return ErrInvalidQuery
}
z := make(url.Values)
for i := 0; i < len(q); i += 2 {
z.Add(q[i], q[i+1])
}
s := z.Encode()
if s != "" {
s = "?" + s
}
req, err := http.NewRequestWithContext(ctx, "GET", urlstr+s, nil)
if err != nil {
return err
}
cl := &http.Client{
Transport: transport,
}
res, err := cl.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return fmt.Errorf("could not retrieve %s (status: %d)", urlstr, res.StatusCode)
}
dec := json.NewDecoder(res.Body)
dec.DisallowUnknownFields()
return dec.Decode(v)
}