На работе и дома я пользуюсь исключительно операционной системой Linux. Но какое же рабочее место может быть без виджетов на рабочем столе? Вот я решил установить и настроить у себя Conky. Приложение показалось очень полезным, но чего-то не хватало. Почему бы не добавить туда актуальные курсы валют и не выводить афоризмы? Для этих целей я и решил написать скрипт. А так как речь идёт о консольном приложении, то всё было сделано на GO.
Для начала нам нужно определиться с API, откуда мы будем получать информацию. Долго выбирать не пришлось, нашёл два сервиса:
- Курсы валют получаем с сайта www.currencyconverterapi.com. Там потребуется сгенерировать ключ API.
- Цитаты и афоризмы получаем с http://rzhunemogu.ru. Там также можно получать и анекдоты. Мне это пригодится в будущем.
Оба моих проекта доступны на github. Можно ознакомиться полностью с исходным кодом и установить у себя:
- Скрипт по получению курсов валют: https://github.com/semelyanov86/currency-rates
- Скрипт по получению афоризмов: https://github.com/semelyanov86/fortune-receiver
Проекты практически одинаковые по функционалу, поэтому в этой статье я разберу только второй, там есть один нюанс, который нужно прояснить.
Всё что будет делать скрипт - это скачивать нужные данные в формате JSON и выводить нужную информацию в консоль.
Первое что нам понадобится - сгенерировать новый проект в вашей IDE.
Далее нам нужно определиться с типами, создать все необходимые интерфейсы, которые бы говорили нам, как работают HTTP-запросы и какие типы данных используются нами.
Поэтому я создаю для этого отдельный файл types.go.
package main
type GetWebRequest interface {
FetchBytes(typeFort int) ([]byte, error)
}
type FortuneResult struct {
Content string
}
Как видно, интерфейс GetWebRequest говорит нам, что принимает тип афоризма, а ответ возвращает результат полученных данных или ошибку.
Для чего я создаю интерфейс в первую очередь? Чтобы иметь возможность покрыть тестами, создать функцию, которая будет возвращать тестовые данные. Также мы абстрагируем операции по получению данных и можем быстро поменять источник.
Теперь нужно сделать имплементацию этого интерфейса. Создаю файл fortune.go:
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
"io"
"log"
"net/http"
"strconv"
)
type LiveGetWebRequest struct {
}
func (g LiveGetWebRequest) FetchBytes(typeFort int) ([]byte, error) {
url := "http://rzhunemogu.ru/RandJSON.aspx?CType=" + strconv.Itoa(typeFort)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return []byte{}, err
}
req.Header.Set("User-Agent", "Conky-Fortune-Ext")
res, err := http.DefaultClient.Do(req)
if err != nil {
return []byte{}, err
}
if res.StatusCode != http.StatusOK {
log.Fatalf("Wrong status code: %d", res.StatusCode)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return []byte{}, err
}
return body, nil
}
func getFortune(getWebRequest GetWebRequest, typeFort int) (string, error) {
var fortune FortuneResult
body, err := getWebRequest.FetchBytes(typeFort)
if err != nil {
return "", err
}
data := bytes.Replace(body, []byte("\r"), []byte(""), -1)
data = bytes.Replace(data, []byte("\n"), []byte("\\n"), -1)
dec := charmap.Windows1251.NewDecoder()
utf8Bytes, _, err := transform.Bytes(dec, data)
if err != nil {
return "", err
}
err = json.Unmarshal(utf8Bytes, &fortune)
if err != nil {
return "", err
}
return fortune.Content, err
}
И конечно же нельзя забыть и о функции main, без неё не запустится наш скрипт.
func main() {
var key int
flag.IntVar(&key, "type", 4, "Type of fortune")
flag.Parse()
liveClient := LiveGetWebRequest{}
content, err := getFortune(liveClient, key)
if err != nil {
log.Fatal(err)
}
fmt.Println(content)
}
Как видите, в этом примере я имплементацию объединил в один файл. Что, может быть, не совсем правильно. Имеет смысл в будущем функцию FetchBytes и структуру LiveGetWebRequest вынести в отдельный файл, под названием http_fortune, которая будет описывать именно имплементацию по http. А если мы в будущем решим грузить афоризмы из файла, то просто сделаем новый файл и поменяем источник только в функции main.
Далее, если вы заметили, в функции getFortune есть один интересный момент. Данные нам приходят в кодировке Windows-1251. Результат в консоли отображается в виде непонятных символов. Нужно всё сконвертировать в UTF-8. Кроме этого, нам нужно поменять символы по переносу строки.
data := bytes.Replace(body, []byte("\r"), []byte(""), -1)
data = bytes.Replace(data, []byte("\n"), []byte("\\n"), -1)
dec := charmap.Windows1251.NewDecoder()
utf8Bytes, _, err := transform.Bytes(dec, data)
Теперь когда мы работаем только с интерфейсами, мы можем всё покрыть тестами.
Файл fortune_test выглядит следующим образом:
package main
import "testing"
type testWebRequest struct {
}
func (t testWebRequest) FetchBytes(typeFort int) ([]byte, error) {
return []byte(`{"content":"this is test"}`), nil
}
func TestGetContent(t *testing.T) {
want := "this is test"
got, err := getFortune(testWebRequest{}, 1)
if err != nil {
t.Fatal(err)
}
if want != got {
t.Errorf("Content want: %s, got: %s", want, got)
}
}
Чтобы запустить тесты, выполните команду
go test ./
Вышеуказанный тест эффективен только при проверке функции json.Unmarshal и наших предположений касательно того, как должен выглядеть корректный ответ по api. Абстрагирование может быть подходящим в нашем примере, но покрытие будет низким.
Поэтому, может иметь смысл сделать низкоуровневые тесты, чтобы убедиться, что сделанный запрос на сервер корректен или что мы отправляем именно get запрос, а не post.
Для этих целей у GO есть набор вспомогательных функций, которые создают тестовые http серверы и клиенты.
Имея в наличии go код, теперь надо сгенерировать исполняемый файл. Делаем это командой
go build ./
Нам сгенерируется файл fortune-receiver.
Если мы его запустим в консоли, то получим на выходе цитату. Мы также можем передать в качестве аргумента тип и получить не цитату, а анекдот, например:
fortune-receiver -type=1
Теперь нужно добавить этот блок в conky.
${offset 0}${font Noto Sans:size=10}${execi 500 cat ~/random.log}
Как видите из примера, мы просто выводим содержимое файла. Просто показывать тот результат, который нам показывает скрипт, у меня не получилось. Поэтому я выбрал такой обходной вариант.
Постоянно обновлять этот файл не проблема - просто добавим задание в планировщик:
*/15 * * * * ~/fortune-receiver > ~/random.log
Итак, теперь у нас есть очень удобный консольный скрипт, который позволяет выводить афоризм или цитату в консоль и в виджеты conky. Также вы можете им пользоваться и в самой консоли. Например, вы можете добавить alias в файле .bashrc:
alias anekdot='/data/sergeyem/forune-receiver/fortune-receiver -type=1'
Это позволит нам просто ввести команду anekdot в командой строке и на выходе мы можем прочитать случайный анекдот, полученный по API.