Hello、world!なしのElixirとAngular 2、またはツリーリファレンスを使用して作業します、パート1

CPAP







関数型プログラミング言語Elixirが人気を集めており、単一ページアプリケーションを作成するための最新のフレームワークの1つであるAngular 2が最近リリースされました。 それらをAngular 2ベースのフロントエンドクライアントアプリケーションにデータを提供するElixirおよびPhoenix Frameworkの完全なバックエンドを最初から作成するいくつかの記事でそれらを理解しましょう。







Hello, world



は私たちのオプションではないので、必要に応じて実際のプロジェクトにあなたがやったことを適用できます:提示されたすべてのコードはMITライセンスの下でレイアウトされます。







記事のボリューム 大きい でかい! 同様に膨大な数のコメントがあればいいのですが。 私は、あなたがコメントから、そしてメイン記事からより多くを得ることに何度も気づきました。







最初の記事にはいくつかの紹介的な言葉があり、バックエンドで動作します。 行こう!







はじめに



数ヶ月前、私は非常に短い時間でプロトタイプのWebアプリケーションを実装するために下請け業者として申し出られました。 要件のうち、機能のみ、終了日、およびオープンソースツールのみを使用する必要性がありました。 「すばらしい」と思ったのは、「これがElixir / Phoenix FrameworkとAngular 2バンドルを実践する大きな理由だ」と、後者は少し前にリリースされました。 その結果、プロジェクトは時間通りに完了し、顧客は満足し、新しいタスクの実装で経験が補充されました。







これらのタスクの1つは、複数の値を選択する機能を備えたGRNTIおよびOECD FOSディレクトリを表示する必要性でした。 通常の準備が整ったツリーのような参考書を表示するための既製のソリューションがなかったため、自転車を作り直す必要がありました。 さらに、 Elixir / Phoenix FrameworkAngular 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のすべての利点を継承しています。









同時に、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
 $ 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
 $ 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



。 とりあえずやってみましょう(これは後で変更します):







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



します。







priv / repo / migrations / 20170215194144_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



ディレクトリにあるモデルの説明を修正しましょう:







ウェブ/モデル/ fos.ex
 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
 $ 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
 $ 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



. . :







priv/repo/seeds.exs
 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 .







require

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
      
      





, :









Enum.reduce(enumerable, acc, fun)



, Enum , , Enumerable (), , , . , . Enum.reduce/3



.







Ecto.Multi



.







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



.







, ' ' `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
 $ 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
 # 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
 $ 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



, . :







test/support/factory_fos_list.ex
 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



, . :







test/support/factory.ex
 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



, ( ) , .







ExMachina.Ecto

, - — , . "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



:







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
 $ 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



. , .







() Elixir

Elixir ExUnit



. , — , .







.







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



. .







ExUnit.Case



. async



.







. 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



:







test/controllers/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
 $ 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
 $ 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
 $ 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



. :







web/controllers/fos_controller.ex
 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
 $ 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/



:







Fos dictionary browser screenshot







, JSON- . !









, — . , , front-end' . has_children



.









, , :







 $ mix phoenix.gen.model Grnti2 grnti2 title:text has_children:boolean
      
      





. , .







(integer) . . , id



Ecto



, integer.







:







priv/repo/migrations/20170211194248_create_grnti.exs
 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



-.







:







web/models/grnti.ex
 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
 $ 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
 $ mix test test/models/grnti_test.exs .. Finished in 0.04 seconds 2 tests, 0 failures Randomized with seed 692361
      
      





— :







mix ecto.migrate
 $ 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



:







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 ###
      
      





:









Enum.reduce/3



. %{}



. row



acc



. ?







, , :







  1. " 00 "
  2. " 00.21 - "
  3. " 02.01.39 "


id



title



. , , . , id



"", — ( ), — , … , . .







binary

Elixir <<>>



. .







. , : 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



.







Map.put/3



\ .







.







Enum.reduce/3



, Ecto.Multi



, , OECD FOS. multi



, Repo.transaction/2



.







:







mix run priv/repo/seeds.exs
 $ 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



, . :







test/support/factory_grnti_list.ex
 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



:







test/support/factory.ex
 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(_)



, . , , .







get_list/1

, , . , , :







  defp get_list(factory) do case factory do :fos -> fos_list() :grnti -> grnti_list() _ -> [] end end
      
      





, - , . , , API:







  # ################################################### # # 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



:







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 get "/fos", FosController, :index get "/grnti/:id", GrntiController, :show end end
      
      





:







mix phoenix.routes
 $ mix phoenix.routes Compiling 6 files (.ex) fos_path GET /api/fos AtvApi.FosController :index grnti_path GET /api/grnti/:id AtvApi.GrntiController :show
      
      





! .







-, :







test/support/factory.ex
  # ... 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
 $ 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



:







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)



. , . :







web/controllers/grnti_controller.ex
 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, :







web/models/grnti.ex
 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



:







web/controllers/grnti_controller.ex
 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



:







web/models/grnti.ex
 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). :







web/views/grnti_view.ex

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



:







test/support/factory.ex
 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



:







test/controllers/grnti_controller_test.exs
 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



. - .







, . :







test/controllers/grnti_controller_test.exs
  # ... @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



:







web/models/grnti.ex
  # ... 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



— .







, , .







.

:







test/support/factory.ex
 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
      
      





テスト:







test/controllers/grnti_controller_test.exs
  # ... @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 .







実装:







web/models/grnti.ex
  # ... 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



:







test/controllers/grnti_controller_test.exs
 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



:







test/controllers/grnti_controller_test.exs
 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



.







:







web/controllers/grnti_controller.ex
 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
      
      





, ,







show/2



, , .. URL. , show/2



. . , , guard



, . , , Content-Type: application/json



, (sigil) .







back-end. , ( , GrntiController - DRY ). , , - , .







mix phoenix.server



:







GRNTI dictionary browser screenshot







, front-end , , . , , .







back-end, , GitHub .







, . 良い一日を!








All Articles