Введение
Во Вк есть наборы стикеров, некоторые из которых даже бесплатные. Но во Вк нет ни какого публичного API для использования данного функционала на сторонних сайта. Задачка состоит в том, чтобы используя функциональный язык Elixir написать расширение над местом хранения стикеров во Вк в виде API.
По моему мнению имена методов, и параметры, которые они принимали были бы следующими. Общим пространством имён для коллекции API методов для работы со стикерами было бы ключевое слово stickers
, а сами методы возможно выглядели бы так:
stickers.get
— со следующими параметрами: pack_ids
, pack_id
, fields
;
stickers.getById
— со следующими параметрами: sticker_ids
, sticker_id
, fields
.
Так как нет возможности создавать или редактировать стикеры, которые есть во Вк, данное API будет иметь только read-only методы. Честно, сложно угадывать, и не хочется подражать разработчикам социальной сети, по этому ограничусь только придумыванием имён методов. И не буду реализовывать API в стиле Вк, хоть это бы и добавило общей идентичности расширению.
Вот такие методы буду реализовывать для работы со стикерами:
Методы для наборов:
GET /packs
GET /packs/{id}
GET /packs/{id}/stickers
Методы для стикеров:
GET /stickers
GET /stickers/{id}
GET /stickers/{id}/pack
Реализация
Как написано выше, языком для написания программирования выбран Elixir. Базой данных в проекте будет выступать PostgreSQL и для взаимодействия с ней будут использованы Postgrex
и Ecto
. В качестве web-сервера будет использован Cowboy
. За сериализацию данных в json-формат будет отвечать Poison
. Вся поставленная задача довольно не объёмная и не сложная, по этому Phoenix
использоваться не будет.
Для создания нового приложения используется команда mix new api_vk_stickers
, она создаст базовую структуру, на основе которой будет строится расширение для API Вк.
Первым делам следует отредактировать файл mix.exs
, который содержит базовую информацию о приложении и список используемых внешних зависимостей:
# mix.exs
defmodule ApiVkStickers.Mixfile do
use Mix.Project
# ...
defp deps do
[{:postgrex, "~> 0.13"},
{:ecto, "~> 2.1.1"},
{:cowboy, "~> 1.0.4"},
{:plug, "~> 1.1.0"},
{:poison, "~> 3.0"}]
end
end
После редактирования списка зависимостей необходимо их все установить, для этого предназначена команда mix deps.get
.
Теперь приступим к написанию логики самого расширения. Структура проекта будет следующая:
models/
pack.ex
sticker.ex
decorators/
pack_decorator.ex
sticker_decorator.ex
encoders/
packs_encoder.ex
stickers_encoder.ex
finders/
packs_finder.ex
stickers_finder.ex
parsers/
ids_param_parser.ex
controllers/
packs_controller.ex
stickers_controller.ex
router.ex
models
Модели создаются с использованием модуля Ecto.Schema
. В модели Pack
вместе с полем title
будет ещё несколько дополнительных не обязательных полей.
Структура модели задаётся с помощью выражения schema/2
, как аргумент она принимает имя источника, то есть название таблицы. Поля задаются в теле schema/2
с помощью выражения filed/3
. filed/3
принимает название поля, тип поля (по умолчанию :string
) и дополнительные не обязательные функции (по умолчанию []
).
Для определение связи один-ко-многим используется выражение has_many/3
.
# pack.ex
defmodule ApiVkStickers.Pack do
use Ecto.Schema
schema "packs" do
field :title
field :author
field :slug
has_many :stickers, ApiVkStickers.Sticker
end
end
Для противоположной связи один-к-одному предназначено выражение belongs_to/3
.
# sticker.ex
defmodule ApiVkStickers.Sticker do
use Ecto.Schema
schema "stickers" do
field :src, :map, virtual: true
belongs_to :pack, ApiVkStickers.Pack
end
end
decorators
В Эликсире по понятным причинам объектов нет, но всё же логика расширения моделей будет размещена в модулях с суффиксом _decorator
. API на ровне с атрибутами полученными из базы данных также будут возвращать несколько дополнительных атрибутов. Для наборов это будет коллекция обложек в двух размерах и url места, где можно добавить себе данный набор во Вк.
# pack_decorator.ex
defmodule ApiVkStickers.PackDecorator do
@storage_url "http://ift.tt/2jdN5g1"
@shop_url "https://vk.com/stickers"
def source_urls(pack) do
id = pack.id
%{small: "#{@storage_url}/#{id}/preview1_296.jpg",
large: "#{@storage_url}/#{id}/preview1_592.jpg"}
end
def showcase_url(pack) do
"#{@shop_url}/#{pack.slug}"
end
end
Для стикеров дополнительным атрибутами будет коллекция адресов картинок в четырёх вариациях.
# sticker_decorator.ex
defmodule ApiVkStickers.StickerDecorator do
@storage_url "http://ift.tt/2ieGRrZ"
def source_urls(sticker) do
id = sticker.id
%{thumb: "#{@storage_url}/#{id}/64.png",
small: "#{@storage_url}/#{id}/128.png",
medium: "#{@storage_url}/#{id}/256.png",
large: "#{@storage_url}/#{id}/512.png"}
end
end
encoders
Сериализаторы будут ответственны за преобразование атрибутов в json-формат. Первым делом из модели будет создан ассоциативный массив с базовыми атрибутами, а затем в него будут добавлены экстра атрибуты полученные из декораторов. Последним шагом будет преобразование массива в JSON с помощью модуля Poison.Encoder.Map
. Модуль PacksEncoder
будет иметь один публичный метод call/1
.
# packs_encoder.ex
defmodule ApiVkStickers.PacksEncoder do
alias ApiVkStickers.PackDecorator
defimpl Poison.Encoder, for: ApiVkStickers.Pack do
def encode(pack, options) do
Map.take(pack, [:id, :title, :author])
|> Map.put(:source_urls, PackDecorator.source_urls(pack))
|> Map.put(:showcase_url, PackDecorator.showcase_url(pack))
|> Poison.Encoder.Map.encode(options)
end
end
def call(stickers) do
Poison.encode!(stickers)
end
end
Сериализатор для стикеров будет идентичен.
# stickers_encoder.ex
defmodule ApiVkStickers.StickersEncoder do
alias ApiVkStickers.StickerDecorator
defimpl Poison.Encoder, for: ApiVkStickers.Sticker do
def encode(sticker, options) do
Map.take(sticker, [:id, :pack_id])
|> Map.put(:source_urls, StickerDecorator.source_urls(sticker))
|> Poison.Encoder.Map.encode(options)
end
end
def call(stickers) do
Poison.encode!(stickers)
end
end
finders
Для того чтобы не хранить логику запросов в базу данных в контроллерах, будут использованы файндеры (простите, искатели). Их будет также два, по количеству моделей. Файндер по наборам будет иметь три базовые функции: all/1
— получение коллекции наборов, one/1
— получение одного набора и by_ids/1
— получение коллекции наборов согласно переданным id
.
# packs_finder.ex
defmodule ApiVkStickers.PacksFinder do
import Ecto.Query
alias ApiVkStickers.{Repo, Pack}
def all(query \\ Pack) do
Repo.all(from p in query, order_by: p.id)
end
def one(id) do
Repo.get(Pack, id)
end
def by_ids(ids) do
all(from p in Pack, where: p.id in ^ids)
end
end
Похожими функциями будет обладать файндер по стикерам, за исключением третьей функции by_pack_id/1
, которая возвращает коллекцию стикеров не по их id
, а по их pack_id
.
# stickers_finder.ex
defmodule ApiVkStickers.StickersFinder do
import Ecto.Query
alias ApiVkStickers.{Repo, Sticker}
def all(query \\ Sticker) do
Repo.all(from s in query, order_by: s.id)
end
def one(id) do
Repo.get(Sticker, id)
end
def by_pack_ids(pack_ids) do
all(from s in Sticker, where: s.pack_id in ^pack_ids)
end
end
parsers
Данный сервис необходим из-за того, что не была познана практика передачи параметров в url GET-запроса таким образом, чтобы Plug
автоматически представлял мне массив. И вообще как-то создавал для переданного набора id
какую-то переменную, без указания принимаемых параметров в выражении get/3
модуля Plug.Router
.
# ids_param_parser.ex
defmodule ApiVkStickers.IdsParamParser do
def call(query_string, param_name \\ "ids") do
ids = Plug.Conn.Query.decode(query_string)[param_name]
if ids do
String.split(ids, ",")
end
end
end
controllers
Контроллеры будут на основе модуля Plug.Router
, DSL которого многим напомнит фреймворк Sinatra. Но прежде чем приступить к самим контроллерам, необходимо собрать модуль который будет отвечать за маршруты.
defmodule ApiVkStickers.Router do
use Plug.Router
plug Plug.Logger
plug :match
plug :dispatch
forward "/packs", to: ApiVkStickers.PacksController
forward "/stickers", to: ApiVkStickers.StickersController
match _ do
conn
|> put_resp_content_type("application/json")
|> send_resp(404, ~s{"error":"not found"}))
end
end
Контроллеры по сути дела тоже будут такими же маршрутными модулями, но в душе есть вера, в то, что размещение этих модулей в папку controllers
было правильным решением.
# packs_controller
defmodule ApiVkStickers.PacksController do
# ...
get "/" do
ids = IdsParamParser.call(conn.query_string)
packs = if ids do
PacksFinder.by_ids(ids)
else
PacksFinder.all
end
|> PacksEncoder.call
send_json_resp(conn, packs)
end
get "/:id" do
pack = PacksFinder.one(id)
|> PacksEncoder.call
send_json_resp(conn, pack)
end
get "/:id/stickers" do
stickers = StickersFinder.by_pack_ids([id])
|> StickersEncoder.call
send_json_resp(conn, stickers)
end
# ...
end
# stickers_controller
defmodule ApiVkStickers.StickersController do
# ...
get "/" do
pack_ids = IdsParamParser.call(conn.query_string, "pack_ids")
stickers = if pack_ids do
StickersFinder.by_pack_ids(pack_ids)
else
StickersFinder.all
end
|> StickersEncoder.call
send_json_resp(conn, stickers)
end
get "/:id" do
sticker = StickersFinder.one(id)
|> StickersEncoder.call
send_json_resp(conn, sticker)
end
get "/:id/pack" do
sticker = StickersFinder.one(id)
pack = PacksFinder.one(sticker.pack_id)
|> PacksEncoder.call
send_json_resp(conn, pack)
end
# ...
end
Результат
[{"title":"Спотти", "source_urls":{"small":"http://ift.tt/2iewW5Q", "large":"http://ift.tt/2jdIhr4"}, "showcase_url":"http://ift.tt/2iewDYI", "id":1,"author":"Андрей Яковенко"}, {"title":"Персик", "source_urls":{"small":"http://ift.tt/2jdOvY1", "large":"http://ift.tt/2iet84q"}, "showcase_url":"http://ift.tt/2jdMv1N", "id":2,"author":"Елена Савченко"}, {"title":"Смайлы", "source_urls":{"small":"http://ift.tt/2ieD1zh", "large":"http://ift.tt/2jdSBiT"}, "showcase_url":"http://ift.tt/2iepgRe", "id":3,"author":"Елена Савченко"}, {"title":"Фруктовощи", "source_urls":{"small":"http://ift.tt/2jdIkDh", "large":"http://ift.tt/2ieDfqb"}, "showcase_url":"http://ift.tt/2jdRIGU", "id":4,"author":"Андрей Яковенко"}]
[{"title":"Персик", "source_urls":{"small":"http://ift.tt/2jdOvY1", "large":"http://ift.tt/2iet84q"},"showcase_url":"http://ift.tt/2jdMv1N", "id":2,"author":"Елена Савченко"}, {"title":"Смайлы", "source_urls":{"small":"http://ift.tt/2ieD1zh", "large":"http://ift.tt/2jdSBiT"},"showcase_url":"http://ift.tt/2iepgRe", "id":3,"author":"Елена Савченко"}]
{"title":"Спотти", "source_urls":{"small":"http://ift.tt/2iewW5Q", "large":"http://ift.tt/2jdIhr4"}, "showcase_url":"http://ift.tt/2iewDYI", "id":1,"author":"Андрей Яковенко"}
[{"source_urls":{"thumb":"http://ift.tt/2jdNor0", "small":"http://ift.tt/2iepO9H", "medium":"http://ift.tt/2jdWowA", "large":"http://ift.tt/2iexCYR"}, "pack_id":1,"id":1},...,{"source_urls":{"thumb":"http://ift.tt/2jdLpmS", "small":"http://ift.tt/2iewYdY", "medium":"http://ift.tt/2jdN6AB", "large":"http://ift.tt/2ieCHjS"}, "pack_id":1,"id":48}]
[{"source_urls":{"thumb":"http://ift.tt/2jdNor0", "small":"http://ift.tt/2iepO9H", "medium":"http://ift.tt/2jdWowA", "large":"http://ift.tt/2iexCYR"}, "pack_id":1,"id":1}, {"source_urls":{"thumb":"http://ift.tt/2ieCZHF", "small":"http://ift.tt/2jdTnwb", "medium":"http://ift.tt/2iey4X5", "large":"http://ift.tt/2jdJbng"}, "pack_id":1,"id":2}, {"source_urls":{"thumb":"http://ift.tt/2ieyQDA", "small":"http://ift.tt/2jdAN7k", "medium":"http://ift.tt/2iewEfe", "large":"http://ift.tt/2jdOH9l"}, "pack_id":1,"id":3},...,{"source_urls":{"thumb":"http://ift.tt/2ieGTQD", "small":"http://ift.tt/2jdDtSj", "medium":"http://ift.tt/2ieDEsm", "large":"http://ift.tt/2jdPbwk"}, "pack_id":4,"id":167}, {"source_urls":{"thumb":"http://ift.tt/2iexMPP", "small":"http://ift.tt/2jdOpzm", "medium":"http://ift.tt/2iepOqd", "large":"http://ift.tt/2jdOItV"}, "pack_id":4,"id":168}]
[{"source_urls":{"thumb":"http://ift.tt/2jdIhHA", "small":"http://ift.tt/2iewrbZ", "medium":"http://ift.tt/2jdOyTH", "large":"http://ift.tt/2ieCFsg"},"pack_id":2,"id":49}, ..., {"source_urls":{"thumb":"http://ift.tt/2jdPF5B", "small":"http://ift.tt/2iet58U", "medium":"http://ift.tt/2jdOKC2", "large":"http://ift.tt/2ierBLM"},"pack_id":3,"id":128}]
{"source_urls":{"thumb":"http://ift.tt/2jdNor0", "small":"http://ift.tt/2iepO9H", "medium":"http://ift.tt/2jdWowA", "large":"http://ift.tt/2iexCYR"}, "pack_id":1,"id":1}
{"title":"Спотти", "source_urls":{"small":"http://ift.tt/2iewW5Q", "large":"http://ift.tt/2jdIhr4"}, "showcase_url":"http://ift.tt/2iewDYI", "id":1,"author":"Андрей Яковенко"}
Послесловие
Из проекта можно убрать PostgreSQL. В таком случае все данные о наборах стикеров будут храниться в коде включая данные об интервале принадлежащих им стикеров. Проект не сильно упростится, но в скорость базы данных вы уже не уткнётесь точно.
- Если вам интересен функциональный язык программирования Elixir или вы просто сочувствующий то советую вам присоединиться к Telegram-каналу про Elixir.
- У отечественного Elixir сообщества начинает появляться единая площадка в лице проекта Wunsh.ru. Сейчас ребята во всю пишут новую версию сайта. Но уже у них есть подписка на рассылку. В ней нет ничего нелегального, раз в недельку будет приходить письмо с подборкой статей про Elixir на русском языке.
Если вам интересна тема создания своих приложений на Elixir, могу посоветовать статью: Создание Elixir-приложения на примере. От инициализации до публикации.
This article passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Recommended article: The Guardian's Summary of Julian Assange's Interview Went Viral and Was Completely False.
Комментарии (0)