Skip to content

Commit 2bd1ac3

Browse files
committed
Расширение опций и рефакторинг:
* Больше опций настройки клиента * Добавлен типа DateOnly, т.к. приходится повторять везде * Немного переработан uncompress тела ответа * пр. мелкий рефакторинг
1 parent 433d6e4 commit 2bd1ac3

File tree

4 files changed

+199
-111
lines changed

4 files changed

+199
-111
lines changed

apiClient.go

Lines changed: 53 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,39 @@
11
package megaplan
22

33
import (
4-
"bytes"
5-
"compress/gzip"
6-
"context"
74
"crypto/tls"
8-
"errors"
9-
"io"
105
"net/http"
6+
"net/http/cookiejar"
7+
"net/url"
118
"runtime"
129
"strconv"
10+
"strings"
1311
"time"
1412
)
1513

1614
// DefaultClient - клиент по умаолчанию для API.
1715
var (
18-
cpus = runtime.NumCPU()
16+
cpus = runtime.NumCPU()
17+
DefaultTransport = &http.Transport{
18+
Proxy: http.ProxyFromEnvironment,
19+
MaxIdleConns: cpus,
20+
MaxConnsPerHost: cpus,
21+
MaxIdleConnsPerHost: cpus,
22+
}
1923
DefaultClient = &http.Client{
20-
Transport: &http.Transport{
21-
Proxy: http.ProxyFromEnvironment,
22-
MaxIdleConns: cpus,
23-
MaxConnsPerHost: cpus,
24-
MaxIdleConnsPerHost: cpus,
25-
},
26-
Timeout: time.Minute,
24+
Transport: DefaultTransport,
25+
Timeout: time.Minute,
2726
}
2827
// DefaultHeaders - заголовок по умолчанию - версия go. Используется при инициализации клиента в NewClient.
2928
DefaultHeaders = http.Header{"User-Agent": {runtime.Version()}}
3029
)
3130

3231
// NewClient - обертка над http.Client для удобной работы с API v3
3332
func NewClient(domain, token string, opts ...ClientOption) (c *ClientV3) {
33+
if strings.HasPrefix("http", domain) {
34+
_URL, _ := url.Parse(domain)
35+
domain = _URL.Host
36+
}
3437
c = &ClientV3{
3538
client: DefaultClient,
3639
domain: domain,
@@ -48,91 +51,7 @@ type ClientV3 struct {
4851
client *http.Client
4952
}
5053

51-
// Do - http.Do + установка обязательных заголовков + декомпрессия ответа, если ответ сжат
52-
func (c *ClientV3) Do(req *http.Request) (*http.Response, error) {
53-
const ct = "Content-Type"
54-
for h := range c.defaultHeaders {
55-
req.Header.Set(h, c.defaultHeaders.Get(h))
56-
}
57-
if _, ok := req.Header[ct]; !ok {
58-
req.Header.Set(ct, "application/json")
59-
}
60-
res, err := c.client.Do(req)
61-
if err != nil {
62-
return nil, err
63-
}
64-
if err := unzipResponse(res); err != nil {
65-
return nil, err
66-
}
67-
return res, nil
68-
}
69-
70-
// DoRequestAPI - т.к. в v3 параметры запроса для GET (json маршализируется и будет иметь вид: "*?{params}=")
71-
func (c ClientV3) DoRequestAPI(method string, endpoint string, search QueryParams, body io.Reader) (*http.Response, error) {
72-
var args string // параметры строки запроса
73-
if search != nil {
74-
args = search.QueryEscape()
75-
}
76-
request, err := http.NewRequest(method, c.domain, body)
77-
if err != nil {
78-
return nil, err
79-
}
80-
request.URL.Path = endpoint
81-
request.URL.RawQuery = args
82-
return c.Do(request)
83-
}
84-
85-
// DoRequestAPI - т.к. в v3 параметры запроса для GET (json маршализируется и будет иметь вид: "*?{params}=")
86-
func (c ClientV3) DoCtxRequestAPI(ctx context.Context, method string, endpoint string, search QueryParams, body io.Reader) (*http.Response, error) {
87-
var args string // параметры строки запроса
88-
if search != nil {
89-
args = search.QueryEscape()
90-
}
91-
request, err := http.NewRequestWithContext(ctx, method, c.domain, body)
92-
if err != nil {
93-
return nil, err
94-
}
95-
request.URL.Path = endpoint
96-
request.URL.RawQuery = args
97-
return c.Do(request)
98-
}
99-
100-
// ErrUnknownCompressionMethod - неизвестное значение в заголовке "Content-Encoding"
101-
// не является фатальной ошибкой, должна возвращаться вместе с http.Response.Body,
102-
// чтобы пользователь мог реализовать свой метод обработки сжатого сообщения
103-
var ErrUnknownCompressionMethod = errors.New("unknown compression method")
104-
105-
// unzipResponse - распаковка сжатого ответа
106-
func unzipResponse(response *http.Response) (err error) {
107-
if response.Uncompressed {
108-
return nil
109-
}
110-
switch response.Header.Get("Content-Encoding") {
111-
case "":
112-
return nil
113-
case "gzip":
114-
gz, err := gzip.NewReader(response.Body)
115-
if err != nil {
116-
return err
117-
}
118-
b, err := io.ReadAll(gz)
119-
if err != nil {
120-
return err
121-
}
122-
if err := response.Body.Close(); err != nil {
123-
return err
124-
}
125-
if err := gz.Close(); err != nil {
126-
return err
127-
}
128-
response.Body = io.NopCloser(bytes.NewReader(b))
129-
response.Header.Del("Content-Encoding")
130-
response.Uncompressed = true
131-
return nil
132-
default:
133-
return ErrUnknownCompressionMethod
134-
}
135-
}
54+
func (c *ClientV3) Close() { c.client.CloseIdleConnections() }
13655

13756
// SetOptions - применить опции
13857
func (c *ClientV3) SetOptions(opts ...ClientOption) {
@@ -149,13 +68,13 @@ type ClientOption func(*ClientV3)
14968

15069
// OptionInsecureSkipVerify - переключение флага bool в http.Client.Transport.TLSClientConfig.InsecureSkipVerify - отключать или нет проверку сертификтов
15170
// Если домен использует самоподписанные сертифика, то удобно включать на время отладки и разработки
152-
func OptionInsecureSkipVerify(b bool) ClientOption {
71+
func OptionInsecureSkipVerify(yes bool) ClientOption {
15372
return func(c *ClientV3) {
15473
if c.client.Transport != nil {
15574
if (c.client.Transport.(*http.Transport)).TLSClientConfig == nil {
156-
(c.client.Transport.(*http.Transport)).TLSClientConfig = &tls.Config{InsecureSkipVerify: b}
75+
(c.client.Transport.(*http.Transport)).TLSClientConfig = &tls.Config{InsecureSkipVerify: yes}
15776
} else {
158-
(c.client.Transport.(*http.Transport)).TLSClientConfig.InsecureSkipVerify = b
77+
(c.client.Transport.(*http.Transport)).TLSClientConfig.InsecureSkipVerify = yes
15978
}
16079
}
16180
}
@@ -168,10 +87,10 @@ func OptionsSetHTTPTransport(tr http.RoundTripper) ClientOption {
16887

16988
// OptionEnableAcceptEncodingGzip - доабвить заголов Accept-Encoding=gzip к запросу
17089
// т.е. объекм трафика на хуках может быть большим, то удобно запрашивать сжатый ответ
171-
func OptionEnableAcceptEncodingGzip(b bool) ClientOption {
90+
func OptionEnableAcceptEncodingGzip(yes bool) ClientOption {
17291
const header = "Accept-Encoding"
17392
return func(c *ClientV3) {
174-
if b {
93+
if yes {
17594
c.defaultHeaders.Set(header, "gzip")
17695
} else {
17796
c.defaultHeaders.Del(header)
@@ -200,3 +119,34 @@ func OptionSetXUserID(userID int) ClientOption {
200119
}
201120
}
202121
}
122+
123+
func OptionDisableCookie(yes bool) ClientOption {
124+
return func(c *ClientV3) {
125+
if yes {
126+
c.client.Jar = nil
127+
} else {
128+
jar, _ := cookiejar.New(nil)
129+
c.client.Jar = jar
130+
}
131+
}
132+
}
133+
134+
func OptionForceAttemptHTTP2(yes bool) ClientOption {
135+
return func(c *ClientV3) {
136+
if c.client.Transport != nil {
137+
(c.client.Transport.(*http.Transport)).ForceAttemptHTTP2 = yes
138+
} else {
139+
c.client.Transport = &http.Transport{ForceAttemptHTTP2: yes}
140+
}
141+
}
142+
}
143+
144+
func OptionDisablekeepAlive(yes bool) ClientOption {
145+
return func(c *ClientV3) {
146+
if c.client.Transport != nil {
147+
(c.client.Transport.(*http.Transport)).DisableKeepAlives = yes
148+
} else {
149+
c.client.Transport = &http.Transport{DisableKeepAlives: yes}
150+
}
151+
}
152+
}

apiQueryParams.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import (
55
"encoding/json"
66
"io"
77
"net/url"
8+
"time"
89
)
910

11+
// ISO8601 - формат даты для api
12+
const ISO8601 = `2006-01-02T15:04:05-07:00`
13+
1014
// QueryParams - параметры запроса
1115
type QueryParams map[string]any
1216

@@ -34,3 +38,22 @@ func (qp QueryParams) PrettyPrintJSON(w io.Writer) error {
3438
enc.SetIndent("", " ")
3539
return enc.Encode(qp)
3640
}
41+
42+
type DateOnly struct {
43+
ContentType string `json:"contentType"`
44+
Day int `json:"day"`
45+
Month int `json:"month"`
46+
Year int `json:"year"`
47+
}
48+
49+
func (d DateOnly) ToTime() time.Time {
50+
return time.Date(d.Year, time.Month(d.Month)+1, d.Day, 0, 0, 0, 0, time.UTC)
51+
}
52+
func NewDateOnly(t time.Time) DateOnly {
53+
return DateOnly{
54+
ContentType: typeDateOnly,
55+
Day: t.Day(),
56+
Month: int(t.Month()) - 1,
57+
Year: t.Year(),
58+
}
59+
}

apiRequest.go

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@ package megaplan
22

33
import (
44
"bytes"
5+
"context"
56
"io"
67
"mime/multipart"
78
"net/http"
9+
"net/url"
810
"time"
911
)
1012

11-
// ISO8601 - формат даты для api
12-
const ISO8601 = `2006-01-02T15:04:05-07:00`
13+
const (
14+
headerContentType = "Content-Type"
15+
valueApplicationJSON = "application/json"
16+
)
17+
18+
const (
19+
typeDateOnly = "DateOnly"
20+
typeDateTime = "DateTime"
21+
typeDateInterval = "DateInterval"
22+
)
1323

1424
// BuildQueryParams - сборка объекта для запроса
1525
func BuildQueryParams(opts ...QueryBuildingFunc) (qp QueryParams) {
@@ -28,23 +38,22 @@ type QueryBuildingFunc func(QueryParams)
2838
func CreateEnity(contentType string, value any) (qp QueryParams) {
2939
qp = make(QueryParams, 2)
3040
qp["contentType"] = contentType
31-
3241
switch contentType {
33-
case "DateOnly":
42+
case typeDateOnly:
3443
t, isTime := value.(time.Time)
3544
if !isTime {
3645
return nil
3746
}
3847
qp["year"] = t.Year()
3948
qp["month"] = t.Month() - 1
4049
qp["day"] = t.Day()
41-
case "DateTime":
50+
case typeDateTime:
4251
t, isTime := value.(time.Time)
4352
if !isTime {
4453
return nil
4554
}
4655
qp["value"] = t.Format(ISO8601)
47-
case "DateInterval":
56+
case typeDateInterval:
4857
// если передается не время, то должно указываться кол-во секунд (актуальная документация мегаплана пишет что миллисекунды - это ошибка)
4958
switch v := value.(type) {
5059
case time.Time:
@@ -106,6 +115,72 @@ func (c *ClientV3) UploadFile(filename string, fileReader io.Reader) (*http.Resp
106115
return nil, err
107116
}
108117
request.URL.Path = "/api/file"
109-
request.Header.Set("Content-Type", mw.FormDataContentType())
118+
request.Header.Set(headerContentType, mw.FormDataContentType())
119+
return c.Do(request)
120+
}
121+
122+
// Do - http.Do + установка обязательных заголовков + декомпрессия ответа, если ответ сжат
123+
func (c *ClientV3) Do(req *http.Request) (*http.Response, error) {
124+
for h := range c.defaultHeaders.Clone() {
125+
req.Header.Set(h, c.defaultHeaders.Get(h))
126+
}
127+
if _, ok := req.Header[headerContentType]; !ok {
128+
req.Header.Set(headerContentType, valueApplicationJSON)
129+
}
130+
res, err := c.client.Do(req)
131+
if err != nil {
132+
return nil, err
133+
}
134+
// slog.Debug("response",
135+
// slog.String("proto", res.Proto),
136+
// slog.Int("status", res.StatusCode),
137+
// slog.String("connection", res.Header.Get("connection")),
138+
// )
139+
if err := unzipResponse(res); err != nil {
140+
return nil, err
141+
}
142+
return res, nil
143+
}
144+
145+
func (c ClientV3) makeRequestURL(endpoint string, search QueryParams) string {
146+
var args string // параметры строки запроса
147+
if search != nil {
148+
args = search.QueryEscape()
149+
}
150+
return (&url.URL{
151+
Scheme: "https",
152+
Host: c.domain,
153+
Path: endpoint,
154+
RawQuery: args,
155+
}).String()
156+
}
157+
158+
// DoRequestAPI - т.к. в v3 параметры запроса для GET (json маршализируется и будет иметь вид: "*?{params}=")
159+
func (c ClientV3) DoRequestAPI(method, endpoint string, search QueryParams, body io.Reader) (*http.Response, error) {
160+
request, err := http.NewRequest(
161+
method,
162+
c.makeRequestURL(endpoint, search),
163+
body)
164+
if err != nil {
165+
return nil, err
166+
}
167+
// slog.Debug("DoRequestAPI.NewRequest",
168+
// slog.String("method", method),
169+
// slog.String("endpoint", request.URL.String()))
170+
return c.Do(request)
171+
}
172+
173+
// DoRequestAPI - т.к. в v3 параметры запроса для GET (json маршализируется и будет иметь вид: "*?{params}=")
174+
func (c ClientV3) DoCtxRequestAPI(ctx context.Context, method, endpoint string, search QueryParams, body io.Reader) (*http.Response, error) {
175+
request, err := http.NewRequestWithContext(ctx,
176+
method,
177+
c.makeRequestURL(endpoint, search),
178+
body)
179+
if err != nil {
180+
return nil, err
181+
}
182+
// slog.Debug("DoCtxRequestAPI.NewRequestWithContext",
183+
// slog.String("method", method),
184+
// slog.String("endpoint", request.URL.String()))
110185
return c.Do(request)
111186
}

0 commit comments

Comments
 (0)