
颿°åããã°ã©ãã³ã°èšèªElixirã人æ°ãéããŠãããåäžããŒãžã¢ããªã±ãŒã·ã§ã³ãäœæããããã®ææ°ã®ãã¬ãŒã ã¯ãŒã¯ã®1ã€ã§ããAngular 2ãæè¿ãªãªãŒã¹ãããŸããã ããããAngular 2ããŒã¹ã®ããã³ããšã³ãã¯ã©ã€ã¢ã³ãã¢ããªã±ãŒã·ã§ã³ã«ããŒã¿ãæäŸããElixirããã³Phoenix Frameworkã®å®å
šãªããã¯ãšã³ããæåããäœæããããã€ãã®èšäºã§ããããçè§£ããŸãããã
Hello, worldã¯ç§ãã¡ã®ãªãã·ã§ã³ã§ã¯ãªãã®ã§ãå¿
èŠã«å¿ããŠå®éã®ãããžã§ã¯ãã«ããªãããã£ãããšãé©çšã§ããŸãïŒæç€ºããããã¹ãŠã®ã³ãŒãã¯MITã©ã€ã»ã³ã¹ã®äžã§ã¬ã€ã¢ãŠããããŸãã
èšäºã®ããªã¥ãŒã 倧ãã ã§ããïŒ åæ§ã«èšå€§ãªæ°ã®ã³ã¡ã³ããããã°ããã®ã§ããã ç§ã¯ãããªããã³ã¡ã³ãããããããŠã¡ã€ã³èšäºããããå€ããåŸãããšã«äœåºŠãæ°ã¥ããŸããã
æåã®èšäºã«ã¯ããã€ãã®ç޹ä»çãªèšèããããããã¯ãšã³ãã§åäœããŸãã è¡ããïŒ
ã¯ããã«
æ°ã¶æåãç§ã¯éåžžã«çãæéã§ãããã¿ã€ãã®Webã¢ããªã±ãŒã·ã§ã³ãå®è£
ããããã«äžè«ãæ¥è
ãšããŠç³ãåºãããŸããã èŠä»¶ã®ãã¡ãæ©èœã®ã¿ãçµäºæ¥ãããã³ãªãŒãã³ãœãŒã¹ããŒã«ã®ã¿ã䜿çšããå¿
èŠæ§ããããŸããã ããã°ãããããšæã£ãã®ã¯ãããããElixir / Phoenix FrameworkãšAngular 2ãã³ãã«ãå®è·µãã倧ããªçç±ã ããšãåŸè
ã¯å°ãåã«ãªãªãŒã¹ãããŸããã ãã®çµæããããžã§ã¯ãã¯æééãã«å®äºããé¡§å®¢ã¯æºè¶³ããæ°ããã¿ã¹ã¯ã®å®è£
ã§çµéšãè£å
ãããŸããã
ãããã®ã¿ã¹ã¯ã®1ã€ã¯ãè€æ°ã®å€ãéžæããæ©èœãåããGRNTIããã³OECD FOSãã£ã¬ã¯ããªã衚瀺ããå¿
èŠæ§ã§ããã éåžžã®æºåãæŽã£ãããªãŒã®ãããªåèæžã衚瀺ããããã®æ¢è£œã®ãœãªã¥ãŒã·ã§ã³ããªãã£ããããèªè»¢è»ãäœãçŽãå¿
èŠããããŸããã ããã«ã Elixir / Phoenix FrameworkãšAngular 2ã®äž¡æ¹ãåæã«æ¢çŽ¢ããããã®ããã®äžé£ã®ããã¬ãŒãã³ã°ãèšäºã®ããŒããæäŸããŸããã
ãããã£ãŠããã®ãµã€ã¯ã«ã®çµããã«ãElixirããã³Phoenixãã¬ãŒã ã¯ãŒã¯ã®äœæ¥ããã¯ãšã³ãããããAPIã䜿çšããŠSRSTIããã³OECD FOSãã£ã¬ã¯ããªã®ã³ã³ãã³ããAngular 2ã®ç¬ç«ããããã³ããšã³ãã«éä¿¡ããŸããã»ã¯ã·ã§ã³\ãµãã»ã¯ã·ã§ã³ãä¿åæã«éžæãŠã£ã³ããŠã®å€ã«ç§»åããéžæãããã®ãéããŠåŸ©å
ããŸãã å€èгã¯Twitter BootstrapãæäŸããŸãã ããã³ããšã³ãã§ã®ãã£ã¬ã¯ããªã®å®è£
ãåå¥ã®ã¢ãžã¥ãŒã«ãšããŠé
眮ããŸããããã¯ãå°æ¥ã©ã®ãããžã§ã¯ãã§ã䜿çšã§ããŸãã
ããã€ãã®å®è£
ããŒã
SRSTIãã£ã¬ã¯ããªã¯3ã¬ãã«ïŒæå€§ïŒæ§é ã§ãåãšã³ããªã¯10é²åé¡ã®ã³ãŒããæã¡ã00ãã99ãŸã§ã®æ°åã®3ã€ã®ã°ã«ãŒãã§æ§æãããããããšååã§åºåãããŠããŸãã ãªãã¡ã¬ã³ã¹ããã¯ã«ã¯çŸåšãçŽ8,000ã®ã»ã¯ã·ã§ã³ãšãµãã»ã¯ã·ã§ã³ã®ã¬ã³ãŒããå«ãŸããŠããããã©ããããã¹ã圢åŒã®ããªã¥ãŒã ã¯400 kb以äžã§ãã ããã¥ã¢ã«ã®å
容ã¯grnti.ruã«ãããŸã ïŒãã®ãªãœãŒã¹ãšã¯é¢ä¿ãããŸããïŒã
OECD FOSã¯ããããã§åºåãããéå±€ã³ãŒããæã€3ã¬ãã«ã®æ§é ãæã£ãŠããŸããã以åã®ããŒãžã§ã³ãšã¯ç°ãªãããã®å Žåãã³ãŒãã®æåŸã®ã°ã«ãŒãã¯2ã€ã®ã©ãã³æåã®çµã¿åããã§ãã ãã£ã¬ã¯ããªå
ã®ãšã³ããªã¯å€§å¹
ã«å°ãªãã300ãå°ãäžåããåèšããªã¥ãŒã ã¯çŽ8kbã§ãã æ®å¿µãªããããã®ã¬ã€ãã®é¢é£ããŒãžã§ã³ããªã³ã©ã€ã³ã§èŠã€ããããšãã§ããªãã£ããããä»ã®ãã£ãã«ã§èŠã€ãã£ããã®ã䜿çšããŸãã
SRSTIãã£ã¬ã¯ããªãŒã®ããªã¥ãŒã ã®ãããããã䜿çšããå ŽåãçŸæç¹ã§å¿
èŠãªã»ã¯ã·ã§ã³ã®ã¿ãããã¯ãšã³ãããèŠæ±ããŸãããOECD FOSå
šäœãæå®ã§ããæ§é ã¯ã¯ã©ã€ã¢ã³ãäžã§ãã§ã«åŠçã§ããŸãã
ããã«æç¢ºã«ãããïŒã¿ã¹ã¯ã¯ããéåããŠããŠãããã¯ããåºãæ©èœã®äžéšã«ãããŸããã§ããã åœç¶ã®ããšãªãããã¿ã¹ã¯ããã£ã¬ã¯ããªã®åºåã®ã¿ãç®çãšããå ŽåïŒæ
å ±æäŸãªã©ïŒãããã¯ãšã³ããSPAãå¿
èŠãããŸããã
ãŸããç§ã¯ç¬¬äžäººè
ã®ãµããããããšã¯äžåãããŸãããããããªããäœããã®éšåã®ãã广çãªå®è£
ãææ¡ãããªããç§åŠã«æè¬ããŸããç§ã¯åŠç¿ã倧奜ãã§ãã
泚æïŒã³ãŒãããŠãŒãã£ãªãã£ã®åºåãããã³ããã€ãã®é«åºŠãªèª¬æã¯ãå°ãªããšãäœããã®æ¹æ³ã§èªã¿åããšäœ¿çšã®å©äŸ¿æ§ãé«ããããã«ãã¹ãã€ã©ãŒã®äžã«é ãããŠããŸãã
æè¡ã¹ã¿ãã¯ã®éžæ
çŸåšãåçŽæ¹åã®é»åå¢å ã¯ããé«äŸ¡ã«ãªã£ãŠãããããã©ãŒãã³ã¹ã¯åšæ³¢æ°ãäžããããšã§ã¯ãªããæ°Žå¹³ã«ãæ°ããã³ã³ãã¥ãŒãã£ã³ã°ã³ã¢ã远å ããããšã§éæãããŠããŸãã ãã®ãããç«¶äºåã®ããã³ã³ãã¥ãŒãã£ã³ã°ïŒäžŠåå®è¡ïŒã«ç¹åããèšèªã®é¢å¿ãé«ãŸã£ãŠããŸãã åæã«ãå
±æããŒã¿ãžã®å
±æã¢ã¯ã»ã¹ã¯æ·±å»ãªé çã®çš®ã«ãªãã 颿°åããã°ã©ãã³ã°èšèªã§ã¯å€§å¹
ã«åæžã§ããŸãã
Elixirã¯ãErlangã§èšè¿°ããã Beamä»®æ³ãã·ã³ã§å®è¡ããããããªãè¥ããã€ãã³ãŒãã³ã³ãã€ã«ããã颿°åèšèªã§ãã ãã®èšèªã¯ã Erlangã®ãã¹ãŠã®å©ç¹ãç¶æ¿ããŠããŸãã
- æ©èœæ§
- å
ç«
- ããã¹ãŠãããã»ã¹ã
- ããã»ã¹ã¯äºãã«åé¢ãããŠããã
- ããã»ã¹ã®äœæãšç Žå£ã®éåžžã«äœãã³ã¹ãã
- åããã»ã¹ã«ã¯äžæã®èå¥åïŒPIDïŒãããããªãã·ã§ã³ã§äžæã®ååãå²ãåœãŠãããšãã§ããŸãã
- ããã»ã¹éã§ãªãœãŒã¹ãå
±æããããšã¯ãããŸããã
- ååãšèå¥åãããã£ãŠããå Žåã¯ãä»»æã®ããã»ã¹ããä»»æã®ããã»ã¹ã«ã¡ãã»ãŒãžãéä¿¡ã§ããŸãã
- ã¡ãã»ãŒãžã³ã°ã¯ââããã»ã¹ééä¿¡ã®å¯äžã®æ¹æ³ã§ããã
- ãã¿ãŒã³ãããã³ã°
- ãå¿
èŠãªããšãè¡ãããŸãã¯åã«æ»ã¬ããšããã€ããªãã®ãŒ
- 忣ã·ã¹ãã ã®äœæã®å®¹æãã
åæã«ãRubyã«ãã䌌ãã·ã³ãã«ãªæ§æããããã³ã«ã¡ã«ããºã ã«ããããªã¢ãŒãã£ãºã ãéåžžã«è±å¯ãªã¡ã¿ããã°ã©ãã³ã°æ©èœãMarkdownããŒã¯ã¢ããã䜿çšããŠããã¥ã¡ã³ããç°¡åã«äœæããæ©èœã ããã³ã¢ãžã¥ãŒã«ã³ãŒãã§ã®å®éã®ãã¹ãïŒ!!!ïŒ ã Erlangã®ãã¹ãŠã®æ©èœãšãErlangåãã«äœæãããã©ã€ãã©ãªã¯ãããã©ãŒãã³ã¹ãæãªãããšãªãElixirã³ãŒãããçŽæ¥åŒã³åºãããšãã§ããããšãéèŠã§ãã
ãã®èšèªã¯ãã¡ãã»ãŒãžã³ã°ãåããæ¬æ Œçãªããã»ã¹ã¢ãžã¥ãŒã«ãæžãã®ã«æåéãæ°åãããããããã«ãããã»ãã·ã³ã°ãšã¡ãã»ãŒãžã³ã°ã®äœ¿çšãåŒãèµ·ãããŸãïŒãã®ãµã€ã¯ã«ãžã®åå¿ãè¯å®çãªå Žåã以äžã®åºçç©ã§ããã«æ»ãããšãæã¿ãŸãïŒã
èšèªã®äœè
ã§ããJoséValimãã³ãã¥ããã£ã®ç掻ã«ç©æ¥µçã«åå ããããšã倧ããªæå³ããããŸãã 圌ã¯åãã§è©³çްã«è³ªåã«çããå¿
èŠã«å¿ããŠãäžè¶³ããŠããæ©èœãèšèª/ã©ã€ãã©ãªã«å°å
¥ããŸãïŒããšãã°ãåŸã§èª¬æããEctoã§-å人çãªååããªçµéšããããŸãïŒã
Phoenix Frameworkã¯Elixirã§æã人æ°ã®ããWebãã¬ãŒã ã¯ãŒã¯ã§ãããMVCãã¿ãŒã³ãå®è£
ãã Webã¢ããªã±ãŒã·ã§ã³ã®éçºã倧å¹
ã«ç°¡çŽ åããŸãã ããã«ãPhoenixã«ã¯ãã£ãã«ããããŸããããã¯ãWebãœã±ãããä»ããã¢ããªã±ãŒã·ã§ã³ãšã®ãªã¢ã«ã¿ã€ã éä¿¡ã®å¯èœæ§ã§ãããããã¯æ¬åœã«çŽ æŽãããæ©èœã§ãã ãã©ãŠã¶ã§äœ¿çšããããã®JavaScriptã³ã³ããŒãã³ããšãAndroidåãã®Javaãªã©ã®ä»ã®èšèªã®å®è£
ããããŸãã
Angular 2ã¯ãäž»ã«GoogleããµããŒãããåäžããŒãžWebã¢ããªã±ãŒã·ã§ã³ã®ã¯ã©ã€ã¢ã³ãåŽãéçºããããã®ãã¬ãŒã ã¯ãŒã¯ã§ãã ããŒãžã§ã³2ã¯ãAngularJSã®éçºããã³éçšäžã«åŸãããçµéšã«åºã¥ããŠå®å
šã«æžãçŽãããŸããã ãã®ãªãªãŒã¹ã¯2016幎9æã«ãªãªãŒã¹ãããŸããã
Elixirããã³Phoenixãã¬ãŒã ã¯ãŒã¯ã®ããã¯ãšã³ã
ãšãªã¯ã·ãŒã«ã«ã€ããŠã®ããã€ãã®èšèElixirã«åºäŒã£ãããšããªãå Žåã¯ãèšèªã®åŠç¿ããå§ããããšã匷ããå§ãããŸãã 䜿çšããã¢ãããŒããšæ©èœã«ã€ããŠè©³çްã«èª¬æããäºå®ã§ãããå°ããªã·ãªãŒãºã®èšäºã®æ çµã¿å
ã§ãã¹ãŠãç¶²çŸ
ããããšã¯äžå¯èœã§ãïŒããã¯ãç§èªèº«ãåžžã«æ°ãããã®ãèŠã€ããŠãããšããäºå®ãæ°ããŠããããã§ã¯ãããŸããïŒã åŠç¿ã®ããã«ããããžã§ã¯ãWebãµã€ãã®åºæ¬çãªç޹ä»ãšãã€ã³ã¿ãŒãããäžã®ä»ã®å€ãã®ãªãœãŒã¹ããããŸãã èšèªã®äœæè
ã倧ããªåã³ã§çãããã©ãŒã©ã ã¯éåžžã«äŸ¿å©ã§ãã ã¡ãªã¿ã«ãRedditã®ãã®ã¹ã¬ããã«ã¯ãããšãªã¯ã·ãŒã«ãæåã«å匷ããããããã«å®¹å§¿ã«çªå
¥ãã䟡å€ãããã ããšãã質åã«å¯ŸããåªããçãããããŸãã èŠçŽãããšããã®ãããã¯ã®äœè
ã¯ãRubyãšãon Elixirãã§æžããããã¹ãã±ãŒã¹ã®ããã©ãŒãã³ã¹ã®ããããªéãã«å€±æãããšèšããŸãã Rubyã®ã³ãŒãã¯ã4.221ç§ãElixirã§ã¯5.923ç§ã§å®è¡ãããŸããã èšèªã®æ©èœã䜿çšããŠïŒRubyãš1察1ã§ç§»æ€ããã ãã§ãªãïŒã³ãŒããæžãçŽããåŸã3åïŒ!!!ïŒéãåäœãå§ããŸããã
æ£ããããšãèšã£ãŠãç§ã¯å·éã«èšããŸããç§ã¯èªåã§ãããããããšã¯ãã£ãã«ãªããéåžžã¯ããã«æŠéã«çªå
¥ããŸãã
ããŒã«ãããã®ã€ã³ã¹ããŒã«ãšãããžã§ã¯ãã®éå§
ErlangãElixirãããã³ããŒââãã®ããŒãžã§ã³ïŒåŸã§ããã³ããšã³ããæäœããããã«å¿
èŠã«ãªããŸãïŒãå¶åŸ¡ããã«ã¯ã asdfããã±ãŒãžãããŒãžã£ãŒã䜿çšããŸãã FedoraãšUbuntuãasdfãErlangãElixirã®äŸåé¢ä¿ã®ã€ã³ã¹ããŒã«ã«ã€ããŠè©³ãã説æããåªããèŠç¹ãããã®ã§ãç¹°ãè¿ããŸããã 圌ã¯è±èªã§ãããååãªã³ããŒã¢ã³ãããŒã¹ãããããŸãã å·çæç¹ã®ææ°ããŒãžã§ã³ïŒErlang-19.2ãElixir-1.4.1ã
ãŸããPostgreSQLã®ææ°ããŒãžã§ã³ïŒçŸæç¹ã§ã¯9.6ã䜿çšïŒãå¿
èŠã§ããããã¯ããã£ã¹ããªãã¥ãŒã·ã§ã³\ OSã®æšæºããã±ãŒãžãããŒãžã£ãŒã䜿çšããŠã€ã³ã¹ããŒã«ã§ããŸãã
ErlangãšElixirãã€ã³ã¹ããŒã«ãããã Phoenix Frameworkãã€ã³ã¹ããŒã«ããå¿
èŠããããŸãã
ãããžã§ã¯ãã®äœæãã³ã³ãã€ã«ããã¹ããããã³äŸåé¢ä¿ã®ç®¡çã®ããã®Elixirã«ã¯ãç¹å¥ãªèªååãŠãŒãã£ãªãã£-Mix ïŒããã¥ã¡ã³ãã®ãã®éšåã«ã泚æãæãå¿
èŠããããŸãïŒããããŸãã mixãŠãŒãã£ãªãã£-makeã«äŒŒmakeããããã䟿å©ã§ãã
ãŸãããã®å©ããåããŠã次ã®ã³ãã³ãã§ïŒæ¬¡ã®ïŒ Hexããã±ãŒãžãããŒãžã£ãŒãã€ã³ã¹ããŒã«ããŸãã
$ mix local.hex
次ã«ãPhoenix Frameworkã¢ãŒã«ã€ãïŒ
$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez
ããã¥ã¡ã³ãã«ã¯node.jsã®å¿
èŠæ§ãèšèŒãããŠããŸããããã®å ŽåãPhoenixã¯APIã®ã¿ãæäŸãããããAngular 2ã«é²ããšãã«ããŒããå¿
èŠã«ãªããŸãã
ãã®èšäºã®å·çæç¹ã§ã¯ãPhoenix FrameworkããŒãžã§ã³1.2.1ãé¢é£ããŠããŸããã
Hexã«ã€ããŠäžèšè©±ã䟡å€ã¯ãããŸãã Elixir / Erlangã®ã©ã€ãã©ãªãå
¬éããåäžã®ãªããžããªhttps://hex.pmããããŸãã ããã©ã«ãã§ã¯ãElixirã®ãã¹ãŠã®ãããžã§ã¯ãäŸåé¢ä¿ãããã§æ€çŽ¢ãããŸãã
å¿
èŠãªãœãããŠã§ã¢ãã€ã³ã¹ããŒã«ããããå¿
èŠãªãã£ã¬ã¯ããªã«ç§»åããæ¬¡ãå®è¡ããŠæ°ããPhoenixãããžã§ã¯ããäœæããŸãã
mix phoenix.new atv_api --no-brunch --no-html $ 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 .
requireElixir - (.. , ). â , (.. ) . , , , . require .
alias , Repo AtvApi.Repo . â import â ( â Ecto.Query, ). (, , ) , only: [function_title: arity] , , : import Ecto.Query, only: [from: 2] (arity â ). â - , . â () , , , , . .
seeds.exs , . Repo.one!/2 , SQL SELECT COUNT(f.id) FROM fos AS f , , .
"" "" ", [] , ( tuple ) {:ok, result} {:error, description} , , , , .
äŸïŒ
iex> File.read("file.txt") {:ok, "file contents"} iex> File.read("no_such_file.txt") {:error, :enoent} iex> File.read!("file.txt") "file contents" iex> File.read!("no_such_file.txt") ** (File.Error) could not read file no_such_file.txt: no such file or directory
, ( pipe operator ). . , (2) (3) , (4) (5), :
iex(1)> some_map = %{one: 1} %{one: 1} iex(2)> Enum.count(some_map) 1 iex(3)> some_map |> Enum.count() 1 iex(4)> Enum.count(Map.put(some_map, :two, 2)) 2 iex(5)> some_map |> Map.put(:two, 2) |> Enum.count() 2
, :
- (
File.read!/1 ), , - ( List ) -
String.split/3 , \n , - , ,
Enum.reject/2 , , false , Enum.sort/1 ,Enum.dedup/1 ,- , ,
Enum.reduce/3 , .
Enum.reduce(enumerable, acc, fun) , Enum , , Enumerable (), , , . , . Enum.reduce/3 .
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 . , .
() ElixirElixir 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/ :
, 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 ###
:
- (
File.read!/1 ), , - ( List ) -
String.split/3 , \n , - , ,
Enum.reject/2 , Enum.reduce/3 ,Enum.reduce/3 .
Enum.reduce/3 . %{} . row acc . ?
, , :
- " 00 "
- " 00.21 - "
- " 02.01.39 "
id title . , , . , id "", â ( ), â , ⊠, . .
binaryElixir <<>> . .
. , : 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.exdefmodule 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 :
, front-end , , . , , .
back-end, , GitHub .
, . è¯ãäžæ¥ãïŒ