[自動コミット] esa.io API を操作する Go 製 CLI ツールを新規作成
All checks were successful
CI / Build & Test (push) Successful in 34s
All checks were successful
CI / Build & Test (push) Successful in 34s
- esa.io API クライアントの実装と単体テストを追加 - 記事の取得・検索・作成・更新を行うサブコマンドを実装 - GitHub Actions による CI ワークフローを設定 - プロジェクトの基本ファイル(README、.gitignore、LICENSE、go.mod)を作成
This commit is contained in:
261
main.go
Normal file
261
main.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.ssig33.com/ssig33/esacli/esa"
|
||||
)
|
||||
|
||||
const usage = `esacli - esa.io API CLI
|
||||
|
||||
Usage:
|
||||
esacli <command> [options]
|
||||
|
||||
Commands:
|
||||
get Fetch a single post
|
||||
search Search posts
|
||||
create Create a new post
|
||||
update Update an existing post
|
||||
help Show this help
|
||||
|
||||
Environment variables:
|
||||
ESA_TOKEN esa.io API access token (required)
|
||||
ESA_TEAM esa.io team name (required)
|
||||
|
||||
Run "esacli <command> -h" for command-specific options.
|
||||
`
|
||||
|
||||
func main() {
|
||||
if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(args []string, stdout, stderr io.Writer) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprint(stderr, usage)
|
||||
return errors.New("no command specified")
|
||||
}
|
||||
|
||||
cmd := args[0]
|
||||
rest := args[1:]
|
||||
|
||||
switch cmd {
|
||||
case "help", "-h", "--help":
|
||||
fmt.Fprint(stdout, usage)
|
||||
return nil
|
||||
case "get":
|
||||
return runGet(rest, stdout)
|
||||
case "search":
|
||||
return runSearch(rest, stdout)
|
||||
case "create":
|
||||
return runCreate(rest, stdout)
|
||||
case "update":
|
||||
return runUpdate(rest, stdout)
|
||||
default:
|
||||
fmt.Fprint(stderr, usage)
|
||||
return fmt.Errorf("unknown command: %s", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func newClient() (*esa.Client, error) {
|
||||
token := os.Getenv("ESA_TOKEN")
|
||||
if token == "" {
|
||||
return nil, errors.New("ESA_TOKEN is not set")
|
||||
}
|
||||
team := os.Getenv("ESA_TEAM")
|
||||
if team == "" {
|
||||
return nil, errors.New("ESA_TEAM is not set")
|
||||
}
|
||||
c := esa.NewClient(team, token)
|
||||
if base := os.Getenv("ESA_BASE_URL"); base != "" {
|
||||
c.BaseURL = base
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func runGet(args []string, stdout io.Writer) error {
|
||||
fs := flag.NewFlagSet("get", flag.ContinueOnError)
|
||||
number := fs.Int("number", 0, "post number (required)")
|
||||
bodyOnly := fs.Bool("body", false, "print only body_md instead of JSON")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *number <= 0 {
|
||||
return errors.New("get: -number is required")
|
||||
}
|
||||
c, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
post, err := c.GetPost(context.Background(), *number)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *bodyOnly {
|
||||
fmt.Fprint(stdout, post.BodyMD)
|
||||
return nil
|
||||
}
|
||||
return printJSON(stdout, post)
|
||||
}
|
||||
|
||||
func runSearch(args []string, stdout io.Writer) error {
|
||||
fs := flag.NewFlagSet("search", flag.ContinueOnError)
|
||||
query := fs.String("q", "", "search query")
|
||||
page := fs.Int("page", 0, "page number")
|
||||
perPage := fs.Int("per-page", 0, "results per page (max 100)")
|
||||
sort := fs.String("sort", "", "sort field (updated, created, number, stars, watches, comments, best_match)")
|
||||
order := fs.String("order", "", "order (desc, asc)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.SearchPosts(context.Background(), esa.SearchOptions{
|
||||
Query: *query,
|
||||
Page: *page,
|
||||
PerPage: *perPage,
|
||||
Sort: *sort,
|
||||
Order: *order,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(stdout, resp)
|
||||
}
|
||||
|
||||
func runCreate(args []string, stdout io.Writer) error {
|
||||
fs := flag.NewFlagSet("create", flag.ContinueOnError)
|
||||
name := fs.String("name", "", "post title (required)")
|
||||
bodyFile := fs.String("body-file", "", "path to file containing body_md ('-' for stdin)")
|
||||
body := fs.String("body", "", "body_md (use -body-file for longer content)")
|
||||
tagsCSV := fs.String("tags", "", "comma-separated tags")
|
||||
category := fs.String("category", "", "category")
|
||||
wip := fs.Bool("wip", true, "WIP flag")
|
||||
message := fs.String("message", "", "commit-like message")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *name == "" {
|
||||
return errors.New("create: -name is required")
|
||||
}
|
||||
bodyMD, err := resolveBody(*body, *bodyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wipVal := *wip
|
||||
post, err := c.CreatePost(context.Background(), esa.PostInput{
|
||||
Name: *name,
|
||||
BodyMD: bodyMD,
|
||||
Tags: splitCSV(*tagsCSV),
|
||||
Category: *category,
|
||||
WIP: &wipVal,
|
||||
Message: *message,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(stdout, post)
|
||||
}
|
||||
|
||||
func runUpdate(args []string, stdout io.Writer) error {
|
||||
fs := flag.NewFlagSet("update", flag.ContinueOnError)
|
||||
number := fs.Int("number", 0, "post number (required)")
|
||||
name := fs.String("name", "", "new title")
|
||||
bodyFile := fs.String("body-file", "", "path to file containing body_md ('-' for stdin)")
|
||||
body := fs.String("body", "", "body_md (use -body-file for longer content)")
|
||||
tagsCSV := fs.String("tags", "", "comma-separated tags")
|
||||
category := fs.String("category", "", "category")
|
||||
wipFlag := fs.String("wip", "", "WIP flag (true/false); empty leaves unchanged")
|
||||
message := fs.String("message", "", "commit-like message")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *number <= 0 {
|
||||
return errors.New("update: -number is required")
|
||||
}
|
||||
bodyMD, err := resolveBody(*body, *bodyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
input := esa.PostInput{
|
||||
Name: *name,
|
||||
BodyMD: bodyMD,
|
||||
Tags: splitCSV(*tagsCSV),
|
||||
Category: *category,
|
||||
Message: *message,
|
||||
}
|
||||
if *wipFlag != "" {
|
||||
v, err := strconv.ParseBool(*wipFlag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update: -wip: %w", err)
|
||||
}
|
||||
input.WIP = &v
|
||||
}
|
||||
c, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
post, err := c.UpdatePost(context.Background(), *number, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(stdout, post)
|
||||
}
|
||||
|
||||
func resolveBody(inline, file string) (string, error) {
|
||||
if inline != "" && file != "" {
|
||||
return "", errors.New("specify only one of -body or -body-file")
|
||||
}
|
||||
if file == "" {
|
||||
return inline, nil
|
||||
}
|
||||
if file == "-" {
|
||||
b, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read stdin: %w", err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read body file: %w", err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func splitCSV(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func printJSON(w io.Writer, v any) error {
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(v)
|
||||
}
|
||||
Reference in New Issue
Block a user