Repository navigation
Expand file tree
/
Copy pathrandomorg.go
More file actions
197 lines (169 loc) · 5.27 KB
/
Copy pathrandomorg.go
File metadata and controls
197 lines (169 loc) · 5.27 KB
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
// Package randomorg is a Random.org API client as described at https://api.random.org/json-rpc/4.
// This is a third-party client. See https://github.com/sgade/randomorg.
// For any method documentation you should take a look at the official API documentation.
// An API key can be acquired here: https://api.random.org/dashboard.
package randomorg
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
// Private constants
const (
// The Random.org API request endpoint URL
requestEndpoint = "https://api.random.org/json-rpc/4/invoke"
// The time.Parse layout for the creationTime field the API returns, e.g. "2013-02-20 17:53:40Z"
creationTimeLayout = time.RFC3339Nano
// API Error template string
errAPI = "api error code %v: %q"
)
// Constants describing error situations.
var (
// ErrAPIKey is the error returned when an invalid API key was given.
ErrAPIKey = errors.New("provide an api key")
// ErrJSONFormat is the error returned when the response JSON had an unexpected format.
ErrJSONFormat = errors.New("could not get key from given json")
// ErrParamRange is returned when invalid parameter ranges where given to a method.
// See the method API documentation for further details.
ErrParamRange = errors.New("invalid parameter range")
// ErrHTTPStatus is the error returned when the server's HTTP response status is unexpected.
ErrHTTPStatus = errors.New("unexpected HTTP status")
// ErrHTTPClient is the error returned when a nil http.Client was given.
ErrHTTPClient = errors.New("provide an http client")
)
// A Random defines a Random.org API Client.
type Random struct {
// the api key
apiKey string
// reusable http.Client
client *http.Client
// guards usage
usageMutex sync.Mutex
// usage cache
usage *Usage
}
// NewRandom creates a new Random client with the given apiKey.
func NewRandom(apiKey string, client *http.Client) (*Random, error) {
// check the api key
if apiKey == "" {
return nil, ErrAPIKey
}
if client == nil {
return nil, ErrHTTPClient
}
random := Random{
apiKey: apiKey,
client: client,
}
return &random, nil
}
// baseParams embeds the API key required by every Random.org API method.
type baseParams struct {
APIKey string `json:"apiKey"`
}
// resolveOptions returns the first element of opts, or the zero value of T
// if opts is empty. It backs the "opts ...MethodOptions" pattern used to add
// optional parameters to methods without breaking existing call sites.
func resolveOptions[T any](opts []T) T {
var o T
if len(opts) > 0 {
o = opts[0]
}
return o
}
// Bool returns a pointer to b. It is a convenience for setting Options
// fields that distinguish "not specified" (nil) from an explicit false,
// such as Replacement.
func Bool(b bool) *bool {
return &b
}
// parseAPITime parses a RANDOM.ORG timestamp such as "2013-02-20 17:53:40Z"
// (a space rather than the RFC 3339 "T" separating date and time) into a
// time.Time.
func parseAPITime(s string) (time.Time, error) {
return time.Parse(creationTimeLayout, strings.Replace(s, " ", "T", 1))
}
// jsonRPCRequest is the envelope for every Random.org JSON-RPC 2.0 request.
type jsonRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params any `json:"params"`
ID string `json:"id"`
}
// jsonRPCResponse is the envelope for every Random.org JSON-RPC 2.0 response.
// R is the method-specific shape of a successful result.
type jsonRPCResponse[R any] struct {
Result *R `json:"result"`
Error *APIError `json:"error"`
}
// APIError describes an error returned by the Random.org API itself, as
// opposed to a transport or decoding failure. Use errors.As to retrieve it.
// See https://api.random.org/json-rpc/4/error-codes.
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e *APIError) Error() string {
return fmt.Sprintf(errAPI, e.Code, e.Message)
}
// invokeRequest sends method with params and decodes the JSON-RPC result into R.
func invokeRequest[R any](ctx context.Context, r *Random, method string, params any) (R, error) {
var zero R
// generate request UUID
requestUUID, err := uuid.NewUUID()
if err != nil {
return zero, err
}
// build request body
requestBody := jsonRPCRequest{
JSONRPC: "2.0",
Method: method,
Params: params,
ID: requestUUID.String(),
}
requestBodyJSON, err := json.Marshal(requestBody)
if err != nil {
return zero, err
}
requestBodyReader := bytes.NewReader(requestBodyJSON)
req, err := http.NewRequestWithContext(ctx, "POST", requestEndpoint, requestBodyReader)
if err != nil {
return zero, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := r.client.Do(req)
if err != nil {
return zero, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return zero, ErrHTTPStatus
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return zero, err
}
var responseBody jsonRPCResponse[R]
if err := json.Unmarshal(body, &responseBody); err != nil {
if len(body) > 0 {
err = fmt.Errorf("%s: %w", body, err)
}
return zero, err
}
if responseBody.Error != nil {
return zero, responseBody.Error
}
if responseBody.Result == nil {
return zero, ErrJSONFormat
}
return *responseBody.Result, nil
}