All checks were successful
CI / Build & Test (push) Successful in 34s
- esa.io API クライアントの実装と単体テストを追加 - 記事の取得・検索・作成・更新を行うサブコマンドを実装 - GitHub Actions による CI ワークフローを設定 - プロジェクトの基本ファイル(README、.gitignore、LICENSE、go.mod)を作成
225 lines
5.7 KiB
Go
225 lines
5.7 KiB
Go
package esa
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const defaultBaseURL = "https://api.esa.io"
|
|
|
|
type Client struct {
|
|
BaseURL string
|
|
Team string
|
|
Token string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
func NewClient(team, token string) *Client {
|
|
return &Client{
|
|
BaseURL: defaultBaseURL,
|
|
Team: team,
|
|
Token: token,
|
|
HTTPClient: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
type User struct {
|
|
Myself bool `json:"myself"`
|
|
Name string `json:"name"`
|
|
ScreenName string `json:"screen_name"`
|
|
Icon string `json:"icon"`
|
|
}
|
|
|
|
type Post struct {
|
|
Number int `json:"number"`
|
|
Name string `json:"name"`
|
|
FullName string `json:"full_name"`
|
|
WIP bool `json:"wip"`
|
|
BodyMD string `json:"body_md"`
|
|
BodyHTML string `json:"body_html"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
Message string `json:"message"`
|
|
URL string `json:"url"`
|
|
Tags []string `json:"tags"`
|
|
Category string `json:"category"`
|
|
RevisionNumber int `json:"revision_number"`
|
|
CreatedBy User `json:"created_by"`
|
|
UpdatedBy User `json:"updated_by"`
|
|
Kind string `json:"kind"`
|
|
CommentsCount int `json:"comments_count"`
|
|
TasksCount int `json:"tasks_count"`
|
|
DoneTasksCount int `json:"done_tasks_count"`
|
|
StargazersCount int `json:"stargazers_count"`
|
|
WatchersCount int `json:"watchers_count"`
|
|
Star bool `json:"star"`
|
|
Watch bool `json:"watch"`
|
|
}
|
|
|
|
type PostsResponse struct {
|
|
Posts []Post `json:"posts"`
|
|
PrevPage *int `json:"prev_page"`
|
|
NextPage *int `json:"next_page"`
|
|
TotalCount int `json:"total_count"`
|
|
Page int `json:"page"`
|
|
PerPage int `json:"per_page"`
|
|
MaxPerPage int `json:"max_per_page"`
|
|
}
|
|
|
|
type PostInput struct {
|
|
Name string `json:"name,omitempty"`
|
|
BodyMD string `json:"body_md,omitempty"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
Category string `json:"category,omitempty"`
|
|
WIP *bool `json:"wip,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type postCreateRequest struct {
|
|
Post PostInput `json:"post"`
|
|
}
|
|
|
|
type APIError struct {
|
|
StatusCode int
|
|
ErrorCode string `json:"error"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
if e.ErrorCode == "" {
|
|
return fmt.Sprintf("esa API error: status=%d", e.StatusCode)
|
|
}
|
|
return fmt.Sprintf("esa API error: status=%d error=%s message=%s", e.StatusCode, e.ErrorCode, e.Message)
|
|
}
|
|
|
|
func (c *Client) do(ctx context.Context, method, path string, query url.Values, body any) ([]byte, error) {
|
|
u, err := url.Parse(c.BaseURL + path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse url: %w", err)
|
|
}
|
|
if len(query) > 0 {
|
|
u.RawQuery = query.Encode()
|
|
}
|
|
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal body: %w", err)
|
|
}
|
|
reqBody = bytes.NewReader(b)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, u.String(), reqBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new request: %w", err)
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+c.Token)
|
|
req.Header.Set("Accept", "application/json")
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("http: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read body: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
apiErr := &APIError{StatusCode: resp.StatusCode}
|
|
_ = json.Unmarshal(respBody, apiErr)
|
|
return nil, apiErr
|
|
}
|
|
|
|
return respBody, nil
|
|
}
|
|
|
|
func (c *Client) GetPost(ctx context.Context, number int) (*Post, error) {
|
|
path := fmt.Sprintf("/v1/teams/%s/posts/%d", url.PathEscape(c.Team), number)
|
|
body, err := c.do(ctx, http.MethodGet, path, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var p Post
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
|
return nil, fmt.Errorf("unmarshal post: %w", err)
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
type SearchOptions struct {
|
|
Query string
|
|
Page int
|
|
PerPage int
|
|
Sort string
|
|
Order string
|
|
}
|
|
|
|
func (c *Client) SearchPosts(ctx context.Context, opts SearchOptions) (*PostsResponse, error) {
|
|
path := fmt.Sprintf("/v1/teams/%s/posts", url.PathEscape(c.Team))
|
|
q := url.Values{}
|
|
if opts.Query != "" {
|
|
q.Set("q", opts.Query)
|
|
}
|
|
if opts.Page > 0 {
|
|
q.Set("page", strconv.Itoa(opts.Page))
|
|
}
|
|
if opts.PerPage > 0 {
|
|
q.Set("per_page", strconv.Itoa(opts.PerPage))
|
|
}
|
|
if opts.Sort != "" {
|
|
q.Set("sort", opts.Sort)
|
|
}
|
|
if opts.Order != "" {
|
|
q.Set("order", opts.Order)
|
|
}
|
|
body, err := c.do(ctx, http.MethodGet, path, q, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var r PostsResponse
|
|
if err := json.Unmarshal(body, &r); err != nil {
|
|
return nil, fmt.Errorf("unmarshal posts: %w", err)
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
func (c *Client) CreatePost(ctx context.Context, input PostInput) (*Post, error) {
|
|
path := fmt.Sprintf("/v1/teams/%s/posts", url.PathEscape(c.Team))
|
|
body, err := c.do(ctx, http.MethodPost, path, nil, postCreateRequest{Post: input})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var p Post
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
|
return nil, fmt.Errorf("unmarshal post: %w", err)
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
func (c *Client) UpdatePost(ctx context.Context, number int, input PostInput) (*Post, error) {
|
|
path := fmt.Sprintf("/v1/teams/%s/posts/%d", url.PathEscape(c.Team), number)
|
|
body, err := c.do(ctx, http.MethodPatch, path, nil, postCreateRequest{Post: input})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var p Post
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
|
return nil, fmt.Errorf("unmarshal post: %w", err)
|
|
}
|
|
return &p, nil
|
|
}
|