Skip to content

Commit 433d6e4

Browse files
committed
Небольшое изменение:
* изменен типа megaplan.Response для удобства работы через дженерики * функция ParseResponse изменена, теперь она работае с новой реализацией типа megaplan.Response[T] * версия go понижена до 1.18, где появились дженерики
1 parent b1a8a17 commit 433d6e4

File tree

6 files changed

+139
-60
lines changed

6 files changed

+139
-60
lines changed

README.md

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Пример использования
22

3-
Иниализация клиента + опция включения заголовка "Accept-Encoding":"gzip", ответ будет возвращаться сжатым:
3+
Иниализация клиента + опция включения заголовка "Accept-Encoding":"gzip", ответ будет возвращаться сжатым, реализована декомпрессия тела ответа внутри вызова.
44

5+
```golang
56
import (
67
"github.com/stvoidit/megaplan/v3"
78
)
@@ -12,42 +13,44 @@
1213
func main() {
1314
client := megaplan.NewClient(domain, token, megaplan.OptionEnableAcceptEncodingGzip(true))
1415
}
15-
16+
```
1617
## Пример создания задачами
1718
https://demo.megaplan.ru/api/v3/docs#entityTask
18-
Для удобства составления json для тела запроса есть функция __megaplan.BuildQueryParams__. Её единственное название - собрать параметры в правильном формате.
19+
Для удобства составления json для тела запроса есть функция __megaplan.BuildQueryParams__. Её единственное назначение - собрать параметры в правильном формате.
1920
Некоторые сущности требуют специального формата (например [Дата и Время](https://demo.megaplan.ru/api/v3/docs#entityDateTime), [Интервал](https://demo.megaplan.ru/api/v3/docs#entityDateInterval), [Дата](https://demo.megaplan.ru/api/v3/docs#entityDateOnly), [~~Сдвиг дат~~](https://demo.megaplan.ru/api/v3/docs#entityShiftDate)), то функция __megaplan.BuildQueryParams__ корректно сформирует структуру этих сущностей.
2021

21-
func CreateTask(c *megaplan.ClientV3) {
22-
const endpoint = "/api/v3/task"
23-
var qp = megaplan.BuildQueryParams(
24-
megaplan.SetRawField("contentType", "Task"),
25-
megaplan.SetRawField("isUrgent", false),
26-
megaplan.SetRawField("isTemplate", false),
27-
megaplan.SetRawField("name", "library test"),
28-
megaplan.SetRawField("subject", "subject library test"),
29-
megaplan.SetRawField("statement", "statement library test"),
30-
megaplan.SetEntityField("owner", "Employee", 1000129),
31-
megaplan.SetEntityField("responsible", "Employee", 1000129),
32-
megaplan.SetEntityField("deadline", "DateOnly", time.Now().Add(time.Hour*72)),
33-
megaplan.SetEntityField("plannedWork", "DateInterval", time.Hour*13),
34-
)
35-
r, err := qp.ToReader()
36-
if err != nil {
37-
panic(err)
38-
}
39-
rc, err := c.DoRequestAPI(http.MethodPost, endpoint, nil, r)
40-
if err != nil {
41-
panic(err)
42-
}
43-
defer rc.Close()
44-
os.Stdout.ReadFrom(rc)
45-
}
4622

23+
```golang
24+
func CreateTask(c *megaplan.ClientV3) {
25+
const endpoint = "/api/v3/task"
26+
var qp = megaplan.BuildQueryParams(
27+
megaplan.SetRawField("contentType", "Task"),
28+
megaplan.SetRawField("isUrgent", false),
29+
megaplan.SetRawField("isTemplate", false),
30+
megaplan.SetRawField("name", "library test"),
31+
megaplan.SetRawField("subject", "subject library test"),
32+
megaplan.SetRawField("statement", "statement library test"),
33+
megaplan.SetEntityField("owner", "Employee", 1000129),
34+
megaplan.SetEntityField("responsible", "Employee", 1000129),
35+
megaplan.SetEntityField("deadline", "DateOnly", time.Now().Add(time.Hour*72)),
36+
megaplan.SetEntityField("plannedWork", "DateInterval", time.Hour*13),
37+
)
38+
r, err := qp.ToReader()
39+
if err != nil {
40+
panic(err)
41+
}
42+
rc, err := c.DoRequestAPI(http.MethodPost, endpoint, nil, r)
43+
if err != nil {
44+
panic(err)
45+
}
46+
defer rc.Close()
47+
os.Stdout.ReadFrom(rc)
48+
}
49+
```
4750
## Пример запроса с параметрами URL
4851
Так как параметры запроса на api "Мегаплан" передаются в нетипичном формате ("*?json=?"), то необходимо их экранировать через url.QueryEscape.
4952
Для удобства составления этих параметров можно так же использовать тип __megaplan.QueryParams__.
50-
53+
```golang
5154
func testGetWithFilters(c *megaplan.ClientV3) {
5255
const endpoint = "/api/v3/task"
5356
var requestedFiled = [...]string{
@@ -141,9 +144,78 @@ https://demo.megaplan.ru/api/v3/docs#entityTask
141144
os.Stdout.ReadFrom(response.Body)
142145
}
143146
}
147+
```
148+
149+
## Чтение ответа
150+
С появлением дженериков улучшена функция для чтения ответов от api.
151+
Внутри функции есть проверка на Content-Type, если это не json, то в 99% это html с ошибкой. В этом случае функция вернет ошибку с текстом в виде html строки.
152+
153+
154+
```golang
155+
package main
156+
157+
import (
158+
"encoding/json"
159+
"fmt"
160+
"net/http"
161+
"strings"
162+
163+
"github.com/stvoidit/megaplan/v3"
164+
)
165+
166+
const (
167+
DOMAIN = `https://example.ru`
168+
TOKEN = `TOKEN`
169+
ACCOUNTINFO = `/api/v3/accountInfo`
170+
)
171+
172+
type AccountInfo struct {
173+
ID string `json:"id"`
174+
ContentType string `json:"contentType"`
175+
PermanentHost string `json:"permanentHost"`
176+
AccountName string `json:"accountName"`
177+
BuildVersion string `json:"buildVersion"`
178+
SystemProductName string `json:"systemProductName"`
179+
TarifId string `json:"tarifId"`
180+
LicenceEndDate map[string]any `json:"licenceEndDate"`
181+
MobileEndDate map[string]any `json:"mobileEndDate"`
182+
PaidToDate map[string]any `json:"paidToDate"`
183+
LicenseExpired bool `json:"licenseExpired"`
184+
MegamailDomain string `json:"megamailDomain"`
185+
DaysRemaining int `json:"daysRemaining"`
186+
TimeCreated string `json:"timeCreated"`
187+
}
188+
189+
func (ai AccountInfo) String() string {
190+
var sb strings.Builder
191+
e := json.NewEncoder(&sb)
192+
e.SetIndent("", " ")
193+
e.Encode(&ai)
194+
return sb.String()
195+
}
196+
197+
func main() {
198+
c := megaplan.NewClient(DOMAIN, TOKEN,
199+
megaplan.OptionEnableAcceptEncodingGzip(true),
200+
megaplan.OptionInsecureSkipVerify(true))
201+
res, err := c.DoRequestAPI(http.MethodGet, ACCOUNTINFO, nil, nil)
202+
if err != nil {
203+
panic(err)
204+
}
205+
// Вы можете указать типа как "any", если вам нужно стандартное поведение json.Decode - возврат в виде map[string]any
206+
body, err := megaplan.ParseResponse[AccountInfo](res)
207+
if err != nil {
208+
panic(err)
209+
}
210+
fmt.Println(body)
211+
}
212+
```
213+
144214

145215
## __!__ Про типы и сущности "мегаплана" __!__
146216

217+
\* _не актуально с появлением дженериков_
218+
147219
Многие реализации библиотек для API "Мегаплана" пытаются строго типизировать и описать полностью сущности, которыми оперирует "Мегаплан".
148220
Однако это подход влечет за собой обязанность этих библиотек поддерживать согласованность с версиями "Мегаплана", а так же каким-то образом поддерживать кастомные варианты полей.
149221
Данная библиотека является просто оберткой для использования API v3 и включает минимальное кол-во вспомогательных функций для составления запросов и парсинга ответов.

apiClient.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ func NewClient(domain, token string, opts ...ClientOption) (c *ClientV3) {
4343

4444
// ClientV3 - клиент
4545
type ClientV3 struct {
46-
client *http.Client
4746
domain string
4847
defaultHeaders http.Header
48+
client *http.Client
4949
}
5050

5151
// Do - http.Do + установка обязательных заголовков + декомпрессия ответа, если ответ сжат
@@ -54,7 +54,7 @@ func (c *ClientV3) Do(req *http.Request) (*http.Response, error) {
5454
for h := range c.defaultHeaders {
5555
req.Header.Set(h, c.defaultHeaders.Get(h))
5656
}
57-
if req.Header.Get(ct) == "" {
57+
if _, ok := req.Header[ct]; !ok {
5858
req.Header.Set(ct, "application/json")
5959
}
6060
res, err := c.client.Do(req)

apiQueryParams.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import (
88
)
99

1010
// QueryParams - параметры запроса
11-
type QueryParams map[string]interface{}
11+
type QueryParams map[string]any
1212

13-
// QueryEscape - urlencode для запроса
13+
// QueryEscape - urlencode для запроса в строке параметрво
1414
func (qp QueryParams) QueryEscape() string {
1515
b, _ := qp.ToJSON()
1616
return url.QueryEscape(string(b))
@@ -19,7 +19,7 @@ func (qp QueryParams) QueryEscape() string {
1919
// ToJSON - маршализация параметров в JSON
2020
func (qp QueryParams) ToJSON() ([]byte, error) { return json.Marshal(&qp) }
2121

22-
// ToReader - преобразование с JSON и io.Reader для удобства записи в http.Request
22+
// ToReader - получить io.Reader для добавление в body часть http.Request
2323
func (qp QueryParams) ToReader() (io.Reader, error) {
2424
b, err := qp.ToJSON()
2525
if err != nil {
@@ -31,6 +31,6 @@ func (qp QueryParams) ToReader() (io.Reader, error) {
3131
// PrettyPrintJSON - SetIndent для читабельного вывода
3232
func (qp QueryParams) PrettyPrintJSON(w io.Writer) error {
3333
enc := json.NewEncoder(w)
34-
enc.SetIndent("", "\t")
34+
enc.SetIndent("", " ")
3535
return enc.Encode(qp)
3636
}

apiRequest.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type QueryBuildingFunc func(QueryParams)
2525

2626
// CreateEnity - создать базовую сущность в формате "Мегаплана"
2727
// ! могут быть не описаны крайние или редкоиспользуемые типы
28-
func CreateEnity(contentType string, value interface{}) (qp QueryParams) {
28+
func CreateEnity(contentType string, value any) (qp QueryParams) {
2929
qp = make(QueryParams, 2)
3030
qp["contentType"] = contentType
3131

@@ -62,7 +62,7 @@ func CreateEnity(contentType string, value interface{}) (qp QueryParams) {
6262
}
6363

6464
// SetEntityField - добавить поле с сущностью
65-
func SetEntityField(fieldName string, contentType string, value interface{}) (qbf QueryBuildingFunc) {
65+
func SetEntityField(fieldName string, contentType string, value any) (qbf QueryBuildingFunc) {
6666
return func(qp QueryParams) { qp[fieldName] = CreateEnity(contentType, value) }
6767
}
6868

@@ -72,7 +72,7 @@ func SetEntityArray(field string, ents ...QueryBuildingFunc) QueryBuildingFunc {
7272
if len(ents) == 0 {
7373
return
7474
}
75-
var arr = make([]interface{}, len(ents))
75+
var arr = make([]any, len(ents))
7676
var tmpParams = make(QueryParams)
7777
for i, ent := range ents {
7878
ent(tmpParams)
@@ -83,19 +83,19 @@ func SetEntityArray(field string, ents ...QueryBuildingFunc) QueryBuildingFunc {
8383
}
8484

8585
// SetRawField - добавить поле с простым типом значения (string, int, etc.)
86-
func SetRawField(field string, value interface{}) QueryBuildingFunc {
86+
func SetRawField(field string, value any) QueryBuildingFunc {
8787
return func(qp QueryParams) { qp[field] = value }
8888
}
8989

9090
// UploadFile - загрузка файла, возвращает обычный http.Response, в ответе стандартная структура ответа + данные для базовой сущности
91-
func (c *ClientV3) UploadFile(filename string, fileRader io.Reader) (*http.Response, error) {
91+
func (c *ClientV3) UploadFile(filename string, fileReader io.Reader) (*http.Response, error) {
9292
var buf bytes.Buffer // default 1024 bytes buffer
9393
var mw = multipart.NewWriter(&buf)
9494
fw, err := mw.CreateFormFile("files[]", filename)
9595
if err != nil {
9696
return nil, err
9797
}
98-
if _, err := io.Copy(fw, fileRader); err != nil {
98+
if _, err := io.Copy(fw, fileReader); err != nil {
9999
return nil, err
100100
}
101101
if err := mw.Close(); err != nil {

apiResponse.go

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,26 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"net/http"
910
"strings"
1011
)
1112

1213
// Response - ответ API
13-
type Response struct {
14-
Meta Meta `json:"meta"` // metainfo ответа
15-
Data interface{} `json:"data"` // поле для декодирования присвоенной структуры
14+
type Response[T any] struct {
15+
Meta Meta `json:"meta"` // metainfo ответа
16+
Data T `json:"data"` // поле для декодирования присвоенной структуры
1617
}
1718

1819
// Next - есть ли следующая страница
19-
func (res Response) Next() bool { return res.Meta.Pagination.HasMoreNext }
20+
func (res Response[T]) Next() bool { return res.Meta.Pagination.HasMoreNext }
2021

2122
// Prev - есть ли предыдущая страница
22-
func (res Response) Prev() bool { return res.Meta.Pagination.HasMorePrev }
23+
func (res Response[T]) Prev() bool { return res.Meta.Pagination.HasMorePrev }
2324

2425
// Decode - парсинг ответа API
25-
func (res *Response) Decode(r io.Reader, i interface{}) (err error) {
26-
res.Data = i
27-
if err := json.NewDecoder(r).Decode(res); err != nil {
26+
func (res *Response[T]) Decode(r *http.Response) (err error) {
27+
defer r.Body.Close()
28+
if err := json.NewDecoder(r.Body).Decode(res); err != nil {
2829
return err
2930
}
3031
return res.Meta.Error()
@@ -85,10 +86,10 @@ func (p Pagination) MarshalJSON() ([]byte, error) { return nil, nil }
8586
// Meta - metainfo
8687
type Meta struct {
8788
Errors []struct {
88-
Fields interface{} `json:"field"`
89-
Message interface{} `json:"message"`
89+
Fields any `json:"field"`
90+
Message any `json:"message"`
9091
} `json:"errors"`
91-
Status int64 `json:"status"`
92+
Status int `json:"status"`
9293
Pagination Pagination `json:"pagination"`
9394
}
9495

@@ -104,12 +105,18 @@ func (m Meta) Error() (err error) {
104105
return
105106
}
106107

107-
// ParseResponse - обертка над методов Response.Decode + данные о пагинации
108-
// utility-функция для упрощения чтения ответа API
109-
func ParseResponse(r io.Reader, i interface{}) (next bool, prev bool, err error) {
110-
var res Response
111-
if err := res.Decode(r, i); err != nil {
112-
return res.Next(), res.Prev(), err
108+
// ParseResponse - utility-функция для упрощения чтения ответа API
109+
func ParseResponse[T any](r *http.Response) (res Response[T], err error) {
110+
defer r.Body.Close()
111+
if !strings.Contains(r.Header.Get("Content-Type"), "application/json") {
112+
b, err := io.ReadAll(r.Body)
113+
if err != nil {
114+
return res, err
115+
}
116+
return res, errors.New(string(b))
113117
}
114-
return res.Next(), res.Prev(), nil
118+
e := json.NewDecoder(r.Body)
119+
e.UseNumber()
120+
err = e.Decode(&res)
121+
return
115122
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/stvoidit/megaplan/v3
22

3-
go 1.19
3+
go 1.18

0 commit comments

Comments
 (0)