325 lines
7.5 KiB
Go
325 lines
7.5 KiB
Go
|
package arukas
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"github.com/manyminds/api2go/jsonapi"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"log"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"os"
|
||
|
"reflect"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// VERSION is cli version.
|
||
|
const VERSION = "v0.1.3"
|
||
|
|
||
|
// Client represents a user client data in struct variables.
|
||
|
type Client struct {
|
||
|
APIURL *url.URL
|
||
|
HTTP *http.Client
|
||
|
Username string
|
||
|
Password string
|
||
|
UserAgent string
|
||
|
Debug bool
|
||
|
Output func(...interface{})
|
||
|
OutputDest io.Writer
|
||
|
Timeout time.Duration
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
client *Client
|
||
|
)
|
||
|
|
||
|
// PrintHeaderln print as the values.
|
||
|
func (c *Client) PrintHeaderln(values ...interface{}) {
|
||
|
fmt.Fprint(c.OutputDest, ToTSV(values[1:]), "\n")
|
||
|
}
|
||
|
|
||
|
// Println print as the values.
|
||
|
func (c *Client) Println(values ...interface{}) {
|
||
|
fmt.Fprint(c.OutputDest, ToTSV(values[1:]), "\n")
|
||
|
}
|
||
|
|
||
|
// Get return *c as the get path of API request.
|
||
|
func (c *Client) Get(v interface{}, path string) error {
|
||
|
return c.APIReq(v, "GET", path, nil)
|
||
|
}
|
||
|
|
||
|
// Patch return *c as the patch path of API request.
|
||
|
func (c *Client) Patch(v interface{}, path string, body interface{}) error {
|
||
|
return c.APIReq(v, "PATCH", path, body)
|
||
|
}
|
||
|
|
||
|
// Post return *c as the post path of API request.
|
||
|
func (c *Client) Post(v interface{}, path string, body interface{}) error {
|
||
|
return c.APIReq(v, "POST", path, body)
|
||
|
}
|
||
|
|
||
|
// Put return *c as the put path of API request.
|
||
|
func (c *Client) Put(v interface{}, path string, body interface{}) error {
|
||
|
return c.APIReq(v, "PUT", path, body)
|
||
|
}
|
||
|
|
||
|
// Delete return *c as the delete path of API request.
|
||
|
func (c *Client) Delete(path string) error {
|
||
|
return c.APIReq(nil, "DELETE", path, nil)
|
||
|
}
|
||
|
|
||
|
// NewClientWithOsExitOnErr return client.
|
||
|
func NewClientWithOsExitOnErr() *Client {
|
||
|
client, err := NewClient()
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
return client
|
||
|
}
|
||
|
|
||
|
// NewClient returns a new arukas client, requires an authorization key.
|
||
|
// You can generate a API key by visiting the Keys section of the Arukas
|
||
|
// control panel for your account.
|
||
|
func NewClient() (*Client, error) {
|
||
|
debug := false
|
||
|
if os.Getenv("ARUKAS_DEBUG") != "" {
|
||
|
debug = true
|
||
|
}
|
||
|
apiURL := "https://app.arukas.io/api/"
|
||
|
if os.Getenv("ARUKAS_JSON_API_URL") != "" {
|
||
|
apiURL = os.Getenv("ARUKAS_JSON_API_URL")
|
||
|
}
|
||
|
client := new(Client)
|
||
|
parsedURL, err := url.Parse(apiURL)
|
||
|
if err != nil {
|
||
|
fmt.Println("url err")
|
||
|
return nil, err
|
||
|
}
|
||
|
parsedURL.Path = strings.TrimRight(parsedURL.Path, "/")
|
||
|
|
||
|
client.APIURL = parsedURL
|
||
|
client.UserAgent = "Arukas CLI (" + VERSION + ")"
|
||
|
client.Debug = debug
|
||
|
client.OutputDest = os.Stdout
|
||
|
client.Timeout = 30 * time.Second
|
||
|
|
||
|
if username := os.Getenv("ARUKAS_JSON_API_TOKEN"); username != "" {
|
||
|
client.Username = username
|
||
|
} else {
|
||
|
return nil, errors.New("ARUKAS_JSON_API_TOKEN is not set")
|
||
|
}
|
||
|
|
||
|
if password := os.Getenv("ARUKAS_JSON_API_SECRET"); password != "" {
|
||
|
client.Password = password
|
||
|
} else {
|
||
|
return nil, errors.New("ARUKAS_JSON_API_SECRET is not set")
|
||
|
}
|
||
|
|
||
|
return client, nil
|
||
|
}
|
||
|
|
||
|
// NewRequest Generates an HTTP request for the Arukas API, but does not
|
||
|
// perform the request. The request's Accept header field will be
|
||
|
// set to:
|
||
|
//
|
||
|
// Accept: application/vnd.api+json;
|
||
|
//
|
||
|
// The type of body determines how to encode the request:
|
||
|
//
|
||
|
// nil no body
|
||
|
// io.Reader body is sent verbatim
|
||
|
// []byte body is encoded as application/vnd.api+json
|
||
|
// else body is encoded as application/json
|
||
|
func (c *Client) NewRequest(method, path string, body interface{}) (*http.Request, error) {
|
||
|
var ctype string
|
||
|
var rbody io.Reader
|
||
|
|
||
|
switch t := body.(type) {
|
||
|
case nil:
|
||
|
case string:
|
||
|
rbody = bytes.NewBufferString(t)
|
||
|
case io.Reader:
|
||
|
rbody = t
|
||
|
case []byte:
|
||
|
rbody = bytes.NewReader(t)
|
||
|
ctype = "application/vnd.api+json"
|
||
|
default:
|
||
|
v := reflect.ValueOf(body)
|
||
|
if !v.IsValid() {
|
||
|
break
|
||
|
}
|
||
|
if v.Type().Kind() == reflect.Ptr {
|
||
|
v = reflect.Indirect(v)
|
||
|
if !v.IsValid() {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
j, err := json.Marshal(body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
rbody = bytes.NewReader(j)
|
||
|
ctype = "application/json"
|
||
|
}
|
||
|
requestURL := *c.APIURL // shallow copy
|
||
|
requestURL.Path += path
|
||
|
if c.Debug {
|
||
|
fmt.Printf("Requesting: %s %s %s\n", method, requestURL, rbody)
|
||
|
}
|
||
|
req, err := http.NewRequest(method, requestURL.String(), rbody)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
req.Header.Set("Accept", "application/vnd.api+json")
|
||
|
req.Header.Set("User-Agent", c.UserAgent)
|
||
|
if ctype != "" {
|
||
|
req.Header.Set("Content-Type", ctype)
|
||
|
}
|
||
|
req.SetBasicAuth(c.Username, c.Password)
|
||
|
|
||
|
return req, nil
|
||
|
}
|
||
|
|
||
|
// APIReq Sends a Arukas API request and decodes the response into v.
|
||
|
// As described in NewRequest(), the type of body determines how to
|
||
|
// encode the request body. As described in DoReq(), the type of
|
||
|
// v determines how to handle the response body.
|
||
|
func (c *Client) APIReq(v interface{}, method, path string, body interface{}) error {
|
||
|
var marshaled []byte
|
||
|
var err1 error
|
||
|
var req *http.Request
|
||
|
if body != nil {
|
||
|
var err error
|
||
|
_, ok := body.(jsonapi.MarshalIdentifier)
|
||
|
if ok {
|
||
|
marshaled, err = jsonapi.Marshal(body)
|
||
|
} else {
|
||
|
marshaled, err = json.Marshal(body)
|
||
|
}
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if c.Debug {
|
||
|
fmt.Println("json: ", string(marshaled))
|
||
|
}
|
||
|
req, err1 = c.NewRequest(method, path, marshaled)
|
||
|
} else {
|
||
|
req, err1 = c.NewRequest(method, path, body)
|
||
|
}
|
||
|
|
||
|
if err1 != nil {
|
||
|
return err1
|
||
|
}
|
||
|
return c.DoReq(req, v)
|
||
|
}
|
||
|
|
||
|
// DoReq Submits an HTTP request, checks its response, and deserializes
|
||
|
// the response into v. The type of v determines how to handle
|
||
|
// the response body:
|
||
|
//
|
||
|
// nil body is discarded
|
||
|
// io.Writer body is copied directly into v
|
||
|
// else body is decoded into v as json
|
||
|
//
|
||
|
func (c *Client) DoReq(req *http.Request, v interface{}) error {
|
||
|
|
||
|
httpClient := c.HTTP
|
||
|
if httpClient == nil {
|
||
|
httpClient = &http.Client{
|
||
|
Timeout: c.Timeout,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
res, err := httpClient.Do(req)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer res.Body.Close()
|
||
|
|
||
|
body, err := ioutil.ReadAll(res.Body)
|
||
|
|
||
|
if c.Debug {
|
||
|
fmt.Println("Status:", res.StatusCode)
|
||
|
headers := make([]string, len(res.Header))
|
||
|
for k := range res.Header {
|
||
|
headers = append(headers, k)
|
||
|
}
|
||
|
sort.Strings(headers)
|
||
|
for _, k := range headers {
|
||
|
if k != "" {
|
||
|
fmt.Println(k+":", strings.Join(res.Header[k], " "))
|
||
|
}
|
||
|
}
|
||
|
fmt.Println(string(body))
|
||
|
}
|
||
|
|
||
|
if err = checkResponse(res); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
switch t := v.(type) {
|
||
|
case nil:
|
||
|
case io.Writer:
|
||
|
_, err = io.Copy(t, res.Body)
|
||
|
default:
|
||
|
err = jsonapi.Unmarshal(body, v)
|
||
|
if err != nil {
|
||
|
err = json.Unmarshal(body, v)
|
||
|
}
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// CheckResponse returns an error (of type *Error) if the response.
|
||
|
func checkResponse(res *http.Response) error {
|
||
|
if res.StatusCode == 404 {
|
||
|
return fmt.Errorf("The resource does not found on the server: %s", res.Request.URL)
|
||
|
} else if res.StatusCode >= 400 {
|
||
|
return fmt.Errorf("Got HTTP status code >= 400: %s", res.Status)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// PrintTsvln print Tab-separated values line.
|
||
|
func PrintTsvln(values ...interface{}) {
|
||
|
fmt.Println(ToTSV(values))
|
||
|
}
|
||
|
|
||
|
// ToTSV return Tab-separated values.
|
||
|
func ToTSV(values []interface{}) string {
|
||
|
var str []string
|
||
|
for _, s := range values {
|
||
|
if v, ok := s.(string); ok {
|
||
|
str = append(str, string(v))
|
||
|
} else {
|
||
|
str = append(str, fmt.Sprint(s))
|
||
|
}
|
||
|
|
||
|
}
|
||
|
return strings.Join(str, "\t")
|
||
|
}
|
||
|
|
||
|
// SplitTSV return splited Tab-separated values.
|
||
|
func SplitTSV(str string) []string {
|
||
|
splitStr := strings.Split(str, "\t")
|
||
|
var trimmed []string
|
||
|
for _, v := range splitStr {
|
||
|
trimmed = append(trimmed, strings.Trim(v, "\n"))
|
||
|
}
|
||
|
return trimmed
|
||
|
}
|
||
|
|
||
|
// removeFirstLine is remove first line.
|
||
|
func removeFirstLine(str string) string {
|
||
|
lines := strings.Split(str, "\n")
|
||
|
return strings.Join(lines[1:], "\n")
|
||
|
}
|