SebastianCzoch/onesky-go

View on GitHub
onesky.go

Summary

Maintainability
B
6 hrs
Test Coverage
// Package onesky is go utils for working with OneSky translation service
// Copyright (c) 2015 Sebastian Czoch <sebastian@czoch.eu>. All rights reserved.
// Use of this source code is governed by a GNU v2 license found in the LICENSE file.
package onesky

import (
    "bytes"
    "crypto/md5"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "mime/multipart"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "time"
)

// APIAddress is https address to OneSky API
const APIAddress = "https://platform.api.onesky.io"

// APIVersion is OneSky API version which will be used
const APIVersion = "1"

// Client is basics struct for this package contains Secret, APIKey and ProjectID which is needed to authorize in OneSky service
type Client struct {
    Secret    string
    APIKey    string
    ProjectID int
}

type apiEndpoint struct {
    path   string
    method string
}

var apiEndpoints = map[string]apiEndpoint{
    "getFile":               apiEndpoint{"projects/%d/translations", "GET"},
    "postFile":              apiEndpoint{"projects/%d/files", "POST"},
    "deleteFile":            apiEndpoint{"projects/%d/files", "DELETE"},
    "listFiles":             apiEndpoint{"projects/%d/files", "GET"},
    "importTasks":           apiEndpoint{"projects/%d/import-tasks", "GET"},
    "importTask":            apiEndpoint{"projects/%d/import-tasks/%d", "GET"},
    "getTranslationsStatus": apiEndpoint{"projects/%d/translations/status", "GET"},
    "getLanguages":          apiEndpoint{"projects/%d/languages", "GET"},
}

// FileData is a struct which contains informations about file uploaded to OneSky service
type FileData struct {
    Name                string     `json:"name"`
    FileName            string     `json:"file_name"`
    StringCount         int        `json:"string_count"`
    LastImport          LastImport `json:"last_import"`
    UpoladedAt          string     `json:"uploaded_at"`
    UpoladedAtTimestamp int        `json:"uploaded_at_timestamp"`
}

// LastImport is a struct which contains informations about last upload
type LastImport struct {
    ID     int    `json:"id"`
    Status string `json:"status"`
}
type listFilesResponse struct {
    Data []FileData `json:"data"`
}
type getLanguagesResponse struct {
    Data []Language `json:"data"`
}

// TaskData is a struct which contains informations about import task
type TaskData struct {
    ID                  int64
    OriginalID          interface{} `json:"id"`
    File                TaskFile    `json:"file"`
    StringCount         int         `json:"string_count"`
    WordCount           int         `json:"word_count"`
    Status              string      `json:"status"`
    CreateddAt          string      `json:"created_at"`
    CreateddAtTimestamp int         `json:"created_at_timestamp"`
}

// Language is a struct which contains informations about locale
type Language struct {
    Code                string `json:"code"`
    EnglishName         string `json:"english_name"`
    LocalName           string `json:"local_name"`
    CustomLocale        string `json:"custom_locale"`
    Locale              string `json:"locale"`
    Region              string `json:"region"`
    TranslationProgress string `json:"translation_progress"`
}

// TaskFile is a struct which contains informations about file of import task
type TaskFile struct {
    Name   string   `json:"name"`
    Format string   `json:"format"`
    Locale Language `json:"locale"`
}

// ImportTasksResponse is a struct which contains informations about the response from list import tasks API
type ImportTasksResponse struct {
    Data []TaskData `json:"data"`
}

// ImportTaskResponse is a struct which contains informations about the response from show an import task API
type ImportTaskResponse struct {
    Data TaskData `json:"data"`
}

// UploadData is a struct which contains informations about uploaded file
type UploadData struct {
    Name     string   `json:"name"`
    Format   string   `json:"format"`
    Language Language `json:"language"`
    Import   TaskData `json:"import"`
}

// UploadResponse is a struct which contains informations about the response from upload file API
type UploadResponse struct {
    Data UploadData `json:"data"`
}

// TranslationsStatus is a struct which contains information about a project's translation status
type TranslationsStatus struct {
    FileName    string   `json:"file_name"`
    Locale      Language `json:"locale"`
    Progress    string   `json:"progress"`
    StringCount int64    `json:"string_count"`
    WordCount   int64    `json:"word_count"`
}
type getTranslationsStatusResponse struct {
    Data TranslationsStatus `json:"data"`
}

// convertToInt64 : Convert interface{} to int64
func convertToInt64(in interface{}) (int64, error) {
    switch in.(type) {
    case string:
        return strconv.ParseInt(in.(string), 10, 64)
    case int:
        return int64(in.(int)), nil
    case int16:
        return int64(in.(int16)), nil
    case int32:
        return int64(in.(int32)), nil
    case int64:
        return in.(int64), nil
    case uint:
        return int64(in.(uint)), nil
    case uint16:
        return int64(in.(uint16)), nil
    case uint32:
        return int64(in.(uint32)), nil
    case uint64:
        return int64(in.(uint64)), nil
    case float32:
        return int64(in.(float32)), nil
    case float64:
        return int64(in.(float64)), nil
    default:
        return 0, fmt.Errorf("%s: %v", "Unable to convert value", in)
    }
}

// ImportTask : Show an import task. Parameters: import_id
func (c *Client) ImportTask(importID int64) (TaskData, error) {
    endpoint, err := getEndpoint("importTask")
    if err != nil {
        return TaskData{}, err
    }
    values := url.Values{}
    urlStr, err := endpoint.full(c, values, importID)
    if err != nil {
        return TaskData{}, err
    }

    res, err := makeRequest(endpoint.method, urlStr, nil, "")
    if err != nil {
        return TaskData{}, err
    }

    if res.StatusCode != http.StatusOK {
        return TaskData{}, fmt.Errorf("bad status: %s", res.Status)
    }

    body, err := getResponseBodyAsString(res)
    if err != nil {
        return TaskData{}, err
    }
    aux := ImportTaskResponse{}
    err = json.Unmarshal([]byte(body), &aux)
    if err != nil {
        return TaskData{}, err
    }
    if i, err := convertToInt64(aux.Data.OriginalID); err == nil {
        // fmt.Printf("%T, %v", i, i)
        aux.Data.ID = i
    }

    return aux.Data, nil
}

// ImportTasks : List import tasks. Parameters: page: 1, per_page: 50, status: [all|completed|in-progress|failed]
// tasks, err := onesky.ImportTasks(map[string]interface{}{"per_page": 50, "status": "in-progress"})
func (c *Client) ImportTasks(params map[string]interface{}) ([]TaskData, error) {
    endpoint, err := getEndpoint("importTasks")
    if err != nil {
        return nil, err
    }
    values := url.Values{}
    values.Set("page", "1")
    values.Set("per_page", "50")
    values.Set("status", "all")
    for k, v := range params {
        values.Set(k, fmt.Sprintf("%v", v))
    }
    urlStr, err := endpoint.full(c, values)
    if err != nil {
        return nil, err
    }

    res, err := makeRequest(endpoint.method, urlStr, nil, "")
    if err != nil {
        return nil, err
    }

    if res.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("bad status: %s", res.Status)
    }

    body, err := getResponseBodyAsString(res)
    if err != nil {
        return nil, err
    }
    aux := ImportTasksResponse{}
    err = json.Unmarshal([]byte(body), &aux)
    if err != nil {
        return nil, err
    }
    for i := range aux.Data {
        task := &aux.Data[i]
        if id, err := convertToInt64(task.OriginalID); err == nil {
            // fmt.Printf("\n%T, %v, %T\n", id, id, task.OriginalID)
            task.ID = id
        }
    }

    return aux.Data, nil
}

// ListFiles is method on Client struct which download form OneSky service informations about uploaded files
func (c *Client) ListFiles(page, perPage int) ([]FileData, error) {
    endpoint, err := getEndpoint("listFiles")
    if err != nil {
        return nil, err
    }

    v := url.Values{}
    v.Set("page", strconv.Itoa(page))
    v.Set("per_page", strconv.Itoa(perPage))
    urlStr, err := endpoint.full(c, v)
    if err != nil {
        return nil, err
    }

    res, err := makeRequest(endpoint.method, urlStr, nil, "")
    if err != nil {
        return nil, err
    }

    if res.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("bad status: %s", res.Status)
    }

    body, err := getResponseBodyAsString(res)
    if err != nil {
        return nil, err
    }

    aux := listFilesResponse{}
    err = json.Unmarshal([]byte(body), &aux)
    if err != nil {
        return nil, err
    }

    return aux.Data, nil
}

// DownloadFile is method on Client struct which download form OneSky service choosen file as string
func (c *Client) DownloadFile(fileName, locale string) (string, error) {
    endpoint, err := getEndpoint("getFile")
    if err != nil {
        return "", err
    }

    v := url.Values{}
    v.Set("locale", locale)
    v.Set("source_file_name", fileName)
    urlStr, err := endpoint.full(c, v)
    if err != nil {
        return "", err
    }

    res, err := makeRequest(endpoint.method, urlStr, nil, "")
    if err != nil {
        return "", err
    }

    if res.StatusCode != http.StatusOK {
        return "", fmt.Errorf("bad status: %s", res.Status)
    }

    body, err := getResponseBodyAsString(res)
    if err != nil {
        return "", err
    }

    return body, nil
}

// UploadFile is method on Client struct which upload file to OneSky service
func (c *Client) UploadFile(file, fileFormat, locale string, keepStrings bool) (UploadData, error) {
    endpoint, err := getEndpoint("postFile")
    if err != nil {
        return UploadData{}, err
    }

    v := url.Values{}
    v.Set("locale", locale)
    v.Set("file_format", fileFormat)
    v.Set("is_keeping_all_strings", strconv.FormatBool(keepStrings))
    urlStr, err := endpoint.full(c, v)
    if err != nil {
        return UploadData{}, err
    }

    var b bytes.Buffer
    w := multipart.NewWriter(&b)
    f, err := os.Open(file)
    if err != nil {
        return UploadData{}, err
    }
    defer w.Close()

    fw, err := w.CreateFormFile("file", file)
    if err != nil {
        return UploadData{}, err
    }

    if _, err = io.Copy(fw, f); err != nil {
        return UploadData{}, err
    }

    w.Close()

    res, err := makeRequest(endpoint.method, urlStr, &b, w.FormDataContentType())
    if err != nil {
        return UploadData{}, err
    }

    if res.StatusCode != http.StatusCreated {
        return UploadData{}, fmt.Errorf("bad status: %s", res.Status)
    }

    body, err := getResponseBodyAsString(res)
    if err != nil {
        return UploadData{}, err
    }

    aux := UploadResponse{}
    err = json.Unmarshal([]byte(body), &aux)
    if err != nil {
        return UploadData{}, err
    }
    if i, err := convertToInt64(aux.Data.Import.OriginalID); err == nil {
        aux.Data.Import.ID = i
    }

    return aux.Data, nil
}

// DeleteFile is method on Client struct which remove file from OneSky service
func (c *Client) DeleteFile(fileName string) error {
    endpoint, err := getEndpoint("deleteFile")
    if err != nil {
        return err
    }

    v := url.Values{}
    v.Set("file_name", fileName)
    urlStr, err := endpoint.full(c, v)
    if err != nil {
        return err
    }

    res, err := makeRequest(endpoint.method, urlStr, nil, "")
    if err != nil {
        return err
    }

    if res.StatusCode != http.StatusOK {
        return fmt.Errorf("bad status: %s", res.Status)
    }

    return nil
}

// GetTranslationsStatus returns information about a project's translation status
func (c *Client) GetTranslationsStatus(fileName, locale string) (TranslationsStatus, error) {
    endpoint, err := getEndpoint("getTranslationsStatus")
    if err != nil {
        return TranslationsStatus{}, err
    }

    v := url.Values{}
    v.Set("file_name", fileName)
    v.Set("locale", locale)
    urlStr, err := endpoint.full(c, v)
    if err != nil {
        return TranslationsStatus{}, err
    }

    res, err := makeRequest(endpoint.method, urlStr, nil, "")
    if err != nil {
        return TranslationsStatus{}, err
    }

    if res.StatusCode != http.StatusOK {
        return TranslationsStatus{}, fmt.Errorf("bad status: %s", res.Status)
    }

    body, err := getResponseBodyAsString(res)
    if err != nil {
        return TranslationsStatus{}, err
    }

    aux := getTranslationsStatusResponse{}
    err = json.Unmarshal([]byte(body), &aux)
    if err != nil {
        return TranslationsStatus{}, err
    }

    return aux.Data, nil
}

// GetLanguages is method on Client struct which download from OneSky service information about available languages in project
func (c *Client) GetLanguages() ([]Language, error) {
    endpoint, err := getEndpoint("getLanguages")
    if err != nil {
        return nil, err
    }

    v := url.Values{}
    urlStr, err := endpoint.full(c, v)
    if err != nil {
        return nil, err
    }

    res, err := makeRequest(endpoint.method, urlStr, nil, "")
    if err != nil {
        return nil, err
    }

    if res.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("bad status: %s", res.Status)
    }

    body, err := getResponseBodyAsString(res)
    if err != nil {
        return nil, err
    }

    aux := getLanguagesResponse{}
    err = json.Unmarshal([]byte(body), &aux)
    if err != nil {
        return nil, err
    }

    return aux.Data, nil
}

func makeRequest(method, urlStr string, body io.Reader, contentType string) (*http.Response, error) {
    req, err := http.NewRequest(method, urlStr, body)
    if err != nil {
        return nil, err
    }

    if contentType != "" {
        req.Header.Set("Content-Type", contentType)
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }

    return resp, nil
}

func getResponseBodyAsString(response *http.Response) (string, error) {
    res, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return "", err
    }

    return string(res), nil
}

func (c *Client) getAuthHashAndTime() (string, string) {
    hasher := md5.New()
    time := strconv.Itoa(int(time.Now().Unix()))
    hasher.Write([]byte(time + c.Secret))

    return hex.EncodeToString(hasher.Sum(nil)), time
}

func (e *apiEndpoint) full(c *Client, additionalArgs url.Values, extends ...interface{}) (string, error) {
    extends = append([]interface{}{c.ProjectID}, extends...)
    urlWithProjectID := fmt.Sprintf(e.path, extends...)
    address, err := url.Parse(APIAddress + "/" + APIVersion + "/" + urlWithProjectID)
    if err != nil {
        return "", err
    }

    hash, timestamp := c.getAuthHashAndTime()
    additionalArgs.Set("api_key", c.APIKey)
    additionalArgs.Set("timestamp", timestamp)
    additionalArgs.Set("dev_hash", hash)

    return address.String() + "?" + additionalArgs.Encode(), nil
}

func getEndpoint(name string) (apiEndpoint, error) {
    endpoint, ok := apiEndpoints[name]
    if !ok {
        return apiEndpoint{}, fmt.Errorf("endpoint %s not found", name)
    }

    return endpoint, nil
}