There are sticker sets in VK, some of which are even free. But in VK there is not any public API for using this functionality on third-party sites. The task is to use the functional language Elixir to write an extension above the storage location of stickers in VK as an API .
In my opinion, the names of the methods, and the parameters that they took would be next. The common namespace for the collection of API methods for working with stickers would be the keyword stickers
, and the methods themselves would probably look like this:
stickers.get
- with the following parameters: pack_ids
, pack_id
, fields
;stickers.getById
- with the following parameters: sticker_ids
, sticker_id
, fields
.
Since there is no possibility to create or edit stickers that are in VK, this API will have only read-only methods. Honestly, it is difficult to guess, and do not want to imitate the developers of a social network, for this I will confine myself only to inventing the names of methods. And I will not implement the API in the style of VK, even though it would add a common identity to the extension.
These are the methods I will implement to work with stickers:
Methods for sets:
GET /packs GET /packs/{id} GET /packs/{id}/stickers
Methods for stickers:
GET /stickers GET /stickers/{id} GET /stickers/{id}/pack
As written above, Elixir is selected as the programming language. The database in the project will be PostgreSQL and Postgrex
and Ecto
will be used to interact with it. Cowboy
will be used as a web server. For the serialization of data in json- format will be responsible Poison
. The whole task is not quite voluminous and not difficult, for this Phoenix
will not be used.
To create a new application, use the mix new api_vk_stickers
, it will create a basic structure, on the basis of which an extension for the VK API will be built.
The first mix.exs
do is edit the mix.exs
file, which contains basic information about the application and a list of external dependencies used:
# 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
After editing the list of dependencies, you need to install them all, for this the mix deps.get
command is mix deps.get
.
Now let's start writing the logic of the extension itself. The project structure will be as follows:
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 are created using the Ecto.Schema
module. In the Pack
model, along with the title
field, there will be some more optional non-mandatory fields.
The model structure is specified using the expression schema/2
, as an argument it takes the name of the source, that is, the name of the table. Fields are specified in the body of schema/2
using the expression filed/3
. filed/3
accepts the field name, field type (default :string
) and optional optional functions (default []
).
To define a one-to-many relationship, use the expression 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
For the opposite one-to-one relation, the expression belongs_to/3
intended.
# sticker.ex defmodule ApiVkStickers.Sticker do use Ecto.Schema schema "stickers" do field :src, :map, virtual: true belongs_to :pack, ApiVkStickers.Pack end end
For obvious reasons, there are no objects in the Elixir, but the logic of model expansion will be placed in modules with the _decorator
suffix. The API, along with the attributes obtained from the database, will also return several additional attributes. For sets, this will be a collection of covers in two sizes and the url of the place where you can add yourself this set in VK.
# pack_decorator.ex defmodule ApiVkStickers.PackDecorator do @storage_url "https://vk.com/images/store/stickers" @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
For stickers, additional attributes will be a collection of image addresses in four variations.
# sticker_decorator.ex defmodule ApiVkStickers.StickerDecorator do @storage_url "https://vk.com/images/stickers" 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
Serializers will be responsible for converting attributes to json format. First of all, an associative array with basic attributes will be created from the model, and then extra attributes obtained from decorators will be added to it. The final step is to convert the array to JSON using the Poison.Encoder.Map
module. The PacksEncoder
module will have one public call/1
method.
# 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
The serializer for stickers will be identical.
# 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
In order not to store the query logic to the database in the controllers, the fader will be used (sorry, searchers). They will also be two, according to the number of models. The ender for sets will have three basic functions: all/1
- getting the collection of sets, one/1
- getting one set and by_ids/1
- getting the collection of sets according to the transferred 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
Similar functions will be provided by the filer by stickers, with the exception of the third function by_pack_id/1
, which returns a collection of stickers not by their id
, but by their 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
This service is necessary due to the fact that the practice of passing parameters to the url of a GET request was not known in such a way that the Plug
automatically presents me an array. And in general, somehow I created some variable for the passed id
set, without specifying the parameters to be received in the Plug.Router
get/3
Plug.Router
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
The controllers will be based on the Plug.Router
module, whose DSL will resemble the Sinatra framework to many. But before proceeding to the controllers themselves, it is necessary to assemble a module that will be responsible for the routes.
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
The controllers, in fact, will also be the same routing modules, but in the soul there is a belief that placing these modules in the controllers
folder was the right decision.
# 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":"https://vk.com/images/store/stickers/1/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/1/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/spotty", "id":1,"author":" "}, {"title":"", "source_urls":{"small":"https://vk.com/images/store/stickers/2/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/2/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/persik", "id":2,"author":" "}, {"title":"", "source_urls":{"small":"https://vk.com/images/store/stickers/3/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/3/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/smilies", "id":3,"author":" "}, {"title":"", "source_urls":{"small":"https://vk.com/images/store/stickers/4/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/4/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/fruitables", "id":4,"author":" "}]
[{"title":"", "source_urls":{"small":"https://vk.com/images/store/stickers/2/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/2/preview1_592.jpg"},"showcase_url":"https://vk.com/stickers/persik", "id":2,"author":" "}, {"title":"", "source_urls":{"small":"https://vk.com/images/store/stickers/3/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/3/preview1_592.jpg"},"showcase_url":"https://vk.com/stickers/smilies", "id":3,"author":" "}]
{"title":"", "source_urls":{"small":"https://vk.com/images/store/stickers/1/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/1/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/spotty", "id":1,"author":" "}
[{"source_urls":{"thumb":"https://vk.com/images/stickers/1/64.png", "small":"https://vk.com/images/stickers/1/128.png", "medium":"https://vk.com/images/stickers/1/256.png", "large":"https://vk.com/images/stickers/1/512.png"}, "pack_id":1,"id":1},...,{"source_urls":{"thumb":"https://vk.com/images/stickers/48/64.png", "small":"https://vk.com/images/stickers/48/128.png", "medium":"https://vk.com/images/stickers/48/256.png", "large":"https://vk.com/images/stickers/48/512.png"}, "pack_id":1,"id":48}]
[{"source_urls":{"thumb":"https://vk.com/images/stickers/1/64.png", "small":"https://vk.com/images/stickers/1/128.png", "medium":"https://vk.com/images/stickers/1/256.png", "large":"https://vk.com/images/stickers/1/512.png"}, "pack_id":1,"id":1}, {"source_urls":{"thumb":"https://vk.com/images/stickers/2/64.png", "small":"https://vk.com/images/stickers/2/128.png", "medium":"https://vk.com/images/stickers/2/256.png", "large":"https://vk.com/images/stickers/2/512.png"}, "pack_id":1,"id":2}, {"source_urls":{"thumb":"https://vk.com/images/stickers/3/64.png", "small":"https://vk.com/images/stickers/3/128.png", "medium":"https://vk.com/images/stickers/3/256.png", "large":"https://vk.com/images/stickers/3/512.png"}, "pack_id":1,"id":3},...,{"source_urls":{"thumb":"https://vk.com/images/stickers/167/64.png", "small":"https://vk.com/images/stickers/167/128.png", "medium":"https://vk.com/images/stickers/167/256.png", "large":"https://vk.com/images/stickers/167/512.png"}, "pack_id":4,"id":167}, {"source_urls":{"thumb":"https://vk.com/images/stickers/168/64.png", "small":"https://vk.com/images/stickers/168/128.png", "medium":"https://vk.com/images/stickers/168/256.png", "large":"https://vk.com/images/stickers/168/512.png"}, "pack_id":4,"id":168}]
[{"source_urls":{"thumb":"https://vk.com/images/stickers/49/64.png", "small":"https://vk.com/images/stickers/49/128.png", "medium":"https://vk.com/images/stickers/49/256.png", "large":"https://vk.com/images/stickers/49/512.png"},"pack_id":2,"id":49}, ..., {"source_urls":{"thumb":"https://vk.com/images/stickers/128/64.png", "small":"https://vk.com/images/stickers/128/128.png", "medium":"https://vk.com/images/stickers/128/256.png", "large":"https://vk.com/images/stickers/128/512.png"},"pack_id":3,"id":128}]
{"source_urls":{"thumb":"https://vk.com/images/stickers/1/64.png", "small":"https://vk.com/images/stickers/1/128.png", "medium":"https://vk.com/images/stickers/1/256.png", "large":"https://vk.com/images/stickers/1/512.png"}, "pack_id":1,"id":1}
{"title":"", "source_urls":{"small":"https://vk.com/images/store/stickers/1/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/1/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/spotty", "id":1,"author":" "}
PostgreSQL can be removed from the project. In this case, all data on sticker sets will be stored in the code, including data on the interval of stickers belonging to them. The project is not much simpler, but in the database speed, you will not be completely sure.
If you are interested in the functional programming language Elixir or you are just sympathetic, then I advise you to join the Wunsh && Elixir and ProElixir Telegram chat rooms.
If you are interested in the topic of creating your own applications on Elixir , I can advise the article: Creating an Elixir application using an example. From initialization to publication https://habrahabr.ru/post/317444/ .
Source: https://habr.com/ru/post/318918/
All Articles