The functional programming language Elixir is gaining popularity, and one of the latest frameworks for creating single-page applications - Angular 2 - has recently been released. Let's take a look at them in a couple of articles by creating a full-fledged back-end on Elixir and Phoenix Framework from scratch, supplying data to the frontend client application based on Angular 2.
Hello, world
is not our option, so what was done if necessary can be applied in real projects: all the presented code is laid out under the MIT license .
The volume of the article big huge! I hope for an equally huge amount of comments - any. Not once noticed that you get from the comments no less than from the main article, and sometimes more.
The first article will contain several introductory words and work on the back-end. Go!
A few months ago, I was offered as a subcontractor to implement in a very short time a prototype of a web application; of the requirements were present only the functional, the extreme end date and the need to use only open source tools. βExcellent,β I thought, βthis is a great reason to put into practice the Elixir / Phoenix Framework and Angular 2 bundle,β since the latter was released shortly before. The project as a result was implemented on time, the customer was satisfied, the experience was replenished with the implementation of new tasks.
One of these tasks was the need to display reference books of the GRNTI and OECD FOS with the possibility of choosing several values. Since there were no ready-made solutions for outputting a tree directory of normal readiness, I had to reinvent my own bicycle. In addition, it provided a topic for this series of βtutorialβ articles for acquaintance with both the Elixir / Phoenix Framework and Angular 2 simultaneously.
So, at the end of this cycle, we will have a working back-end on Elixir and Phoenix Framework, using the API to provide the contents of the GRNTI and OECD FOS directories to an independent front-end on Angular 2, where you can call the output form of the multiple-choice data sections \ subsections, save (get outside of the selection window) and restore selected when opened. Appearance will provide us with Twitter Bootstrap. We shall design the reference book on the front-end as a separate module that can be used in the future with any projects.
The GRNTI Reference Book is a three-level (maximum) structure, each entry of which has a code in the decimal classification , consisting of three groups of numbers from 00 to 99, separated by a period, as well as a name. The reference book currently includes about 8,000 records of sections and subsections, the volume in a flat text form - more than 400 kb. The contents of the directory can be found on grnti.ru (I have no relation to this resource).
OECD FOS also has a three-level structure with a hierarchical code separated by a dot, however, unlike the previous version, in this case the last group of code is a combination of two Latin letters. There are significantly fewer entries in the directory - just under 300, and the total is about 8kb. Unfortunately, online I could not find at least some of the current versions of this reference book, so we will use the fact that it was found through other channels.
Due to the volume of the GRNTI reference book, when working with it, we will request with the back-end only those sections that we need at the moment, but the OECD FOS can be rendered as a whole and process the structure already on the client.
I want to clarify right away: the problem is somewhat degenerate, it was only part of a wider functional. Naturally, if the task is only to display reference books (for example, for informational purposes), then neither the back-end nor the SPA are needed.
Also, I do not pretend to be a guru at all, and if you offer a more effective implementation of any part, I will be grateful for the science: I love to study.
Please note: the code, the output of utilities and some advanced explanations are hidden under the spoiler in order to somehow improve the readability and use.
Nowadays, vertical power expansion costs more and more, and performance is achieved not by increasing the frequency, but horizontally, by adding new cores. Because of this, languages ββspecializing in competitive computing (parallel execution) are of increasing interest. At the same time, sharing of common data becomes a serious headache, which can be significantly reduced by functional programming languages.
Elixir is a fairly young, byte-codeable functional language written in Erlang and executed in its Beam virtual machine. The language inherits all the advantages of Erlang :
and at the same time it has a simpler syntax, somewhat similar to Ruby, polymorphism through the protocol mechanism, very rich meta-programming capabilities, opportunities for easy creation of documentation with Markdown markup and real examples testing (!!!) directly in the code of modules . It is important that all Erlang functions and libraries written for it can be called directly from Elixir code without any loss of performance.
The language simply provokes the use of multiprocessing and messaging, the benefit of writing a full-fledged process module with messaging needs to be spent literally a couple of minutes (I hope we will return to this in future publications if the response to this cycle is positive).
Much means the active participation of the author of the language - JosΓ© Valim - in the life of the community. He is happy to answer questions in detail and, if necessary, brings the missing functionality to the language / library (for example, in Ecto, which will be discussed further - there is a personal positive experience).
The Phoenix Framework is the most popular web framework for Elixir, which implements the MVC pattern and greatly simplifies the development of web applications. In addition, Phoenix has Channels - the possibility of realtime communications with the application via websockets, and this is a real killer feature. There is a JavaScript component for use with browsers, as well as implementations for some other languages, for example, for Java under Android.
Angular 2 is a framework for developing the client side of single page web applications, mainly supported by Google. Version two has been completely rewritten based on the experience gained during the development and operation of AngularJS. The release was released in September 2016.
If you have not encountered Elixir before, I highly recommend starting with learning a language. I plan to give detailed explanations of the approaches and features used, but it is impossible to cover everything fully within a small series of articles (except for the fact that I myself constantly find something new). There is a basic introduction to the project site for studying, as well as many other resources on the Internet. A very useful forum where the creator of the language responds with great pleasure. By the way, an excellent example answer to the question "is it worth it to study Elixir first or immediately rush to the embrasure" can be seen in this thread on Reddit. As a summary, I can say that the author of the topic was disappointed by the insignificant difference in the performance of the test case written in Ruby and "on Elixir" (as he believed). The code on Ruby was executed in 4.221 seconds, on Elixir - 5.923 seconds. After the code was rewritten using language features (and not just porting one-to-one with Ruby), he began working three (!!!) times faster.
Having said the right things, I will say a seditious one: I rarely do this myself, and usually I rush into battle immediately.
To manage the versions of Erlang, Elixir and node (which will be needed later to work with the front-end), I use the asdf package manager. There is a great gist in which the installation of dependencies for Fedora and Ubuntu, asdf, Erlang and Elixir is described in great detail, so I will not repeat. He is in English, but there is enough copy-paste. The latest versions at the time of this writing: Erlang - 19.2, Elixir - 1.4.1.
You will also need PostgreSQL with one of the latest versions (I am using 9.6 at the moment), which you can install using the standard package managers of your distribution \ OS.
After installing Erlang and Elixir, you need to install the Phoenix Framework.
In Elixir for creating, compiling, testing projects, as well as managing their dependencies, there is a special automation utility - Mix (you should also pay attention to this part of the documentation). The mix
utility is just like make
, just more convenient.
To begin with, we will install (regular) package manager with Hex command
$ mix local.hex
and then the Phoenix Framework archive:
$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez
The documentation mentions the need for node.js, but since in this case Phoenix will only provide the API, we will need the node later when we move on to Angular 2.
At the time of this writing, the Phoenix Framework version 1.2.1 was relevant.
It should say a few words about Hex . There is a single https://hex.pm repository in which libraries for Elixir / Erlang are published. By default, all dependencies of projects on Elixir are searched there.
Having installed the necessary software, go to the directory we need and create a new Phoenix project by running:
$ mix phoenix.new atv_api --no-brunch --no-html * creating atv_api/config/config.exs * creating atv_api/config/dev.exs * creating atv_api/config/prod.exs ... * creating atv_api/priv/static/images/phoenix.png * creating atv_api/priv/static/favicon.ico Fetch and install dependencies? [Yn] y * running mix deps.get We are all set! Run your Phoenix application: $ cd atv_api $ mix phoenix.server You can also run your app inside IEx (Interactive Elixir) as: $ iex -S mix phoenix.server Before moving on, configure your database in config/dev.exs and run: $ mix ecto.create $
We are creating a new project without html templates and without the support of brunch , since the client part will be in a separate project.
I advise you to immediately change the database connection settings in the config files config/prod.exs
, config/dev.exs
and config/test.exs
for the production, development and testing modes, respectively.
In addition, if you are using Elixir version 1.4 and above and the current version of Phoenix is ββstill 1.2.1, I recommend changing the mix.exs
file mix.exs
. Elixir 1.4 brought several new features , in particular, simplified the addition of dependencies that have their own process trees that require starting at the start of the project. If earlier such dependencies (and most of them) were needed to be added both to the list of dependencies ( deps
) and to the list of applications for launching ( applications
), now now it is enough just the first one: mix
will figure out whether the dependency is an application and launch it. You must specify only applications that are not listed in dependencies. Let's bring the method that returns the description of the application to the following form:
# mix.exs ... # Configuration for the OTP application. # # Type `mix help compile.app` for more information. def application do [mod: {AtvApi, []}, extra_applications: [:logger]] end ...
If you compare it with what it was before, you will see that the list that had the key :applications
disappeared and a new - :extra_applications
, in which only :logger
remained, and everything that is listed in the dependencies is excluded.
Having done this, start database creation using mix ecto.create
. The default environment is dev
, respectively, a database will be created for this environment, the standard name for which will be atv_api_dev
.
At any time you can remove the database by running the mix ecto.drop
task. At the same time, mix ecto.reset
will delete the database, create a new one, start the migrations and execute the contents of seeds.exs
to fill the data for the first time (more on the latter later).
By adding a variable initialized with the desired value β MIX_ENV=prod
, MIX_ENV=dev
(by default) or MIX_ENV=test
β before mix
, you can perform the required tasks in the appropriate environment.
Since the OECD FOS directory is simpler, let's start with it.
To work with data in Phoenix, the Ecto library is used . Ecto is a DSL for working with database tables through views (models) and for writing database queries. Ecto, unlike Rails ActiveRecords, is very simple (takes on a minimum), but at the same time a powerful tool.
In the Phoenix Framework, there are code generators of various types that can create a complete set of migration, model, controller that implements CRUD , view modules (view), generating json, as well as basic tests. In the case of both directories, we do not need a full-fledged CRUD, but for OECD FOS, you can start with the generated code and remove the excess.
In the table of the OECD FOS directory, we will have two fields: id
and title
, both with type text
. (Why text
? Because if there is no difference , then why be sprayed?)
Let's use the generator and conduct:
$ mix phoenix.gen.json Fos fos title:text * creating web/controllers/fos_controller.ex * creating web/views/fos_view.ex * creating test/controllers/fos_controller_test.exs * creating web/views/changeset_view.ex * creating web/models/fos.ex * creating test/models/fos_test.exs * creating priv/repo/migrations/20170215194144_create_fos.exs Add the resource to your api scope in web/router.ex: resources "/fos", FosController, except: [:new, :edit] Remember to update your repository by running migrations: $ mix ecto.migrate
Here phoenix.gen.json
is the task of the mix (mix task) utility, Fos
is the model name in the singular, fos
is the name for the table, according to the convention there should be a model name with a small letter in the plural, and then comes the description of the fields. In this case, we want to see in the model a field with the name title
and type text
(this is the PostgreSQL data type). About the id
field we will talk a bit later. The mix task list can be obtained by running the mix help
command, and for more information about phoenix tasks, see the documentation .
After the completion of the command, we will be asked to add the line resources "/fos", FosController, except: [:new, :edit]
web/router.ex
resources "/fos", FosController, except: [:new, :edit]
to the web/router.ex
. Let's do it for now (we'll change it later):
defmodule AtvApi.Router do use AtvApi.Web, :router pipeline :api do plug :accepts, ["json"] end scope "/api", AtvApi do pipe_through :api resources "/fos", FosController, except: [:new, :edit] end end
You will also be prompted to start the migration process, but do not hurry. By default, Ecto generates a model and migration (a script to create a table in the database) with an auto-increment id
field of integer
type as the primary key, but we do not need this type of field, since we will use the section code as the key. Let's change this behavior for our model.
Let's start with the migration file. The model generator creates migrations to the priv/repo/migrations
directories. Open the file that ends with _create_fos.exs
and bring it to the following view:
defmodule AtvApi.Repo.Migrations.CreateFos do use Ecto.Migration def change do create table(:fos, primary_key: false) do add :id, :text, null: false, primary_key: true add :title, :text timestamps() end end end
The code in Elixir is organized in the form of modules and functions . Each module is defined by the macro defmodule
, the description of the functions by the macro def
or defp
. Don't pay attention to use
yet, we'll come back to this later. This migration module is called AtvApi.Repo.Migrations.CreateFos
, and it is formed for convenience based on the convention. The language does not force to give such names, and the language does not oblige you to have the whole chain with the "parent" modules such as AtvApi.Repo.Migrations
and AtvApi.Repo
.
We added the primary_key: false
option to the create/2
table creation macro. By this we cancel the creation of the standard id
field and below we manually add a field with the same name, but with the text
type, which will become the primary key.
Let's correct the description of the model, located in the web/models
directory:
defmodule AtvApi.Fos do use AtvApi.Web, :model @primary_key {:id, :string, autogenerate: false} schema "fos" do field :title, :string timestamps() end @doc """ Builds a changeset based on the `struct` and `params`. """ def changeset(struct, params \\ %{}) do struct |> cast(params, [:id, :title]) |> validate_required([:id, :title]) end end
Notice that we have added the @primary_key
constant with the description of the primary key. We also added an atom with the name of the field :id
to the list of allowed changes (see the description of the function cast/3
, the last parameter allowed
) - otherwise we will not be able to add a field with the code specified by us to the changeset; The same atom is added to the list of validator function validate_required/2
, which, as the name implies, checks for the presence of the corresponding field in the changeset and, if it is not present, marks the set as erroneous.
It is worth noting the call to the timestamps/1
macro , which adds the inserted_at
and updated_at
fields of the timestamp
type to the model schema. The first field is initialized with the current time at the moment of creation, the second - with each change of the record by the Ecto
functions.
Here you also need to say a few words about what the model is.
In Elixir, there is the concept of "structure" ( struct
). A structure is an extension of an associative array (i.e., storage of key-value pairs, standardly denoted as %{ key => value, ...}
, and if the key is an atom, then %{ key: value, ...}
); the structure has an additional key __struct__
, the value of which contains its name, and is limited to only those fields that are specified in the code at the time of compilation . If you try to add a value to a structure with a key that is not described in it at the time of compilation, an error will be generated. The structure is determined using the defstruct
construct and gets the name of the module in which it is described:
iex> defmodule User do ...> defstruct title: "John", age: 27 ...> end
The list of keywords used with defstruct
defines the fields that this structure can and will contain, along with their default values. In the example above, the structure will be named %User{}
.
As mentioned, the structure is an extension of the associative array, so all the functions of the Map
module will work with them. However, the Enumerable
protocol for structures is not implemented, so the Enum
module will not work with them.
It remains to add that a model is a structure obtained using meta-programming (more on this later) from the description contained in the do ... end
block of the macro macro.Our model described in the module AtvApi.Fos
will be of type %Fos{}
and contain key fields :id
(by default) and :title
(explicitly defined).
For more detailed information on structures, welcome to the documentation .
, :
$ mix test test/models/fos_test.exs Compiling 7 files (.ex) Generated atv_api app 1) test changeset with valid attributes (AtvApi.FosTest) test/models/fos_test.exs:9 Expected truthy, got false code: changeset.valid?() stacktrace: test/models/fos_test.exs:11: (test) . Finished in 0.05 seconds 2 tests, 1 failure Randomized with seed 166025
test/models/fos_test.exs
, , @valid_attrs
, id
. , - id
, . β . :
@valid_attrs %{title: "Humanities, multidisciplinary", id: "0605BQ"}
:
$ mix test test/models/fos_test.exs .. Finished in 0.04 seconds 2 tests, 0 failures Randomized with seed 892257
, , , :
$ mix ecto.migrate 17:54:26.080 [info] == Running AtvApi.Repo.Migrations.CreateFos.change/0 forward 17:54:26.080 [info] create table fos 17:54:26.097 [info] == Migrated in 0.0s
, mix ecto.rollback
.
. , .
priv/repo/seeds.exs
. oecd_fos.txt
grnti.txt
( ) priv/repo
. . :
require Logger alias AtvApi.Repo import Ecto.Query ### OECD FOS dictionary ### alias AtvApi.Fos unless Repo.one!(from f in Fos, select: count(f.id)) > 0 do multi = File.read!("priv/repo/oecd_fos.txt") |> String.split("\n") |> Enum.reject(fn(row) -> byte_size(row) < 1 end) |> Enum.sort |> Enum.dedup |> Enum.reduce(Ecto.Multi.new, fn(row, multi) -> [id, title] = row |> String.trim |> String.split(";") changeset = Fos.changeset(%Fos{}, %{id: id, title: title}) Ecto.Multi.insert(multi, id, changeset) end) Repo.transaction(multi) Logger.info "OECD FOS load complete" end ### OECD FOS dictionary ###
. ( require ) Logger .
Elixir - (.. , ). β , (.. ) . , , , . require
.
alias
, Repo
AtvApi.Repo
. β import
β ( β Ecto.Query, ). (, , ) , only: [function_title: arity]
, , : import Ecto.Query, only: [from: 2]
(arity β ). β - , . β () , , , , . .
seeds.exs
, . Repo.one!/2 , SQL SELECT COUNT(f.id) FROM fos AS f
, , .
, [] , ( tuple ) {:ok, result}
{:error, description}
, , , , .
For example:
iex> File.read("file.txt") {:ok, "file contents"} iex> File.read("no_such_file.txt") {:error, :enoent} iex> File.read!("file.txt") "file contents" iex> File.read!("no_such_file.txt") ** (File.Error) could not read file no_such_file.txt: no such file or directory
, ( pipe operator ). . , (2) (3) , (4) (5), :
iex(1)> some_map = %{one: 1} %{one: 1} iex(2)> Enum.count(some_map) 1 iex(3)> some_map |> Enum.count() 1 iex(4)> Enum.count(Map.put(some_map, :two, 2)) 2 iex(5)> some_map |> Map.put(:two, 2) |> Enum.count() 2
, :
File.read!/1
), ,String.split/3
, \n
,Enum.reject/2
, , false
,Enum.sort/1
,Enum.dedup/1
,Enum.reduce/3
, .Enum.reduce(enumerable, acc, fun)
, Enum , , Enumerable (), , , . , . Enum.reduce/3
.
String.trim/1
, white-space ; String.split/3
, . oecd_fos.txt
, . String.split/3
, . (pattern matching) id
, β title
.
( pattern matching ) β Elixir. , , =
( ) β , (match operator). , :
iex> x = 1 1 iex> x 1
, :
iex> 1 = x 1 iex> 2 = x ** (MatchError) no match of right hand side value: 1
x
1, .
, .
:
iex> {a, b, c} = {:hello, "world", 42} {:hello, "world", 42} iex> a :hello iex> b "world"
:
iex> {a, b, {d, e} = c} = {:hello, "world", {:grey, "hole"}} {:hello, "world", {:grey, "hole"}} iex> a :hello iex> b "world" iex> c {:grey, "hole"} iex> d :grey iex> e "hole"
, . , :
iex> {a, b, c} = {:hello, "world"} ** (MatchError) no match of right hand side value: {:hello, "world"}
, :
iex> {a, b, c} = [:hello, "world", 42] ** (MatchError) no match of right hand side value: [:hello, "world", 42]
, . , :ok
:
iex> {:ok, result} = {:ok, 13} {:ok, 13} iex> result 13 iex> {:ok, result} = {:error, :oops} ** (MatchError) no match of right hand side value: {:error, :oops}
, .
- (pin operator). , , , , Elixir , . , ? pin operator:
iex> x = 1 1 iex> ^x = 2 ** (MatchError) no match of right hand side value: 2 iex> {y, ^x} = {2, 1} {2, 1} iex> y 2 iex> {y, ^x} = {2, 2} ** (MatchError) no match of right hand side value: {2, 2}
x 1, :
iex> {y, 1} = {2, 2} ** (MatchError) no match of right hand side value: {2, 2}
( changeset
) AtvApi.Fos.changeset/2
, OECD FOS (, , , :id
). Ecto.Changeset
.
, Ecto.Changeset
, , () () (constraints) ( , β ). Ecto.Changeset
" ", .. changeset
. changeset
cast/3
change/2
. , , , , API, .., β . , , ( ) .
AtvApi.Fos.changeset/2
, Ecto.Changeset
, β cast/3
β ( struct
), ( params
) , , , ( [:id, :title]
). , , ( , ). , , :id
:
|> validate_length(:id, min: 6)
, , valid?
false
. .
For example:
iex> valid = AtvApi.Fos.changeset(%AtvApi.Fos{}, %{id: "123", title: "Some title"}) #Ecto.Changeset<action: nil, changes: %{id: "123", title: "Some title"}, errors: [], data: #AtvApi.Fos<>, valid?: true> iex> invalid = valid |> Ecto.Changeset.validate_length(:id, min: 6) #Ecto.Changeset<action: nil, changes: %{id: "123", title: "Some title"}, errors: [id: {"should be at least %{count} character(s)", [count: 6, validation: :length, min: 6]}], data: #AtvApi.Fos<>, valid?: false> iex> AtvApi.Repo.insert!(invalid) # "" , ** (Ecto.InvalidChangesetError) could not perform insert because changeset is invalid. Applied changes %{id: "123", title: "Some title"} Params %{"id" => "123", "title" => "Some title"} Errors %{id: [{"should be at least %{count} character(s)", [count: 6, validation: :length, min: 6]}]} Changeset #Ecto.Changeset<action: :insert, changes: %{id: "123", title: "Some title"}, errors: [id: {"should be at least %{count} character(s)", [count: 6, validation: :length, min: 6]}], data: #AtvApi.Fos<>, valid?: false> (ecto) lib/ecto/repo/schema.ex:134: Ecto.Repo.Schema.insert!/4 iex> AtvApi.Repo.insert(invalid) # "" , {:error, description} {:error, #Ecto.Changeset<action: :insert, changes: %{id: "123", title: "Some title"}, errors: [id: {"should be at least %{count} character(s)", [count: 6, validation: :length, min: 6]}], data: #AtvApi.Fos<>, valid?: false>}
, , , .
insert* Ecto.Repo
. , ( use
) AtvApi.Repo
, AtvApi.Repo.insert ...
, . , Enum.reduce/3
( β Enum.each/2
), , - ? () . , . Ecto.Repo.transaction/2
, , , Ecto.Multi
, . , Ecto.Multi
, , Enum.reduce/3
. Elixir () , Ecto.Multi
, [ ] , Enum.reduce/3
multi
.
Repo.transaction/2
, (, , , , arity β β , ? , , .. ). OECD FOS.
:
$ mix run priv/repo/seeds.exs [debug] QUERY OK source="fos" db=0.7ms SELECT count(f0."id") FROM "fos" AS f0 [] [debug] QUERY OK db=0.1ms begin [] [debug] QUERY OK db=1.4ms INSERT INTO "fos" ("id","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4) ["010000", "Natural Sciences", {{2017, 2, 21}, {11, 50, 38, 799789}}, {{2017, 2, 21}, {11, 50, 38, 804086}}] [debug] QUERY OK db=0.3ms ... INSERT INTO "fos" ("id","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4) ["0605BQ", "Humanities, multidisciplinary", {{2017, 2, 21}, {11, 50, 38, 973021}}, {{2017, 2, 21}, {11, 50, 38, 973025}}] [debug] QUERY OK db=5.8ms commit [] [info] OECD FOS load complete
back-end. ? β ( , )!
CRUD-, (View), , , . OECD FOS , β index
, . ( , ), . , ( , ).
ExMachina . , mix test.watch β , .
mix.exs
:
# mix.exs # ... # Specifies your project dependencies. # # Type `mix help deps` for examples and options. defp deps do [{:phoenix, "~> 1.2.1"}, {:phoenix_pubsub, "~> 1.0"}, {:phoenix_ecto, "~> 3.0"}, {:postgrex, ">= 0.0.0"}, {:gettext, "~> 0.11"}, {:cowboy, "~> 1.0"}, # - {:ex_machina, "~> 1.0", only: :test}, {:mix_test_watch, "~> 0.3", only: :dev, runtime: false}] end # ...
:
$ mix deps.get Running dependency resolution... Dependency resolution completed: ex_machina 1.0.2 fs 2.12.0 mix_test_watch 0.3.3 * Getting ex_machina (Hex package) Checking package (https://repo.hex.pm/tarballs/ex_machina-1.0.2.tar) Using locally cached package * Getting mix_test_watch (Hex package) Checking package (https://repo.hex.pm/tarballs/mix_test_watch-0.3.3.tar) Fetched package * Getting fs (Hex package) Checking package (https://repo.hex.pm/tarballs/fs-2.12.0.tar) Fetched package
!
. , test/support
. , :id
:title
, AtvApi.FactoryFosList.fos_list/0
, . :
defmodule AtvApi.FactoryFosList do @fos_list [ %{id: "010000", title: "Natural Sciences"}, %{id: "020000", title: "Engineering and Technology"}, %{id: "030000", title: "Medical and Health Sciences"}, # ... %{id: "0604YG", title: "Theater"}, %{id: "0605BQ", title: "Humanities, multidisciplinary"}, ] def fos_list, do: @fos_list end
AtvApi.Factory
, . :
defmodule AtvApi.Factory do use ExMachina.Ecto, repo: AtvApi.Repo import AtvApi.FactoryFosList, only: [fos_list: 0] def fos_factory do %AtvApi.Fos{ id: "0", title: "Some science-technology name", } end def build_all(factory_name, insert? \\ false) do get_list(factory_name) |> Enum.map(fn(rec) -> case insert? do true -> insert(factory_name, rec) false -> build(factory_name, rec) end end) end def insert_all(factory_name) do build_all(factory_name, true) end defp get_list(:fos) do fos_list() end defp get_list(_) do [] end end
use
, use ExMachina.Ecto, repo: IasipApi.Repo
:
require ExMachina.Ecto ExMachina.Ecto.__using__(repo: AtvApi.Repo)
require
, __using__/1
, , use
, ( ) , .
, - β , . "Quote and unquote" , "" "Domain Specific Languages" Elixir.
, , , , ...
β¦ β , , use ExMachina.Ecto, ...
. ExMachina.Ecto
GitHub . , , .
, , .. ExMachina.Ecto
( require ExMachina.Ecto
) ExMachina.Ecto.__using__/1
, - ( β repo: AtvApi.Repo
; , Elixir []
). :repo
, quote do ... end
, (.. AtvApi.FactoryFos
), quote
unquote
(. ). , params_for/2
, string_params_for/2
. use ExMachina
use ExMachine.EctoStrategy, ...
, β .. ( ).
, build/2
build_list/3
, . ExMachina.Ecto
insert/2
insert_list/3
, , . , ExMachina.Strategy
, ExMachina.EctoStrategy
use ExMachine.Strategy, function_title: :insert
. , insert/2
insert_list/3
β __using__/1
ExMachina.Strategy
:function_name
.
, , .
AtvApi.FactoryFosList.fos_list/0
, , fos_factory/0
.
ExMachine.Ecto
, : build/2
/ build_list/3
insert/2
/ insert_list/3
. ( , ) , . . build/2
build(factory_name, attrs)
, build_list/3
build_list(number_of_factories, factory_name, attrs)
. , build/2
, <factory_name>_factory/0
. Those. build(:fos, %{})
, fos_factory/0
, .
AtvApi.Factory.build/2
Elixir:
$ MIX_ENV=test iex -S mix Erlang/OTP 19 [erts-8.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help) iex> AtvApi.Factory.build(:fos, %{id: "0103SY", title: "Optics"}) %AtvApi.Fos{__meta__: #Ecto.Schema.Metadata<:built, "fos">, id: "0103SY", inserted_at: nil, title: "Optics", updated_at: nil}
build_all/2
, .
: mix.exs
Phoenix Framework mix
, test/support
. MIX_ENV=test
.
, ( build_all/2
), ( insert_all/1
). , build_all/2
, insert_all/1
β .
get_list/1
:
defp get_list(:fos) do fos_list() end defp get_list(_) do [] end
, defp
def
, . , ( , -) .
, . get_list/1
:fos
, , AtvApi.FactoryFosList.fos_list/0
; , . , . .
build_all/2
. get_list/1
, . Enum.map/2
, , . insert?
ExMachina.build/2
, ExMachina.Ecto.insert/2
(, - ). AtvApi.FactoryFos.build_all/2
%Fos{}
.
, mix test
. , mix-test.watch
. mix test.watch
:
$ mix test.watch Running tests... ..... Finished in 0.05 seconds 5 tests, 0 failures Randomized with seed 806690
. , Ctrl+C.
Phoenix Framework test/controllers
. , β exs
, Elixir Script
. , .
.
ExUnit
:
# File: assertion_test.exs # 1) ExUnit. ExUnit.start # 2) ( , test case) # (use) "ExUnit.Case". defmodule AssertionTest do # 3) : "async: true", # . # , # . use ExUnit.Case, async: true # 4) "test" "def" test "the truth" do assert true end end
:
$ elixir assertion_test.exs warning: this check/guard will always yield the same result assertion_test.exs:17 . Finished in 0.03 seconds (0.03s on load, 0.00s on tests) 1 test, 0 failures Randomized with seed 598489
Mix
test/test_helper.exs
. .
. setup_all
setup
( ):
defmodule ExampleTest do use ExUnit.Case setup do {:ok, [hello: :world]} end test "context contains key-value pairs", context do assert context[:hello] == :world end end
, , :
test "context is a map and pattern matching", %{hello: hello} do assert hello == :world end
setup
, β , .
, ExUnit.Case
(callbacks) ExUnit.Callbacks
. setup_all
setup
, on_exit/2
.
. , (. ).
setup_all
. setup
. , setup_all
setup
.
on_exit/2
, , setup
.
setup_all
{:ok, keywords}
, - keywords
setup_all
, setup
.
setup
setup
.
:ok
.
setup_all
- , , setup
.
:
defmodule AssertionTest do use ExUnit.Case, async: true # "setup_all" setup_all do IO.puts " AssertionTest" # No metadata :ok end # "setup" setup do IO.puts " 'setup'" on_exit fn -> IO.puts " " end # [hello: "world"] end # , # setup context do IO.puts " : #{context[:test]}" :ok end # setup :invoke_local_or_imported_function test "always pass" do assert true end test "another one", context do assert context[:hello] == "world" end defp invoke_local_or_imported_function(context) do [from_named_setup: true] end end
ExUnit.Assertions
. , ExUnit.Case
, .
assert/1
refute/1
.
, , , .
Phoenix Framework , - , .. , , .
, , .
fos_controller_test.exs
:
defmodule AtvApi.FosControllerTest do use AtvApi.ConnCase import AtvApi.Factory import AtvApi.FactoryFosList, only: [fos_list: 0] setup %{conn: conn} do insert_all(:fos) fos = fos_list() |> Enum.sort |> Poison.encode! |> Poison.decode! {:ok, conn: put_req_header(conn, "accept", "application/json"), fos: fos} end test "lists all entries on index", %{conn: conn, fos: fos} do conn = get conn, fos_path(conn, :index) assert json_response(conn, 200)["data"] == fos end end
(use) AtvApi.ConnCase
, Phoenix Framework. Phoenix.ConnTest
, , , ; , setup
, conn
Plug.Conn
, .
AtvApi.Factory
AtvApi.FactoryFosList
.
setup
, conn
Plug.Conn.put_req_header/3
. AtvApi.Factory.insert_all/1
. AtvApi.FactoryFosList.fos_list/0
, Enum.sort/1
, JSON Poison
( JSON, β ). conn
, .
, FosController
fos
, setup
. conn
fos
, .
. , HTTP GET- . Phoenix.ConnTest.get/3
, conn
, β URL, . β "/api/fos"
β , Phoenix.Router
, web/router.ex
. - fos_path/2
.
Phoenix.ConnTest.get/3
conn
, .
JSON- :
{"data": [ {"id": "010000", "title": "Natural Sciences"}, {"id": "020000", "title": "Engineering and Technology"}, ... ] }
Phoenix.ConnTest.json_response/2
, β 200 (.. HTTP_OK), JSON- . , , "data"
β .. β fos
.
web/router.ex
resources "/fos", FosController, except: [:new, :edit]
. CRUD-, . , :
$ mix phoenix.routes fos_path GET /api/fos AtvApi.FosController :index fos_path GET /api/fos/:id AtvApi.FosController :show fos_path POST /api/fos AtvApi.FosController :create fos_path PATCH /api/fos/:id AtvApi.FosController :update PUT /api/fos/:id AtvApi.FosController :update fos_path DELETE /api/fos/:id AtvApi.FosController :delete
β β . β . resources "/fos", FosController, except: [:new, :edit]
get "/fos", FosController, :index
mix phoenix.routes
:
$ mix phoenix.routes Compiling 6 files (.ex) fos_path GET /api/fos AtvApi.FosController :index
, β HTTP GET- http://:/api/fos/
:index
AtvApi.FosController
.
. , , . Which one :
$ mix test test/controllers/fos_controller_test.exs Compiling 6 files (.ex) 1) test lists all entries on index (AtvApi.FosControllerTest) test/controllers/fos_controller_test.exs:19 Assertion with == failed code: json_response(conn, 200)["data"] == fos left: [%{"id" => "010000", "title" => "Natural Sciences"}, %{"id" => "020000", "title" => "Engineering and Technology"}, %{"id" => "030000", "title" => "Medical and Health Sciences"}, ... %{"id" => "0101PO", "title" => "Mathematics, interdisciplinary applications"}, %{"id" => "0101PQ", "title" => "Mathematics"}, %{"id" => "0101UR", ...}, %{...}, ...] right: [%{"id" => "010000", "title" => "Natural Sciences"}, %{"id" => "010100", "title" => "Mathematics"}, %{"id" => "0101PN", "title" => "Mathematics, applied"}, ... %{"id" => "010600", "title" => "Biological sciences"}, %{"id" => "0106BD", "title" => "Biodiversity conservation"}, %{"id" => "0106CO", ...}, %{...}, ...] stacktrace: test/controllers/fos_controller_test.exs:21: (test) Finished in 0.1 seconds 1 test, 1 failure Randomized with seed 415134
, , . , :index
AtvApi.FosController
, , ( Enum.sort/1
, setup
). , AtvApi.FosController.index/2
. :
defmodule AtvApi.FosController do use AtvApi.Web, :controller alias AtvApi.Fos import Ecto.Query def index(conn, _params) do fos = Repo.all(from f in Fos, order_by: f.id) render(conn, "index.json", fos: fos) end end
(use) AtvApi.Web
, __using__/1
controller
. β web/web.ex
, .
, index/2
. Ecto.Repo.all/2
, Ecto.Queryable
, , . , , : Repo.all(Fos)
. , , - :
SELECT f0."id", f0."title", f0."inserted_at", f0."updated_at" FROM "fos" AS f0
those. , . , DSL Ecto.Query
, Repo.all(from f in Fos, order_by: f.id)
, :
SELECT f0."id", f0."title", f0."inserted_at", f0."updated_at" FROM "fos" AS f0 ORDER BY f0."id"
, fos
fos
, id
.
Phoenix.Controller.render/3
, (View) conn
( ), ( ) ( ). , , , , ; , Phoenix Framework ( ) AtvApi.FosView
, render/2
, "index.json", β , fos
. , view
β , . web/views/fos_view.ex
β .
:
$ mix test test/controllers/fos_controller_test.exs Compiling 1 file (.ex) . Finished in 0.2 seconds 1 test, 0 failures Randomized with seed 347227
, .
(, , , mix ecto.create
, mix ecto.migrate
mix run priv/repo/seeds.exs
):
$ mix phoenix.server [info] Running AtvApi.Endpoint with Cowboy using http://localhost:4000
http://localhost:4000/api/fos/
:
, JSON- . !
, β . , , front-end' . has_children
.
, , :
$ mix phoenix.gen.model Grnti2 grnti2 title:text has_children:boolean
. , .
(integer) . . , id
Ecto
, integer.
:
defmodule AtvApi.Repo.Migrations.CreateGrnti do use Ecto.Migration def change do create table(:grnti, primary_key: false) do add :id, :integer, null: false, primary_key: true add :title, :text add :has_children, :boolean, default: false, null: false timestamps() end end end
, :has_children
-.
:
defmodule AtvApi.Grnti do use AtvApi.Web, :model schema "grnti" do field :title, :string field :has_children, :boolean, default: false timestamps() end @doc """ Builds a changeset based on the `struct` and `params`. """ def changeset(struct, params \\ %{}) do struct |> cast(params, [:id, :title, :has_children]) |> validate_required([:id, :title, :has_children]) end end
, . , id
, . , changeset/2
.
, :
$ mix test test/models/grnti_test.exs . 1) test changeset with valid attributes (AtvApi.GrntiTest) test/models/grnti_test.exs:9 Expected truthy, got false code: changeset.valid?() stacktrace: test/models/grnti_test.exs:11: (test) Finished in 0.05 seconds 2 tests, 1 failure Randomized with seed 788882
, , , id
, :
@valid_attrs %{title: "some content", has_children: true, id: 100001}
:
$ mix test test/models/grnti_test.exs .. Finished in 0.04 seconds 2 tests, 0 failures Randomized with seed 692361
β :
$ mix ecto.migrate 17:54:26.080 [info] == Running AtvApi.Repo.Migrations.CreateGrnti.change/0 forward 17:54:26.080 [info] create table grnti 17:54:26.097 [info] == Migrated in 0.0s
. priv/repo/seeds.exs
:
### Grnti dictionary ### alias AtvApi.Grnti unless Repo.one!(from g in Grnti, select: count(g.id)) > 0 do multi = File.read!("priv/repo/grnti.txt") |> String.split("\n") |> Enum.reject(fn(row) -> byte_size(row) < 2 end) |> Enum.reduce(%{}, fn(row, acc) -> {id, parent_id, title} = case <<String.trim(row)::binary>> do <<a::binary-size(2), ".", b::binary-size(2), ".", c::binary-size(2), " ", title::binary>> -> { String.to_integer("#{a}#{b}#{c}"), String.to_integer("#{a}#{b}00"), title } <<a::binary-size(2), ".", b::binary-size(2), " ", title::binary>> -> { String.to_integer("#{a}#{b}00"), String.to_integer("#{a}0000"), title } <<a::binary-size(2), " ", title::binary>> -> { String.to_integer("#{a}0000"), -1, title } end parent = case Map.get(acc, parent_id) do nil -> {"", true} {p_title, _} -> {p_title, true} end current = case Map.get(acc, id) do nil -> {title, false} {_, has_children} -> {title, has_children} end acc |> Map.put(id, current) |> Map.put(parent_id, parent) end) |> Enum.reduce(Ecto.Multi.new, fn({id, {title, has_children}}, multi) -> if id > -1 do changeset = Grnti.changeset(%Grnti{}, %{id: id, title: String.trim(title), has_children: has_children}) Ecto.Multi.insert(multi, "#{id}", changeset) else multi end end) Repo.transaction(multi) Logger.info "GRNTI load complete" end ### Grnti dictionary ###
:
File.read!/1
), ,String.split/3
, \n
,Enum.reject/2
,Enum.reduce/3
,Enum.reduce/3
.Enum.reduce/3
. %{}
. row
acc
. ?
, , :
id
title
. , , . , id
"", β ( ), β , β¦ , . .
. , : Erlang , β , , Elixir .
β , β , :
defmodule ImageTyper @png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8), 13::size(8), 10::size(8), 26::size(8), 10::size(8)>> @jpg_signature <<255::size(8), 216::size(8)>> def type(<<@png_signature, rest::binary>>), do: :png def type(<<@jpg_signature, rest::binary>>), do: :jpg def type(_), do :unknown end
ImageTyper.type/1
, , : :png
| :jpg
| :unknown
.
, -, id
, β title
has_children
: %{id => {title, has_children}}
.
, .
{id, parent_id, title}
case
, , String.trim/1
. - .
a
, b
c
, , title
, . case
: , "#{a}#{b}#{c}"
( #{}
β ( )), , , , . c
, β b
. β β -1. id
, parent_id
title
.
. Map.get/3
. - nil
. , ( , , ), , β has_children
true
, parent
.
id
: β , β , has_children
.
.
Enum.reduce/3
, Ecto.Multi
, , OECD FOS. multi
, Repo.transaction/2
.
:
$ mix run priv/repo/seeds.exs [debug] QUERY OK source="fos" db=0.9ms queue=0.1ms SELECT count(f0."id") FROM "fos" AS f0 [] [debug] QUERY OK source="grnti" db=3.6ms SELECT count(g0."id") FROM "grnti" AS g0 [] [debug] QUERY OK db=0.1ms begin [] [debug] QUERY OK db=2.1ms INSERT INTO "grnti" ("has_children","id","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) [false, 443135, " ", {{2017, 2, 22}, {16, 51, 9, 581608}}, {{2017, 2, 22}, {16, 51, 9, 585864}}] [debug] QUERY OK db=0.3ms INSERT INTO "grnti" ("has_children","id","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) [false, 722335, " ", {{2017, 2, 22}, {16, 51, 9, 593526}}, {{2017, 2, 22}, {16, 51, 9, 593531}}] [debug] QUERY OK db=0.1ms INSERT INTO "grnti" ("has_children","id","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) [true, 761300, " ", {{2017, 2, 22}, {16, 51, 9, 593995}}, {{2017, 2, 22}, {16, 51, 9, 594000}}] ... [debug] QUERY OK db=0.4ms INSERT INTO "grnti" ("has_children","id","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) [false, 107161, "", {{2017, 2, 22}, {16, 51, 56, 376371}}, {{2017, 2, 22}, {16, 51, 56, 376375}}] [debug] QUERY OK db=0.3ms INSERT INTO "grnti" ("has_children","id","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) [false, 292931, " ", {{2017, 2, 22}, {16, 51, 56, 376969}}, {{2017, 2, 22}, {16, 51, 56, 376972}}] [debug] QUERY OK db=5.0ms commit [] [info] GRNTI load complete
, fos
( , ) grnti
. .
.
, test/support
. , , :id
, :title
:has_children
, AtvApi.FactoryGrntiList.grnti_list/0
, . :
defmodule AtvApi.FactoryGrntiList do @grnti_list [ %{id: 000000, has_children: true, title: " "}, %{id: 000800, has_children: false, title: " "}, %{id: 000900, has_children: false, title: " "}, %{id: 001100, has_children: false, title: " "}, # ... %{id: 032323, has_children: false, title: " ( XII .)"}, %{id: 032325, has_children: false, title: " ( XII . XVI .)"}, ] def grnti_list, do: @grnti_list end
AtvApi.Factory
:
defmodule AtvApi.Factory do use ExMachina.Ecto, repo: AtvApi.Repo import AtvApi.FactoryFosList, only: [fos_list: 0] import AtvApi.FactoryGrntiList, only: [grnti_list: 0] def fos_factory do %AtvApi.Fos{ id: "0", title: "Some science-technology name", } end def grnti_factory do %AtvApi.Grnti{ id: 0, title: "Some grnti chapter name", has_children: false, } end def build_all(factory_name, insert? \\ false) do get_list(factory_name) |> Enum.map(fn(rec) -> case insert? do true -> insert(factory_name, rec) false -> build(factory_name, rec) end end) end def insert_all(factory_name) do build_all(factory_name, true) end defp get_list(:fos) do fos_list() end defp get_list(:grnti) do grnti_list() end defp get_list(_) do [] end end
, , grnti_factory/0
build/2
, build_list/3
, insert/2
insert_list/3
, get_list/1
. build_all/1
insert_all/1
grnti
, ! , : - (.. , :fos
:grnti
) . , get_list(_)
, . , , .
, , . , , :
defp get_list(factory) do case factory do :fos -> fos_list() :grnti -> grnti_list() _ -> [] end end
# ################################################### # # proceed API request results # # ################################################### # # check for task defp proceed_response(task_uuid, response, state) do # could be rewriten inline, but this is for better code readability task = Map.get(state, task_uuid) proceed_response(task, task_uuid, response, state) end # no task with such uuid - do nothing defp proceed_response(task, _task_uuid, _response, state) when is_nil(task) do state end # Got a normal HTTP response defp proceed_response(task, task_uuid, {:ok, %HTTPoison.Response{body: body, status_code: 200}} = _response, state) do json_decode_result = Poison.decode(body) proceed_response(task, task_uuid, json_decode_result, state) end # API task ID defp proceed_response(task, task_uuid, {:ok, %{"errorId" => 0, "taskId" => api_task_id} = _json_body}, state) do Process.send_after(self(), {:api_get_task_result, task_uuid}, task.result_request_interval) put_in(state, [task_uuid, :api_task_id], api_task_id) end # Set a timer to try again if the task is still processing defp proceed_response(task, task_uuid, {:ok, %{"errorId" => 0, "status" => "processing"} = _json_body}, state) do Process.send_after(self(), {:api_get_task_result, task_uuid}, task.result_retry_interval) state end # Deal with result if the task is done and task type is Image # in case of push: true defp proceed_response( %{type: "ImageToTextTask"} = task, task_uuid, {:ok, %{"errorId" => 0, "status" => "ready", "solution" => %{"text" => text}} = _json_body}, state) do state |> put_in([task_uuid, :result], %{text: text}) |> put_in([task_uuid, :status], :ready) |> push_data(task, task_uuid, {:ready, task_uuid, %{text: text}}) end # Any other - probably an error defp proceed_response(_task, task_uuid, error, state) do parse_error(task_uuid, error, state) end
if\else
.
. , {"errorId" => 0, "taskId" => 12345}
, API , {"errorId" => 0, "status" => "processing"}
, , , {"errorId" => 0, "status" => "ready", "solution" => {"text" => "some_text"}}
. , , , , , JSON {"errorId" => 0, "status" => "ready", "solution" => %{"image" => image_string}}
(, , JSON, ). , API β - proceed_response/3
.
β . , :
$ iex Erlang/OTP 19 [erts-8.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help) iex> defmodule ListSum do ...> def list_sum(list), do: list_sum(list, 0) ...> def list_sum([head | tail], acc), do: list_sum(tail, acc + head) ...> def list_sum([], acc), do: acc ...> end {:module, ListSum, <<70, 79, 82, 49, 0, 0, 5, 180, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 223, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:list_sum, 2}} iex> ListSum.list_sum([1, 5, 10, 20]) 36
. , .. , , , , , β , . mix test.watch
, .
AtvApi.GrntiController
"/api/grnti/<id>"
<id>
, , , <id>
-1.
get "/grnti/:id", GrntiController, :show
:
defmodule AtvApi.Router do use AtvApi.Web, :router pipeline :api do plug :accepts, ["json"] end scope "/api", AtvApi do pipe_through :api get "/fos", FosController, :index get "/grnti/:id", GrntiController, :show end end
:
$ mix phoenix.routes Compiling 6 files (.ex) fos_path GET /api/fos AtvApi.FosController :index grnti_path GET /api/grnti/:id AtvApi.GrntiController :show
Fine! .
-, :
# ... def get_descendants(:grnti, -1) do grnti_list() |> Enum.filter(fn(%{id: id}) -> rem(id, 10000) == 0 end) end # ...
, β ? , ? ! iex
:
$ MIX_ENV=test iex -S mix Erlang/OTP 19 [erts-8.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help) iex> AtvApi.Factory.get_descendants(:grnti, -1) [%{has_children: true, id: 0, title: " "}, %{has_children: true, id: 20000, title: ""}, %{has_children: true, id: 30000, title: ". "}]
.
test/controllers/grnti_controller_test.exs
:
defmodule AtvApi.GrntiControllerTest do use AtvApi.ConnCase import AtvApi.Factory setup %{conn: conn} do insert_all(:grnti) {:ok, conn: put_req_header(conn, "accept", "application/json")} end test "the root level descendants", %{conn: conn} do id = -1 grnti_subtree = get_descendants(:grnti, id) conn = get conn, grnti_path(conn, :show, id) assert json_response(conn, 200)["data"] == grnti_subtree |> Poison.encode! |> Poison.decode! end end
mix test.watch
, , ** (UndefinedFunctionError) function AtvApi.GrntiController.init/1 is undefined (module AtvApi.GrntiController is not available)
. , . :
defmodule AtvApi.GrntiController do use AtvApi.Web, :controller alias AtvApi.Grnti def show(conn, %{"id" => id}) do conn |> put_resp_content_type("text/plain") |> send_resp(200, "request_ok") end end
show/2
, conn
, "id"
. content-type 200 . , , , . .
Elixir, Ecto Phoenix Framework "thin model, fat controller" β , .
. Grnti, :
defmodule AtvApi.Grnti do # ... def descendants(parent_id) when parent_id == -1 do from g in AtvApi.Grnti, where: fragment("mod(?, ?)", g.id, 10000) == 0, order_by: g.id end end
when
. Erlang Elixir guards
: , , ( , guards case
, ). guards
, . id
-1. , , , .
β DSL Ecto.Query
. β where
fragment/1
Ecto.Query.API
. , Ecto.Query
. , β SQL-, ( ?
) , , . id
10000 β .
show/2
:
defmodule AtvApi.GrntiController do # ... def show(conn, %{"id" => parent_id}) do grnti = parent_id |> Grnti.descendants |> Repo.all render(conn, "index.json", grnti: grnti) end end
** (FunctionClauseError) no function clause matching in AtvApi.Grnti.descendants/1
. , , , AtvApi.Grnti.descendants/1
"-1" -1. β "id"
URL GET-, . AtvApi.Grnti.descendants/1
:
defmodule AtvApi.Grnti do # ... def descendants(parent_id) when is_binary(parent_id) do parent_id |> String.to_integer |> descendants end def descendants(parent_id) when parent_id == -1 do from g in AtvApi.Grnti, where: fragment("mod(?, ?)", g.id, 10000) == 0, order_by: g.id end end
β ( is_binary/1
, β ), descendants/1
, .
, : ** (UndefinedFunctionError) function AtvApi.GrntiView.render/2 is undefined (module AtvApi.GrntiView is not available)
. , , , (view). :
defmodule AtvApi.GrntiView do
use AtvApi.Web, :view
def render("index.json", %{grnti: grnti}) do
%{data: render_many(grnti, AtvApi.GrntiView, "grnti.json")}
end
def render("grnti.json", %{grnti: grnti}) do
%{id: grnti.id,
title: grnti.title,
has_children: grnti.has_children}
end
end
, β , . , Phoenix.Controller.render/3
, , , Phoenix.View.render/2
, , ( ) () . . , , def render("index.json", %{grnti: grnti})
. :data
, , render_many/3
. , , , β (. ). Phoenix.View.render/2
, , , render("index.json", %{grnti: grnti})
, β .
, , , β !
, !
. , , - . AtvApi.Factory
get_descendants/2
:
defmodule AtvApi.Factory do # ... def get_descendants(:grnti, -1) do grnti_list() |> Enum.filter(fn(%{id: id}) -> rem(id, 10000) == 0 end) end def get_descendants(:grnti, parent_id) when rem(parent_id, 10000) == 0 do grnti_list() |> Enum.filter(fn(%{id: id}) -> rem(id, 100) == 0 and id > parent_id and id < parent_id + 10000 end) end defp get_list(:fos) do # .. end
guard, 10000, (.. xx0000). , id
xxxx00 id
id
.
, , .
. , , , , , . , DRY , , β . , setup
:
defmodule AtvApi.GrntiControllerTest do use AtvApi.ConnCase import AtvApi.Factory setup %{conn: conn, id: id} do insert_all(:grnti) conn = put_req_header(conn, "accept", "application/json") descendants = :grnti |> get_descendants(id) |> Poison.encode! |> Poison.decode! conn = get conn, grnti_path(conn, :show, id) {:ok, conn: conn, descendants: descendants} end @tag id: -1 test "shows chosen root level subtree", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end end
setup
, , , . . @tag id: -1
, id: -1
, setup
. - .
, . :
# ... @tag id: 000000 test "shows chosen second level subtree - id: 000000", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end @tag id: 020000 test "shows chosen second level subtree - id: 020000", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end @tag id: 030000 test "shows chosen second level subtree - id: 030000", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end #...
, ** (FunctionClauseError) no function clause matching in AtvApi.Grnti.descendants/1
. β , -1.
, , AtvApi.Grnti/descendants/1
:
# ... def descendants(parent_id) when parent_id == -1 do from g in AtvApi.Grnti, where: fragment("mod(?, ?)", g.id, 10000) == 0, order_by: g.id end def descendants(parent_id) when rem(parent_id, 10000) == 0 do from g in AtvApi.Grnti, where: g.id > ^parent_id, where: g.id < ^(parent_id + 10000), where: fragment("mod(?, ?)", g.id, 100) == 0, order_by: g.id end # ...
, Ecto
, SQL-:
SELECT g0."id", g0."title", g0."has_children", g0."inserted_at", g0."updated_at" FROM "grnti" AS g0 WHERE (g0."id" > $1) AND (g0."id" < $2) AND (mod(g0."id", 100) = 0) ORDER BY g0."id"
$1
β , , $2
β .
, , .
.
:
defmodule AtvApi.Factory do # ... def get_descendants(:grnti, parent_id) when rem(parent_id, 10000) == 0 do grnti_list() |> Enum.filter(fn(%{id: id}) -> rem(id, 100) == 0 and id > parent_id and id < parent_id + 10000 end) end def get_descendants(:grnti, parent_id) when rem(parent_id, 100) == 0 do grnti_list() |> Enum.filter(fn(%{id: id}) -> id > parent_id and id < parent_id + 100 end) end defp get_list(:fos) do # .. end
Tests:
# ... @tag id: 000900 test "shows chosen second level subtree - id: 000900", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end @tag id: 021500 test "shows chosen second level subtree - id: 021500", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end @tag id: 032300 test "shows chosen second level subtree - id: 032300", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end #...
3 .
:
# ... def descendants(parent_id) when rem(parent_id, 10000) == 0 do from g in AtvApi.Grnti, where: g.id > ^parent_id, where: g.id < ^(parent_id + 10000), where: fragment("mod(?, ?)", g.id, 100) == 0, order_by: g.id end def descendants(parent_id) when rem(parent_id, 100) == 0 do from g in AtvApi.Grnti, where: g.id > ^parent_id, where: g.id < ^(parent_id + 100), order_by: g.id end # ...
!
, . , , , id
, , .
, . , id
β setup
, AtvApi.Factory.descendants/2
, . , ?
ExUnit.Case
describe/2
. setup
. setup
, describe do ... end
, . describe
setup
:
defmodule AtvApi.GrntiControllerTest do use AtvApi.ConnCase import AtvApi.Factory setup %{conn: conn, id: id} do insert_all(:grnti) conn = conn |> put_req_header("accept", "application/json") |> get(grnti_path(conn, :show, id)) {:ok, conn: conn} end describe "Controller must return descendants of" do setup %{id: id} do descendants = :grnti |> get_descendants(id) |> Poison.encode! |> Poison.decode! {:ok, descendants: descendants} end @tag id: -1 test "the root level", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end @tag id: 000000 test "the chapter with id: 000000", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end @tag id: 020000 test "the chapter with id: 020000", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end @tag id: 030000 test "the chapter with id: 030000", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end @tag id: 000900 test "the chapter with id: 000900", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end @tag id: 021500 test "the chapter with id: 021500", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end @tag id: 032300 test "the chapter with id: 032300", %{conn: conn, descendants: descendants} do assert json_response(conn, 200)["data"] == descendants end end end
.
id
. :
describe/2
:
defmodule AtvApi.GrntiControllerTest do use AtvApi.ConnCase # ... describe "Request must be declined with status code 422 and appropriate JSON error message in case of" do @tag id: "somestring" test "id as a non-digit symbol string", %{conn: conn} do assert json_response(conn, 422)["error"] == %{message: "Unprocessable Entity"} end @tag id: -2 test "id is less than -1 and equal -2", %{conn: conn} do assert json_response(conn, 422)["error"] == %{message: "Unprocessable Entity"} end @tag id: -100 test "id is less than -1 and equal -100", %{conn: conn} do assert json_response(conn, 422)["error"] == %{message: "Unprocessable Entity"} end @tag id: 1000000 test "id is greater than 999999 and equal 1000000", %{conn: conn} do assert json_response(conn, 422)["error"] == %{message: "Unprocessable Entity"} end @tag id: 90000000 test "id is greater than 999999 and equal 90000000", %{conn: conn} do assert json_response(conn, 422)["error"] == %{message: "Unprocessable Entity"} end @tag id: 030955 test "id is a third level section code and equal 030955", %{conn: conn} do assert json_response(conn, 422)["error"] == %{message: "Unprocessable Entity"} end @tag id: 020129 test "id is a third level section code and equal 020129", %{conn: conn} do assert json_response(conn, 422)["error"] == %{message: "Unprocessable Entity"} end end end
. id
. String.to_integer/1
, . Integer.parse/2
. β , β 10, .. , . {integer, reminder_of_binary}
:error
.
:
defmodule AtvApi.GrntiController do use AtvApi.Web, :controller alias AtvApi.Grnti def show(conn, %{"id" => parent_id}) do case Integer.parse(parent_id) do :error -> show(conn, :error) {int, _} -> show(conn, int) end end def show(conn, parent_id) when is_integer(parent_id) and parent_id > -2 and parent_id < 1000000 and ( rem(parent_id, 100) == 0 or parent_id == -1 ) do grnti = parent_id |> Grnti.descendants() |> Repo.all() render(conn, "index.json", grnti: grnti) end def show(conn, _parent_id) do conn |> put_resp_content_type("application/json") |> send_resp(422, ~S({"error":{"message":"Unprocessable Entity"}})) end end
, ,
back-end. , ( , GrntiController - DRY ). , , - , .
mix phoenix.server
:
, front-end , , . , , .
back-end, , GitHub .
, . Have a nice day!
Source: https://habr.com/ru/post/322376/
All Articles