関数型プログラミング言語Elixirが人気を集めており、単一ページアプリケーションを作成するための最新のフレームワークの1つであるAngular 2が最近リリースされました。 それらをAngular 2ベースのフロントエンドクライアントアプリケーションにデータを提供するElixirおよびPhoenix Frameworkの完全なバックエンドを最初から作成するいくつかの記事でそれらを理解しましょう。
Hello, world
は私たちのオプションではないので、必要に応じて実際のプロジェクトにあなたがやったことを適用できます:提示されたすべてのコードはMITライセンスの下でレイアウトされます。
記事のボリューム 大きい でかい! 同様に膨大な数のコメントがあればいいのですが。 私は、あなたがコメントから、そしてメイン記事からより多くを得ることに何度も気づきました。
最初の記事にはいくつかの紹介的な言葉があり、バックエンドで動作します。 行こう!
はじめに
数ヶ月前、私は非常に短い時間でプロトタイプのWebアプリケーションを実装するために下請け業者として申し出られました。 要件のうち、機能のみ、終了日、およびオープンソースツールのみを使用する必要性がありました。 「すばらしい」と思ったのは、「これがElixir / Phoenix FrameworkとAngular 2バンドルを実践する大きな理由だ」と、後者は少し前にリリースされました。 その結果、プロジェクトは時間通りに完了し、顧客は満足し、新しいタスクの実装で経験が補充されました。
これらのタスクの1つは、複数の値を選択する機能を備えたGRNTIおよびOECD FOSディレクトリを表示する必要性でした。 通常の準備が整ったツリーのような参考書を表示するための既製のソリューションがなかったため、自転車を作り直す必要がありました。 さらに、 Elixir / Phoenix FrameworkとAngular 2の両方を同時に探索するための、この一連の「トレーニング」記事のテーマも提供しました。
したがって、このサイクルの終わりに、ElixirおよびPhoenixフレームワークの作業バックエンドがあり、APIを使用してSRSTIおよびOECD FOSディレクトリのコンテンツをAngular 2の独立したフロントエンドに送信します。セクション\サブセクション、保存時に選択ウィンドウの外に移動し、選択したものを開いて復元します。 外観はTwitter Bootstrapを提供します。 フロントエンドでのディレクトリの実装を個別のモジュールとして配置します。これは、将来どのプロジェクトでも使用できます。
いくつかの実装ノート
SRSTIディレクトリは3レベル(最大)構造で、各エントリは10進分類のコードを持ち、00から99までの数字の3つのグループで構成され、ドットと名前で区切られています。 リファレンスブックには現在、約8,000のセクションとサブセクションのレコードが含まれており、フラットテキスト形式のボリュームは400 kb以上です。 マニュアルの内容はgrnti.ruにあります (このリソースとは関係ありません)。
OECD FOSは、ドットで区切られた階層コードを持つ3レベルの構造も持っていますが、以前のバージョンとは異なり、この場合、コードの最後のグループは2つのラテン文字の組み合わせです。 ディレクトリ内のエントリは大幅に少なく、300を少し下回り、合計ボリュームは約8kbです。 残念ながら、このガイドの関連バージョンをオンラインで見つけることができなかったため、他のチャネルで見つかったものを使用します。
SRSTIディレクトリーのボリュームのため、それを使用する場合、現時点で必要なセクションのみをバックエンドから要求しますが、OECD FOS全体を指定でき、構造はクライアント上ですでに処理できます。
すぐに明確にしたい:タスクはやや退化していて、それはより広い機能の一部にすぎませんでした。 当然のことながら、タスクがディレクトリの出力のみを目的とする場合(情報提供など)、バックエンドもSPAも必要ありません。
また、私は第一人者のふりをすることは一切ありません。もしあなたが何らかの部分のより効果的な実装を提案するなら、科学に感謝します。私は学習が大好きです。
注意:コード、ユーティリティの出力、およびいくつかの高度な説明は、少なくとも何らかの方法で読み取りと使用の利便性を高めるために、スポイラーの下に隠されています。
技術スタックの選択
現在、垂直方向の電力増加はより高価になっており、パフォーマンスは周波数を上げることではなく、水平に、新しいコンピューティングコアを追加することで達成されています。 このため、競争力のあるコンピューティング(並列実行)に特化した言語の関心が高まっています。 同時に、共有データへの共有アクセスは深刻な頭痛の種になり、 関数型プログラミング言語では大幅に削減できます。
Elixirは、Erlangで記述され、 Beam仮想マシンで実行される、かなり若いバイトコードコンパイルされた関数型言語です。 この言語は、 Erlangのすべての利点を継承しています。
- 機能性
- 免疫
- 「すべてがプロセス」
- プロセスは互いに分離されている、
- プロセスの作成と破壊の非常に低いコスト、
- 各プロセスには一意の識別子(PID)があり、オプションで一意の名前を割り当てることができます。
- プロセス間でリソースを共有することはありません。
- 名前と識別子がわかっている場合は、任意のプロセスから任意のプロセスにメッセージを送信できます。
- メッセージングはプロセス間通信の唯一の方法であり、
- パターンマッチング
- 「必要なことを行う、または単に死ぬ」というイデオロギー
- 分散システムの作成の容易さ。
同時に、Rubyにやや似たシンプルな構文、プロトコルメカニズムによるポリモーフィズム、非常に豊富なメタプログラミング機能、Markdownマークアップを使用してドキュメントを簡単に作成する機能、 およびモジュールコードでの実際のテスト(!!!) 。 Erlangのすべての機能と、Erlang向けに作成されたライブラリは、パフォーマンスを損なうことなくElixirコードから直接呼び出すことができることが重要です。
この言語は、メッセージングを備えた本格的なプロセスモジュールを書くのに文字通り数分かかるため、マルチプロセッシングとメッセージングの使用を引き起こします(このサイクルへの反応が肯定的な場合、以下の出版物でこれに戻ることを望みます)。
言語の作者であるJoséValimがコミュニティの生活に積極的に参加することも大きな意味があります。 彼は喜んで詳細に質問に答え、必要に応じて、不足している機能を言語/ライブラリに導入します(たとえば、後で説明するEctoで-個人的な前向きな経験があります)。
Phoenix FrameworkはElixirで最も人気のあるWebフレームワークであり、MVCパターンを実装し、 Webアプリケーションの開発を大幅に簡素化します。 さらに、Phoenixにはチャネルがあります。これは、Webソケットを介したアプリケーションとのリアルタイム通信の可能性であり、これは本当に素晴らしい機能です。 ブラウザで使用するためのJavaScriptコンポーネントと、Android向けのJavaなどの他の言語の実装があります。
Angular 2は、主にGoogleがサポートする単一ページWebアプリケーションのクライアント側を開発するためのフレームワークです。 バージョン2は、AngularJSの開発および運用中に得られた経験に基づいて完全に書き直されました。 このリリースは2016年9月にリリースされました。
ElixirおよびPhoenixフレームワークのバックエンド
Elixirに出会ったことがない場合は、言語の学習から始めることを強くお勧めします。 使用するアプローチと機能について詳細に説明する予定ですが、小さなシリーズの記事の枠組み内ですべてを網羅することは不可能です(これは、私自身が常に新しいものを見つけているという事実を数えているわけではありません)。 学習のために、プロジェクトWebサイトの基本的な紹介と、インターネット上の他の多くのリソースがあります。 言語の作成者が大きな喜びで答えるフォーラムは非常に便利です。 ちなみに、Redditのこのスレッドには、「エリクシールを最初に勉強するか、すぐに容姿に突入する価値があるか 」という質問に対する優れた答えがあります。 要約すると、このトピックの作者は、Rubyと「on Elixir」で書かれたテストケースのパフォーマンスのわずかな違いに失望したと言えます。 Rubyのコードは、4.221秒、Elixirでは5.923秒で実行されました。 言語の機能を使用して(Rubyと1対1で移植するだけでなく)コードを書き直した後、3倍(!!!)速く動作し始めました。
正しいことを言って、私は冷静に言います。私は自分でそれをすることはめったになく、通常はすぐに戦闘に突入します。
ツールキットのインストールとプロジェクトの開始
Erlang、Elixir、およびノードのバージョン(後でフロントエンドを操作するために必要になります)を制御するには、 asdfパッケージマネージャーを使用します。 FedoraとUbuntu、asdf、Erlang、Elixirの依存関係のインストールについて詳しく説明した優れた要点があるので、繰り返しません。 彼は英語ですが、十分なコピーアンドペーストがあります。 執筆時点の最新バージョン:Erlang-19.2、Elixir-1.4.1。
また、PostgreSQLの最新バージョン(現時点では9.6を使用)が必要です。これは、ディストリビューション\ OSの標準パッケージマネージャーを使用してインストールできます。
ErlangとElixirをインストールしたら、 Phoenix Frameworkをインストールする必要があります。
プロジェクトの作成、コンパイル、テスト、および依存関係の管理のためのElixirには、特別な自動化ユーティリティ-Mix (ドキュメントのこの部分にも注意を払う必要があります)があります。 mix
ユーティリティ-makeに似make
おり、より便利です。
まず、その助けを借りて、次のコマンドで(次の) Hexパッケージマネージャーをインストールします。
$ mix local.hex
次に、Phoenix Frameworkアーカイブ:
$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez
ドキュメントにはnode.jsの必要性が記載されていますが、この場合、PhoenixはAPIのみを提供するため、Angular 2に進むときにノードが必要になります。
この記事の執筆時点では、Phoenix Frameworkバージョン1.2.1が関連していました。
Hexについて一言話す価値はあります。 Elixir / Erlangのライブラリを公開する単一のリポジトリhttps://hex.pmがあります。 デフォルトでは、Elixirのすべてのプロジェクト依存関係がそこで検索されます。
必要なソフトウェアをインストールしたら、必要なディレクトリに移動し、次を実行して新しいPhoenixプロジェクトを作成します。
$ 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 $
クライアント部分は別のプロジェクトになるため、htmlテンプレートとブランチサポートなしで新しいプロジェクトを作成しています。
構成ファイルconfig/prod.exs
、 config/dev.exs
、およびconfig/test.exs
のデータベース接続設定を、それぞれ本番モード、開発モード、およびテストモードに変更することをお勧めします。
さらに、Elixirバージョン1.4以降を使用していて、現在のバージョンのPhoenixがまだ1.2.1である場合、 mix.exs
ファイルをmix.exs
変更することをお勧めします。 Elixir 1.4はいくつかの新機能をもたらしました。特に、プロジェクトの開始時に開始する必要がある独自のプロセスツリーを持つ依存関係の追加を簡素化しました。 以前にそのような依存関係(およびそれらのほとんど)を依存関係のリスト( deps
)と実行するアプリケーションのリスト( deps
)の両方に追加する必要があった場合、最初はそれだけで十分です: mix
は依存関係がアプリケーションであるかどうかを判別して開始します。 依存関係にリストされていないアプリケーションのみを指定する必要があります。 アプリケーションの説明を返すメソッドを次の形式にしましょう。
# mix.exs ... # Configuration for the OTP application. # # Type `mix help compile.app` for more information. def application do [mod: {AtvApi, []}, extra_applications: [:logger]] end ...
以前のものと比較すると、キー:applications
なくなったリストと新しいリストが追加されたことがわかります:applications
:extra_applications
、ここでは:logger
のみが残り、依存関係にリストされたすべてが除外されました。
これがmix ecto.create
、 mix ecto.create
を使用してデータベースの作成を開始します。 デフォルトの環境はそれぞれdev
atv_api_dev
。この環境用のデータベースが作成されatv_api_dev
とatv_api_dev
。
mix ecto.drop
タスクを実行することで、いつでもデータベースを削除できます。 この場合、 mix ecto.reset
はデータベースを削除し、新しいデータベースを作成し、移行を開始し、初期データ入力のためにseeds.exs
の内容を実行します(後者については以下で詳しく説明します)。
mix
前に、目的の値で初期化されmix
変数MIX_ENV=prod
、 MIX_ENV=dev
(デフォルト)またはMIX_ENV=test
することにより、適切な環境で必要なタスクを実行できます。
OECD FOSリファレンス
OECD FOSのリファレンスは簡単なので、それから始めましょう。
PhoenixはEctoライブラリを使用してデータを処理します。 Ectoは、ビュー(モデル)を介してデータベーステーブルを操作し、データベースクエリを作成するためのDSLです。 Ectoは、Rails ActiveRecordsとは異なり、非常にシンプルです(最低限必要)が、同時に強力なツールです。
コードジェネレーター
Phoenix Frameworkには、移行の完全なセット、モデル、 CRUDを実装するコントローラー、jsonを生成するビューモジュール、および基本的なテストを作成できるさまざまなタイプのコードジェネレーターがあります。 両方のディレクトリの場合、完全なCRUDは必要ありませんが、OECD FOSの場合は、生成されたコードから始めて余分な部分を削除できます。
OECD FOSディレクトリテーブルには、 id
とtitle
2つのフィールドがあり、両方ともtext
タイプtext
。 (なぜtext
ですか? 違いがない場合 、なぜスプレーするのですか?)
ジェネレーターとドライブを使用します。
$ 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
ここで、 phoenix.gen.json
は、混合(混合タスク)ユーティリティのタスク、 Fos
は単数形のモデルの名前、 fos
はテーブルの名前です。慣例により、複数形の小文字とフィールドの説明を含むモデルの名前が必要です。 この場合、モデルにtitle
とタイプtext
(これはPostgreSQLデータタイプ)という名前のフィールドを見たいと思いtext
。 id
フィールドについては後ほど説明します。 mix help
コマンドを実行すると、ミックスタスクのリストを取得できます 。フェニックスタスクの詳細については、 ドキュメントを参照してください 。
コマンドを完了すると、 resources "/fos", FosController, except: [:new, :edit]
web/router.ex
resources "/fos", FosController, except: [:new, :edit]
をresources "/fos", FosController, except: [:new, :edit]
行resources "/fos", FosController, except: [:new, :edit]
web/router.ex
に追加するように求められweb/router.ex
。 とりあえずやってみましょう(これは後で変更します):
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
また、移行プロセスを開始するよう求められますが、急がないでください。 デフォルトでは、Ectoはinteger
型の自動インクリメントid
フィールドを主キーとしてモデルと移行(データベースにテーブルを作成するスクリプト)を生成しますが、パーティションコードをキーとして使用するため、このタイプのフィールドは必要ありません。 モデルのこの動作を変更します。
移行ファイルから始めましょう。 モデルジェネレーターは、 priv/repo/migrations
ディレクトリに移行を作成します。 _create_fos.exs
で終わるファイルを開き、次の形式に_create_fos.exs
します。
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
Elixirコードはモジュールと関数に編成されています 。 各モジュールはdefmodule
マクロによって定義され、関数の説明はdef
またはdefp
によって定義されます。 use
に注意を払うまで、後でこれに戻ります。 この移行モジュールはAtvApi.Repo.Migrations.CreateFos
と呼ばれ、規約に基づいて便宜上作成されています。 この言語では、そのような名前だけを強制することはありません。また、言語は、 AtvApi.Repo.Migrations
やAtvApi.Repo
などの「親」モジュールをAtvApi.Repo.Migrations
チェーン全体を持つことを強制しません。
create/2
テーブル作成マクロにprimary_key: false
オプションを追加しました。 これにより、標準のid
フィールドの作成をキャンセルし、同じ名前のフィールドを手動で追加しますが、タイプはtext
、これが主キーになります。
web/models
ディレクトリにあるモデルの説明を修正しましょう:
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
主キーの説明に@primary_key
定数を追加したことに注意してください。 また、許可された変更のリストにフィールド名:id
アトムを追加しました ( cast/3
関数の説明を参照、最後のパラメーターがallowed
)-そうでない場合、変更セットに設定されたコードのフィールドを追加できません。 同じアトムがvalidate_required/2
バリデータ関数のリストに追加されます 。これは、名前が示すように、チェンジセット内の対応するフィールドの存在をチェックし、存在しない場合はセットをエラーとしてマークします。
timestamp
タイプのupdated_at
およびupdated_at
フィールドをモデルの回線に追加するマクロ呼び出しtimestamps/1
に注目する価値があります。 最初のフィールドは作成時に現在の時刻で初期化され、2番目のフィールドはレコードがEcto
関数によって変更されるたびに初期化されます。
ここでは、モデルが何であるかについていくつかの言葉を言う必要もあります。
Elixirには「 struct
」という概念があります。 構造体は連想配列の拡張です(つまり、キーと値のペアのストアであり、通常は%{ key => value, ...}
と呼ばれ%{ key => value, ...}
キーがアトムの場合、 %{ key: value, ...}
); 構造には追加のキー__struct__
、その値には名前が含まれ、 コンパイル時にコードで指定されたフィールドによってのみ制限されます。 , , . defstruct
, :
iex> defmodule User do ...> defstruct title: "John", age: 27 ...> end
, defstruct
, , , . %User{}
.
, — , Map
. Enumerable
, Enum
.
, — , - ( ) , do ... end
scheme
. , AtvApi.Fos
, %Fos{}
- :id
( ) :title
( ).
, :
$ 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}
, , , , .
例:
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
), , - ( List ) -
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
. .
例:
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
FosController
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
. つまり 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
.
. , , . どっち? :
$ 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
つまり , . , 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
), , - ( List ) -
String.split/3
,\n
, - , ,
Enum.reject/2
, -
Enum.reduce/3
, -
Enum.reduce/3
.
Enum.reduce/3
. %{}
. row
acc
. ?
, , :
- " 00 "
- " 00.21 - "
- " 02.01.39 "
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
. .
GrntiController
.
, 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
! .
-, :
# ... 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")}
終わり
def render("grnti.json", %{grnti: grnti}) do
%{id: grnti.id,
title: grnti.title,
has_children: grnti.has_children}
終わり
終わり
, — , . , 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
テスト:
# ... @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
. :
- ,
- -1 999999,
- , .. 100, .
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 .
, . 良い一日を!