Skip to content

Commit eb403ce

Browse files
committed
Added streaming outputs
1 parent af35c2b commit eb403ce

File tree

14 files changed

+695
-61
lines changed

14 files changed

+695
-61
lines changed

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ This repository contains a generic HTTP client which can be adapted to provide:
1010

1111
Documentation: https://pkg.go.dev/github.com/mutablelogic/go-client/pkg/client
1212

13-
There are also some example API clients:
13+
There are also some example clients which use this library:
1414

15-
* [Bitwarden Client](https://github.com/mutablelogic/go-client/tree/main/pkg/bitwarden)
16-
* [Elevenlabs Client](https://github.com/mutablelogic/go-client/tree/main/pkg/elevenlabs)
17-
* [Home Assistant Client](https://github.com/mutablelogic/go-client/tree/main/pkg/homeassistant)
15+
* [Bitwarden API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/bitwarden)
16+
* [Elevenlabs API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/elevenlabs)
17+
* [Home Assistant API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/homeassistant)
1818
* [IPify Client](https://github.com/mutablelogic/go-client/tree/main/pkg/ipify)
1919
* [Mistral API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/mistral)
2020
* [NewsAPI client](https://github.com/mutablelogic/go-client/tree/main/pkg/newsapi)
21-
* [OpenAI client](https://github.com/mutablelogic/go-client/tree/main/pkg/openai)
21+
* [Ollama API client](https://github.com/mutablelogic/go-client/tree/main/pkg/ollama)
22+
* [OpenAI API client](https://github.com/mutablelogic/go-client/tree/main/pkg/openai)
2223

2324
## Basic Usage
2425

@@ -29,7 +30,7 @@ to a JSON endpoint:
2930
package main
3031

3132
import (
32-
client "github.com/mutablelogic/go-client"
33+
client "github.com/mutablelogic/go-client/pkg/client"
3334
)
3435

3536
func main() {
@@ -82,7 +83,7 @@ For example,
8283
package main
8384

8485
import (
85-
client "github.com/mutablelogic/go-client"
86+
client "github.com/mutablelogic/go-client/pkg/client"
8687
)
8788

8889
func main() {
@@ -155,7 +156,7 @@ The authentication token can be set as follows:
155156
package main
156157

157158
import (
158-
client "github.com/mutablelogic/go-client"
159+
client "github.com/mutablelogic/go-client/pkg/client"
159160
)
160161

161162
func main() {

etc/test/IMG_20130413_095348.JPG

136 KB
Loading
File renamed without changes.

pkg/client/client.go

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"encoding/xml"
7+
"errors"
78
"fmt"
89
"io"
910
"mime"
@@ -43,7 +44,6 @@ type Client struct {
4344
}
4445

4546
type ClientOpt func(*Client) error
46-
type RequestOpt func(*http.Request) error
4747

4848
///////////////////////////////////////////////////////////////////////////////
4949
// GLOBALS
@@ -54,6 +54,7 @@ const (
5454
PathSeparator = string(os.PathSeparator)
5555
ContentTypeAny = "*/*"
5656
ContentTypeJson = "application/json"
57+
ContentTypeJsonStream = "application/x-ndjson"
5758
ContentTypeTextXml = "text/xml"
5859
ContentTypeApplicationXml = "application/xml"
5960
ContentTypeTextPlain = "text/plain"
@@ -124,12 +125,12 @@ func (client *Client) DoWithContext(ctx context.Context, in Payload, out any, op
124125
now := time.Now()
125126
if !client.ts.IsZero() && client.rate > 0.0 {
126127
next := client.ts.Add(time.Duration(float32(time.Second) / client.rate))
127-
if next.After(now) {
128+
if next.After(now) { // TODO allow ctx to cancel the sleep
128129
time.Sleep(next.Sub(now))
129130
}
130131
}
131132

132-
// Set timestamp at return
133+
// Set timestamp at return, for rate limiting
133134
defer func(now time.Time) {
134135
client.ts = now
135136
}(now)
@@ -164,7 +165,7 @@ func (client *Client) Request(req *http.Request, out any, opts ...RequestOpt) er
164165
now := time.Now()
165166
if !client.ts.IsZero() && client.rate > 0.0 {
166167
next := client.ts.Add(time.Duration(float32(time.Second) / client.rate))
167-
if next.After(now) {
168+
if next.After(now) { // TODO allow ctx to cancel the sleep
168169
time.Sleep(next.Sub(now))
169170
}
170171
}
@@ -235,12 +236,23 @@ func (client *Client) request(ctx context.Context, method, accept, mimetype stri
235236
// Do will make a JSON request, populate an object with the response and return any errors
236237
func do(client *http.Client, req *http.Request, accept string, strict bool, out any, opts ...RequestOpt) error {
237238
// Apply request options
239+
reqopts := requestOpts{
240+
Request: req,
241+
}
238242
for _, opt := range opts {
239-
if err := opt(req); err != nil {
243+
if err := opt(&reqopts); err != nil {
240244
return err
241245
}
242246
}
243247

248+
// NoTimeout
249+
if reqopts.noTimeout {
250+
defer func(v time.Duration) {
251+
client.Timeout = v
252+
}(client.Timeout)
253+
client.Timeout = 0
254+
}
255+
244256
// Do the request
245257
response, err := client.Do(req)
246258
if err != nil {
@@ -276,16 +288,31 @@ func do(client *http.Client, req *http.Request, accept string, strict bool, out
276288
return nil
277289
}
278290

279-
// Decode the body
291+
// Decode the body - and call any callback once the body has been decoded
280292
switch mimetype {
281-
case ContentTypeJson:
282-
if err := json.NewDecoder(response.Body).Decode(out); err != nil {
283-
return err
293+
case ContentTypeJson, ContentTypeJsonStream:
294+
dec := json.NewDecoder(response.Body)
295+
for {
296+
if err := dec.Decode(out); errors.Is(err, io.EOF) {
297+
break
298+
} else if err != nil {
299+
return err
300+
}
301+
if reqopts.callback != nil {
302+
if err := reqopts.callback(); err != nil {
303+
return err
304+
}
305+
}
284306
}
285307
case ContentTypeTextXml, ContentTypeApplicationXml:
286308
if err := xml.NewDecoder(response.Body).Decode(out); err != nil {
287309
return err
288310
}
311+
if reqopts.callback != nil {
312+
if err := reqopts.callback(); err != nil {
313+
return err
314+
}
315+
}
289316
default:
290317
if v, ok := out.(Unmarshaler); ok {
291318
return v.Unmarshal(mimetype, response.Body)
@@ -296,6 +323,11 @@ func do(client *http.Client, req *http.Request, accept string, strict bool, out
296323
} else {
297324
return ErrInternalAppError.Withf("do: response does not implement Unmarshaler for %q", mimetype)
298325
}
326+
if reqopts.callback != nil {
327+
if err := reqopts.callback(); err != nil {
328+
return err
329+
}
330+
}
299331
}
300332

301333
// Return success

pkg/client/doc.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
/*
2-
client impleemts a generic REST API client which can be used for creating
3-
gateway-specific clients
2+
Implements a generic REST API client which can be used for creating
3+
gateway-specific clients. Basic usage:
4+
5+
package main
6+
7+
import (
8+
client "github.com/mutablelogic/go-client/pkg/client"
9+
)
10+
11+
func main() {
12+
// Create a new client
13+
c := client.New(client.OptEndpoint("https://api.example.com/api/v1"))
14+
15+
// Send a GET request, populating a struct with the response
16+
var response struct {
17+
Message string `json:"message"`
18+
}
19+
if err := c.Do(nil, &response, OptPath("test")); err != nil {
20+
// Handle error
21+
}
22+
23+
// Print the response
24+
fmt.Println(response.Message)
25+
}
426
*/
527
package client

pkg/client/payload.go

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
///////////////////////////////////////////////////////////////////////////////
1414
// TYPES
1515

16-
type Request struct {
16+
type request struct {
1717
method string
1818
accept string
1919
mimetype string
@@ -31,20 +31,30 @@ type Payload interface {
3131
///////////////////////////////////////////////////////////////////////////////
3232
// LIFECYCLE
3333

34-
// Return a new empty request which defaults to GET. The accept parameter is the
35-
// accepted mime-type of the response.
36-
func NewRequest(accept string) *Request {
37-
this := new(Request)
38-
this.method = http.MethodGet
34+
// Return a new empty request which defaults to GET
35+
func NewRequest() Payload {
36+
return NewRequestEx(http.MethodGet, ContentTypeAny)
37+
}
38+
39+
// Return a new empty request. The accept parameter is the accepted mime-type
40+
// of the response.
41+
func NewRequestEx(method, accept string) Payload {
42+
this := new(request)
43+
this.method = method
3944
this.accept = accept
4045
return this
4146
}
4247

43-
// Return a new request with a JSON payload which defaults to GET. The accept
48+
// Return a new request with a JSON payload which defaults to POST.
49+
func NewJSONRequest(payload any) (Payload, error) {
50+
return NewJSONRequestEx(http.MethodPost, payload, ContentTypeAny)
51+
}
52+
53+
// Return a new request with a JSON payload with method. The accept
4454
// parameter is the accepted mime-type of the response.
45-
func NewJSONRequest(payload any, accept string) (*Request, error) {
46-
this := new(Request)
47-
this.method = http.MethodGet
55+
func NewJSONRequestEx(method string, payload any, accept string) (Payload, error) {
56+
this := new(request)
57+
this.method = method
4858
this.mimetype = ContentTypeJson
4959
this.accept = accept
5060
this.buffer = new(bytes.Buffer)
@@ -56,8 +66,8 @@ func NewJSONRequest(payload any, accept string) (*Request, error) {
5666

5767
// Return a new request with a Multipart Form data payload which defaults to POST. The accept
5868
// parameter is the accepted mime-type of the response.
59-
func NewMultipartRequest(payload any, accept string) (*Request, error) {
60-
this := new(Request)
69+
func NewMultipartRequest(payload any, accept string) (Payload, error) {
70+
this := new(request)
6171
this.method = http.MethodPost
6272
this.accept = accept
6373
this.buffer = new(bytes.Buffer)
@@ -77,8 +87,8 @@ func NewMultipartRequest(payload any, accept string) (*Request, error) {
7787

7888
// Return a new request with a Form data payload which defaults to POST. The accept
7989
// parameter is the accepted mime-type of the response.
80-
func NewFormRequest(payload any, accept string) (*Request, error) {
81-
this := new(Request)
90+
func NewFormRequest(payload any, accept string) (Payload, error) {
91+
this := new(request)
8292
this.method = http.MethodPost
8393
this.accept = accept
8494
this.buffer = new(bytes.Buffer)
@@ -99,52 +109,44 @@ func NewFormRequest(payload any, accept string) (*Request, error) {
99109
///////////////////////////////////////////////////////////////////////////////
100110
// STRINGIFY
101111

102-
func (req *Request) String() string {
112+
func (req *request) String() string {
103113
str := "<payload"
104114
if req.method != "" {
105-
str += " method=" + strconv.Quote(req.method)
115+
str += " method=" + strconv.Quote(req.Method())
106116
}
107117
if req.accept != "" {
108-
str += " accept=" + strconv.Quote(req.accept)
118+
str += " accept=" + strconv.Quote(req.Accept())
109119
}
110120
if req.mimetype != "" {
111-
str += " mimetype=" + strconv.Quote(req.mimetype)
121+
str += " mimetype=" + strconv.Quote(req.Type())
112122
}
113123
return str + ">"
114124
}
115125

116126
///////////////////////////////////////////////////////////////////////////////
117127
// PAYLOAD METHODS
118128

119-
// Set the HTTP method to POST
120-
func (req *Request) Post() *Request {
121-
req.method = http.MethodPost
122-
return req
123-
}
124-
125-
// Set the HTTP method to DELETE
126-
func (req *Request) Delete() *Request {
127-
req.method = http.MethodDelete
128-
return req
129-
}
130-
131129
// Return the HTTP method
132-
func (req *Request) Method() string {
130+
func (req *request) Method() string {
133131
return req.method
134132
}
135133

136134
// Set the request mimetype
137-
func (req *Request) Type() string {
135+
func (req *request) Type() string {
138136
return req.mimetype
139137
}
140138

141139
// Return the acceptable mimetype responses
142-
func (req *Request) Accept() string {
143-
return req.accept
140+
func (req *request) Accept() string {
141+
if req.accept == "" {
142+
return ContentTypeAny
143+
} else {
144+
return req.accept
145+
}
144146
}
145147

146148
// Implements the io.Reader interface for a payload
147-
func (req *Request) Read(b []byte) (n int, err error) {
149+
func (req *request) Read(b []byte) (n int, err error) {
148150
if req.buffer != nil {
149151
return req.buffer.Read(b)
150152
} else {

0 commit comments

Comments
 (0)