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 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のすべおの利点を継承しおいたす。



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


, . 良い䞀日を



Source: https://habr.com/ru/post/J322376/


All Articles