Medley Developer Blog

株式会社メドレーのエンジニア・デザイナーによるブログです

iOSDC Japan 2018にメドレーが協賛しました

こんにちは、開発本部の高井です。

メドレーは、去年に引き続き8/30〜9/2に早稲田大学で開催された iOSDC Japan 2018(以下iOSDC)に協賛しました。 みなさんご存知かと思いますが、iOSDCは国内のiOSイベントの中ではtry! Swiftと並ぶ最大級のイベントです。 f:id:medley_inc:20180913175434p:plain

(オンライン診療アプリ「CLINICS」初期開発時からSwiftで実装しています)

CLINICS (クリニクス)

CLINICS (クリニクス)

  • Medley, Inc.
  • メディカル
  • 無料

メドレーは今回、シルバースポンサーとして協賛させていただきました。ブース出展はしていないのですが、スポンサー枠で私が参加してきました。

オープニングのスポンサー紹介は去年に引き続き今年も三石琴乃さんのナレーションでした!豪華です。

イベントの様子

ランチのお弁当はもちろん、朝はドーナツ、夕方のLTが始まるとビールが提供されるなど至れり尽くせりでした。セッションは最大4つが同時に進み、それに加えてアンカンファレンスと特定のテーマを設けたディスカッション企画などもあったのですが、進行も非常に円滑で非常に参加者の満足度の高いカンファレンスではないかと思います。 f:id:medley_inc:20180913175617p:plain

セッション

今年はSwiftのコンパイラのソースからSwiftの機能を解説するようなセッションがいくつかありました。去年はそのようなセッションはなかったのではないかと思います。Swiftがオープンソース化されて3年弱ぐらいになり、日本でもSwiftのコミュニティがより成熟しているように感じました。 また、機械学習などのキャッチーなトピックの話題が少し減って、業務上得た知見や設計についてなどの実用的なテーマが多かった印象でした。 特にViewの設計や実装についてのセッションが面白かったです。やはり、iOSアプリ開発ではView周りの実装が悩みの多い領域ですよね。

ここからは特に気になったセッションをいくつかご紹介します。

MicroViewControllerで無限にスケールするiOS開発

www.icloud.com

UIパーツごとにViewControllerを持たせて、一つの画面の異なる機能を複数のエンジニアで開発しやすいように実装しているというお話でした。一般的な画面単位でViewControllerを持つ実装だと、開発の人数を増やしてもコンフリクトやオーバーヘッドが発生しやすくなり、効率的に開発することが難しくなってしまいます。そのため、開発チームの規模をスケールさせることができないということからそのような方法を取り入れたそうです。

MicroViewControllerを採用したことによって、一つのアプリに20人のエンジニアを充てることができるようになったということでしたが、他にそんなところあるのでしょうか、、、という気がしないでもなかったです。ただ、数人でやっていてもコンフリクトはよく起こりますし(project.pbxproj、、、!)、機能を追加したり、削除したりということを素早く試行錯誤するのには良さそうだなと思いました。

また、ビルドの効率化のためにサンドボックスアプリを用意したり、ViewControllerのテンプレートを作って実装の効率化を図るなど開発効率向上の参考になる取り組みも多かったです。

バイス・OSバージョンの依存が少なく、メンテナンスしやすいビューを作る

ビューの実装について起こりがちな問題とその対処法について紹介したセッションでした。特にレイアウト崩れを防ぐために意識すべきポイントについて、具体的な失敗例を示しながら紹介されていました。

UIコンポーネントのサンプル実装(https://github.com/folio-sec/Folio-UI-Collection/tree/master/Folio-UI-Collection)も紹介されていましたが、CLINICSでも共通UIをコンポーネント化して使っているので、カスタムViewの実装方法やコンポーネントの粒度などが再確認でき、非常に有益でした。また、UIのユニットテストもあり、CLINICSではあまりUI関連のテストは書けていないので参考にしたいと思います。

宣言的UICollectionView

UICollectionViewに複数の種類がある場合(よくあります!)に、宣言的な実装をすることでコードの見通しをよくする方法を提案されているセッションでした。

自分もあのswitch文をまとめようとして、うまくいかなかった経験があるのでとても興味深かったです。すぐに自分たちのコードにも適用できる実用的なセッションでした。

あとはReactorKitのライブコーディングや差分検出アルゴリズムに関連する話がいくつかあり、ちょうど業務でReactorKitとRxDataSourcesを使っているので参考になりました。

差分アルゴリズムの原理について

UITableViewなどで変更があった箇所だけを更新するのに利用される差分検出のアルゴリズムについて解説したセッションでした。IGListKitやRxDataSourcesなどで使われているアルゴリズムAndroidのDiffUtilで利用されているアルゴリズムの原理について紹介されていました。ライブラリを使っていると、利用方法は分かるけど内部でどう動いてるかはあまり理解していないということも割とあると思いますが、その辺りを理解できると用途に応じて最適なライブラリを選択できたり、より効果的な使い方ができそうだと思いました。

5000行のUITableViewを差分更新する

差分更新のライブラリを使って多数行を更新した際に発生した問題の紹介とその原因を特定して、改善した内容についてのお話でした。採用していたライブラリはDifferでしたが、ライブラリごとの特徴やInstrumentsによるボトルネックの特定などが参考になりました。

まとめ

iOSDCは去年に引き続きの参加だったのですが、今年のセッションもどれも興味深く、勉強になることが多かったです。またセッションの裏で特定の技術についてディスカッションするコーナーができるなど年々充実してきているように感じました。また、LTのときの会場の一体感はとても良いなあと思いました。

f:id:medley_inc:20180913175405p:plain 弊社のアプリ開発でもそういった知見などを活かして開発していける仲間を引き続き募集しています! 興味がある方は、こちらからご連絡ください。

www.wantedly.com

Kotlin Fest 2018にメドレーが"ひよこスポンサー"として協賛しました

こんにちは、開発本部平木です。去る8/25に行われた、日本初(!!)のKotlinの言語カンファレンスであるKotlin Fest 2018に弊社は"ひよこスポンサー"として協賛させていただきました。

公式Twitterで紹介された様子

今回スポンサーチケットで参加させていただいたので、つれづれとレポートを書いてまいります。

メドレーとKotlinの関わり

なぜ、メドレーがKotlin Festに協賛したかというとKotlinを使ってAndroidアプリを作っているからになります。

オンライン診療アプリ CLINICSのAndroid版で使っています。 play.google.com

Androidで正式にKotlinサポートすることがアナウンスされてからできるところをJavaからKotlinに書きかえていっています。

方針としては、ムリに全部のソースをKotlinにするという形ではなく改修などで触ったソースで余力があれば書きかえるというスタンスでやっていますが、それでも現在50%弱のソースがKotlinになってい ます。

Kotlinで書いた場合にJavaよりも可読性や堅牢性が上がるというメリットを実感していたところ、今回のKotlin Festの開催を知り、協賛させていただいたという次第です。

イベントの様子

会場は東京コンファレンスセンター品川でした。自分は初訪問だったのですが、設備も充実しており良い会場だと思いました。

無限に出てくるかのようなコーヒー・飲み物とお菓子が大変ホスピタリティを感じさせます。

スポンサーブースも盛況で、なかでもYahoo! Japanさんのモブプロ実演や、CyberAgentさんのKotlinクイズなどが人気を集めていました。

f:id:medley_inc:20180903111847j:plain

M3さんのブースでいただいたロゴ入りじゃがりこ

f:id:medley_inc:20180903111902j:plain

セッション

セッションは2セッションが同時に行われるという形式でした。

自分が参加したセッションのみですが簡単な感想でご紹介します。

Kotlinで改善するAndroidアプリの品質

speakerdeck.com

JavaからKotlinへの書きかえを考えた場合のアプリの品質を主軸にしてメリットを紹介する…というものでした。

Effective Javaの中で紹介されている項目について、Kotlinではどうなるかという視点での紹介は大変興味深かったです。

KotlinはJavaよりもNull安全を始め、堅牢だというイメージがありましたが、こうしてJavaするべきであるという項目がKotlinでは言語仕様レベルで対応されていることが多いというのを目の当たりにすると、さらに頼もしく思えるというようなセッションでした。

Kotlinアプリのリファクタリングポイント

www.slideshare.net

既に存在するKotlinのコードをどのような指針でリファクタリングしていくかというセッションです。

「こういうときに書き方複数あるけどどうしよう…」という例ばかりだったので、自分達のアプリでもすぐに使えるような実践的なセッションでした。

中でもいかにMutableなプロパティを避けるかというフローチャートは、理路整然としていてこれからKotlinを書いていく上でかなり参考になりました。

Kotlin linter

speakerdeck.com

自分の場合、JavaScriptなどでもわりとLinterは興味がある分野だったのですが、KotlinのLintについて はandroid-lintしか使ったことがなかったので、紹介されているLinterの情報が参考になるセッションでした。

KotlinのLinterもやはりASTを触らないとオリジナルルールの設定ができないのかーなど普段触れていなかった知識が得られて有意義でした。

が、KotlinのASTはPsiViewerというIntliJ プラグインくらいしか対応してなさそうで、自分で設定するとなると若干つらそうだなという印象でした。

AST Exploreあたりで気軽に試せると良いですね。

Kotlinで愛でるMicroservices

speakerdeck.com

サーバサイドKotlinをMicroserviceでガンガン使用するために必要なエッセンスがまとまったセッションでした。

元々使っていたGoとの使いわけや、実際どのようなアーキテクチャで作って、デプロイや監視など運用をどのようにしているのかなどがコンパクトにまとまっていて大変分かりやすかったです。

サーバサイドKotlinは弊社で使う予定は現状まだ無いのですが、このセッションを見た限りミニマムに始めることが可能な感じに思えたのが収穫でした。

まとめ

Kotlinの日本初のカンファレンスでしたが、来場者もかなり多くKotlinエンジニアの裾野が広いなという印象でした(セッションによっては立ち見も出ていました)。

またセッションも初級から上級まで幅広く取り揃えられていたので、飽きることなく楽しめましたし、来年以降も続いていってほしいと思ったカンファレンスでした。

メドレーでは今後も色々なTechカンファレンスをスポンサードして参ります。色々な場所で、お会いできたらと思います!

HTTP Cacheで求人サイトのスピード改善を試してみた話

こんにちは、開発本部の楊です。メドレーの社内勉強会「TechLunch」で、前回は、Reactの基本を紹介しましたが、今回はHTTP Cacheで、医療介護求人サイト「ジョブメドレー」のスピード改善ができないか検討した話について、共有しました。

なぜHTTP Cacheについて話すことにしたのか

ジョブメドレーには、医療機関や保育園、介護施設などさまざまな事業所の求人が掲載されています。現在、14万を超える事業部の求人が掲載されており、かつ事業所側で求人原稿を修正することもできるため、求職者側が閲覧するスピードについては、いつも意識して改善に取り組んでいます。

その試行錯誤の中で、HTTP Cacheを使って改善する方法について、実現性など含めて検証することにしました。

今回は、あるページをモデルケースに試してみました。このページは、PCは平均250ms、モバイルは180msになっています。

f:id:medley_inc:20180824171410p:plain

ユーザが該当のページを見たときに「内容に更新がないときはブラウザ側のキャッシュを利用してスピードを最適化する」「ページが更新されていたら、最新の内容をすぐユーザへ反映する」という要件でスピード改善されることを目指すことにしました。

HTTP Cacheとは?

Google Developersでは以下のように説明されています。

ネットワーク経由で情報を取得するには時間もコストもかかります。レスポンスが大きいと、クライアントとサーバ間のラウンドトリップを何度も繰り返す必要があるため、レスポンスが利用可能となってブラウザで処理できるようになるまで時間がかかります。さらに、ユーザ側ではデータの通信コストが発生します。そのため、前に取得したリソースをキャッシュに保存して再使用できることは、パフォーマンスを最適化する上で非常に重要です。

この機能はほぼ全てのブラウザに対応しており、HTTPヘッダーでCacheControlETAGLast-Modifiedなどを利用して、リソース更新するタイミングなどを細かくコントロール可能です。

RailsでのHTTP Cache

expire_inでキャッシュ

1時間キャッシュしたい場合は、コントローラにコードを一行書けば大丈夫です。

expires_in(1.hour)

これで問題なく1時間キャッシュされるのですが、「ページが編集されたら即時にユーザへ反映する」という要件を満たしてはいません。

ETAGを利用

では「ページが更新されたら最新の内容をすぐユーザへ反映して、更新がない時はブラウザ側のキャッシュを利用してスピードを最適化する」を行いたい場合はどうすればよいでしょうか。 ここではETAGを利用しようと思います。

ETAGはページの内容によって、ユニークな文字列を作成して、ブラウザ側でページの更新あるかどうかを判定するものです。

ETAGはどう作成されているか

RailsのデフォルトではHTTP Cacheを有効になっていて、ETAGを自動的に作成しています。

header['ETag'] = Digest::MD5.hexdigest(body)

上のコードのようなイメージで、レスポンスBodyからETAGを作成します。

つまり、サーバから返されるHTMLソースの内容が毎回同じであれば、ブラウザは前回キャッシュした内容を読み込むようになります。

サーバレスポンスタイムの短縮

今回の対象ページの機能としてサーバ側では主に二つの部分で時間がかかります。

  1. DB、Redisなどで画面表示が必要なデータ取得、ロジック処理
  2. クライアントに返すHTMLのレンダリング

対象ページは「HTMLのレンダリング」する時間が長かったので、それを改善できれば、サーバレスポンスタイムを一気に短くできます。

画面に表示する必要なデータに変換がなければ、HTMLのレンダリングをせずにレスポンスを返す機能がないかをさらに調べました。

fresh_whenを使う

名前の通り、いつ画面更新するかをコントロールするメソッドです。

データベース中の該当データが更新されたら、新しいETAGを作成するコードは以下のようになります。

fresh_when(etag: [@job_offer, @job_offer.facility])

modelからETAGをどう作成するか

modelのcache_keyからETAGを作ります。

該当ページで使用するjob_offerモデルのcache_keyは"job_offer/5-20071224150000"のような感じになります。

modelのidとupdated_atの組み合わせでユニークなcache_keyを作成しています。

Railsの該当コード

def cache_key(*timestamp_names)
  case
  when new_record?
    "#{model_name.cache_key}/new"
  when timestamp_names.any?
    timestamp = max_updated_column_timestamp(timestamp_names)
    timestamp = timestamp.utc.to_s(cache_timestamp_format)
    "#{model_name.cache_key}/#{id}-#{timestamp}"
  when timestamp = max_updated_column_timestamp
    timestamp = timestamp.utc.to_s(cache_timestamp_format)
    "#{model_name.cache_key}/#{id}-#{timestamp}"
  else
    "#{model_name.cache_key}/#{id}"
  end
end

自前の作成したクラスからETAGをどう作成するか

modelだけではなく、自前で作成したクラスでデータ管理をしてるところもあります。 そちらでの実現方法も試してみました。

そこでETAGを作るコードがRailsでどうなっているか追ってみました。

Railsの該当コード

def retrieve_cache_key(key)
  case
  when key.respond_to?(:cache_key) then key.cache_key
  when key.is_a?(Array)            then key.map { |element| retrieve_cache_key(element) }.to_param
  when key.respond_to?(:to_a)      then retrieve_cache_key(key.to_a)
  else                                  key.to_param
  end.to_s
end

このように、自作クラスにcache_keyのメソッドを定義したら、その結果からETAGが作成されるようになっています。このメソッドを使って、ユニークな値を返せば大丈夫です。

ここまで調査したことを踏まえて、該当ページをHTTP Cacheで実装をしてみました。以下が該当の疑似コードになります。

class JobOfferBrowsingHistory
  def cache_key
    # return job offer ids
  end
end

fresh_whenに渡す

@user_history = JobOfferBrowsingHistory.new
fresh_when(etag: [@job_offer, @job_offer.facility, @user_history])

テスト

開発環境で該当ページを2回ロードしました。 1回目は1200ms、キャッシュが効いた2回目は130msになりました。すごく早いです! もちろん、ページ内のデータが更新されたら、最新の内容がページに反映されるようにもなっています。

これで「ページのデータが更新されたら、最新の内容をすぐユーザへ反映する」「更新がなければHTTP Cacheを返す」という状態が実現しました。

課題

  • 実装ミスで、更新が必要なのに、更新されない問題が起こりえる
    • 例えば、画面に新しいmodelを追加したが、fresh_whenに渡すのを忘れたとか
  • 問題なく更新されることを保証する仕組みの実装

まとめ

HTTP Cacheを利用して、ジョブメドレーのスピード改善を検討してみた調査過程を紹介しました。

expire_inETAGの作成方法など色々調べて、最終的にfresh_whenで実現できましたが、運用していく上での課題については、引き続き検討して行きたいと思います。

Rails Developers Meetup 2018 Day 3 Extremeで弊社の宍戸が発表させていただきました

こんにちは、開発本部の平木です。

去る7/14(土)にRails Developers Meetup 2018 Day 3 Extremeというイベントが開催されまして、弊社の宍戸電子カルテとセキュリティガイドラインAWSと私というタイトルで発表させていただきましたので、レポートさせていただきます。 f:id:medley_inc:20180719114338j:plain

発表のきっかけ

昨年に引き続きRubyKaigi 2018で弊社CTOの平山がLTスポンサーとして発表させていただいたり、ブースに出展をしていたことがきっかけになり、Rails Developers Meetupの主催者である平野さん(@yoshi_hirano)から平山へ出演のお声がけをしていただきました。イベントからイベントへ関係がつながるのはとても嬉しいですね。

発表テーマについて

せっかく、お声がけしていただいたので、発表テーマも弊社の特色が出るものが良いであろうと決まったのが今回のテーマです。

4月に発表した電子カルテシステムCLINICSカルテですが、何度かこちらのブログでもエントリが上がっていますとおり、電子カルテというシステムを構築する上で、セキュリティはかなり重要なウェイトを占める要素になっています。

このセキュリティを担保するための指針として3省4ガイドラインという総務省経産省厚労省の各省庁から出ている4種類のガイドラインが出ています。

これを実際にAWSをベースとしたシステムを構築する際の一例としてCLINICSカルテでの構築例を発表しようということになりました。

発表スライド

当日の発表スライドはこちらになります。

speakerdeck.com

会場ではスライドの色見がすっごく色褪せしていたりしましたが、発表した中でのAWSのサービスは会場のみなさんの中でも、あまり馴染みがないものが多いようでスライドの写真など撮られている方もいらっしゃって、知見共有という意味で参考になった模様です。

やはりクライアント認証に関しては普段サービス開発ではそこまでは使われないので、興味を持っていただいたようで良かったです!

まとめ

弊社のような医療業界ではなくても、フルマネージドサービスでセキュリティを意識するような場面があればご参考になるかもしれないテーマをエンジニアの宍戸から発表させていただきました。

社内で貯まった知見で機会があれば、ぜひ公開していきたいと思います。

メドレーについて気になった方は、こちらからどうぞ。

www.wantedly.com

Rubyを使ってHPKIカードのデータを読み取る

こんにちは、開発本部の宮内です。今回、HPKIカードについて調査を行いましたので、それについて書きます。

tl;dr

JAHIS HPKI 対応 IC カードガイドライン Ver.3.0を参考にして、HPKIテストカードから実際に公開鍵証明書を取得しました。

今後もHPKIについて調査を続行していきたいと思います。

HPKIとは?

HPKIとは厚生労働省が認める医療福祉関係資格(医師・薬剤師・看護師など26種類の保健医療福祉分野の国家資格と、院長・管理薬剤師など5種類の管理者資格)を認証することができるPKIです。

配布されたHPKIカードには、ルートCA、中間CA、証明書が格納されています。

このカードは、電子署名などに使用することができ、今後普及していけば、医療文書(処方箋や病院への紹介状など)を印刷、押印、送付するなどの非効率な業務をすることなく、すべてデジタル化することができるようになります。

また、電子認証用の証明書も含まれているため、認証・認可処理にも使用することができます。

今回、HPKIテストカードを用いて調査を行いました。 f:id:medley_inc:20180712151907j:plain

調査環境

  • macOS v10.13.5
  • ACR39-NTTCom
  • Ruby v2.5.1
  • smartcard v0.5.6
  • HPKIテスト用カード

PC/SC

HPKIカードのようなICカードとやり取りを行うには、PC/SCというAPI仕様を使う必要があります。

PC/SCはもともとWindows環境のみで利用可能でしたが、pcsc-liteというOSS実装があり、現在では様々なUNIX like OSでも利用できます。

macOSの場合、/System/Library/Frameworks/PCSC.framework/PCSCにライブラリが用意されており、特に準備する必要なく利用可能です。(2018年07月現在)

ただし、ICカードリーダーのドライバーをインストールする必要があります。

今回利用したACR39-NTTComダウンロードページmacOS v10.13に対応したドライバーが配布されていなかったため、ICカードリーダーのチップメーカーであるACS社のダウンロードページからドライバーを入手しました。

smartcard

検証する際に使用したgemはsmartcardです。 普通のrubygemと同じくgem installして利用します。

gem install smartcard

ICカードリーダーをPCに接続し、

ruby -rsmartcard -e 'pp Smartcard::PCSC::Context.new.readers'

を実行し、ICカードリーダー名が表示されれば接続成功です。

アプリケーション識別子の取得

実際にHPKIテストカードから情報を取得していきます。

ガイドラインの「附属書A(参考)PKI カードアプリケーション利用のシーケンス」にある「A.2.2 JIS X 6320-15 に従った PKI カードアプリケーションの検索と利用」を実装していきます。

f:id:medley_inc:20180712152004p:plain 引用 ガイドライン

prog01.rb

# prog01.rb
require "smartcard"

def puts_response(response)
  puts "status = %04X" % response[:status]
  puts "data = %s" % response[:data].map { |i| "%02X" % i }.join(" ")
end

context = Smartcard::PCSC::Context.new
begin
  card = context.card context.readers.first

  # SELECTコマンドで`E8 28 BD 08 0F`をパーシャル指定したDFを指定
  apdu = [0x00,
          0xA4,
          0x04,
          0x00,
          0x05,
          0xE8, 0x28, 0xBD, 0x08, 0x0F,
          0x00]
  response = card.transmit apdu.pack("C*")
  response = Smartcard::Iso::IsoCardMixin.deserialize_response response.unpack("C*")
  puts_response response

  while response[:status] == 0x9000
    # SELECTコマンドで次のDFを探す
    apdu = [0x00,
            0xA4,
            0x04,
            0x02,
            0x05,
            0xE8, 0x28, 0xBD, 0x08, 0x0F,
            0x00]
    response = card.transmit apdu.pack("C*")
    response = Smartcard::Iso::IsoCardMixin.deserialize_response response.unpack("C*")
    puts_response response
  end
ensure
  context.release
end

上記のプログラムを実行すると、次のような出力が得られます。

status = 9000
data = 6F 12 84 10 E8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 01
status = 9000
data = 6F 12 84 10 E8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 02
status = 6A82
data =

SELECTコマンドを発行するとBER-TLVで符号化されたFCI(ファイル制御情報)が取得できます。

1つ目のデータから見ていきます。

1バイト目は6Fなので、このデータはファイル制御パラメタ及びファイル管理データの集合を表します。

f:id:medley_inc:20180712152144p:plain 引用 JIS X 6320-4 表8-ファイル制御情報用の産業感共通利用テンプレート

2バイト目は12なので、後続するデータの長さが18バイトあることを表します。

3バイト目は84なので、データ要素がDF名であることを表します。

f:id:medley_inc:20180712152208p:plain 引用 JIS X 6320-4 表10-ファイル制御パラメタデータオブジェクト

4バイト目は10なので、後続するデータの長さが16バイトあることを表します。 5バイト目以降は、DF名(= アプリケーション識別子)です。

2つ目のデータもデータ構造は同じなため省略します。

これでHPKIテストカードには、

  • E8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 01
  • E8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 02

という2つのアプリケーション識別子が含まれていることが分かります。

公開鍵証明書を取得する

前段にてHPKIテストカードに含まれているアプリケーション識別子が分かりましたので、次は公開鍵証明書を取得していきます。

ガイドラインの「A.3.2 証明書の読み出し」にあるコマンドの通りにAPDUを発行しても、正しいデータは返ってきません。 これは、HPKIテストカードのEF識別子が、ガイドラインに記載されているEF識別子とは異なるためです。

HPKIカードはJIS X 6320に準拠しているため、各種暗号情報オブジェクトへのパス情報を含んだEF.ODが存在しています。 このEF.ODを使い公開鍵証明書へのパスを取得してから、公開鍵証明書を取得していきます。

f:id:medley_inc:20180712152231p:plain f:id:medley_inc:20180712152245p:plain 引用 ガイドライン

EF.ODを読み込む

prog02.rb

# prog02.rb
require "smartcard"
require "openssl"

def puts_response(response)
  puts "status = %04X" % response[:status]
  puts "data = %s" % response[:data].map { |i| "%02X" % i }.join(" ")
end

def decode_asn1(response)
  data = response[:data].reverse_each.drop_while { |i| i == 0xFF }.reverse
  return if data.empty?
  OpenSSL::ASN1.decode_all data.pack("C*")
end

context = Smartcard::PCSC::Context.new
begin
  card = context.card context.readers.first

  [
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02]
  ].each do |aid|
    # SELECTコマンドでアプリケーションを選択する
    apdu = [0x00,
            0xA4,
            0x04,
            0x00,
            0x10,
            *aid,
            0x00]
    card.transmit apdu.pack("C*")

    # EF.ODの読み出し
    apdu = [0x00,
            0xB0,
            0x91,
            0x00,
            0x00]
    response = card.transmit apdu.pack("C*")
    response = Smartcard::Iso::IsoCardMixin.deserialize_response response.unpack("C*")
    pp decode_asn1 response
  end
ensure
  context.release
end

EF.ODを読み込むとDER符号化されたデータが返ってきます。 これを OpenSSL::ANS1 モジュールで復号化すると、次に取得するべきEF識別子が分かります。

EF.ODのASN.1定義は以下のようになっているため、タグが4であるデータを読み込めば良さそうです。

CIOChoice ::= CHOICE {
  privateKeys          [0] PrivateKeys,
  publicKeys           [1] PublicKeys,
  trustedPublicKeys    [2] PublicKeys,
  secretKeys           [3] SecretKeys,
  certificates         [4] Certificates,
  trustedCertificates  [5] Certificates,
  usefulCertificates   [6] Certificates,
  dataContainerObjects [7] DataContainerObjects,
  authObjects          [8] AuthObjects,
}

prog02.rbを実行して実際に得られたデータ

[
 # 中略
 #<OpenSSL::ASN1::ASN1Data:0x00007f8b8e0ef7b0
  @indefinite_length=false,
  @tag=4,
  @tag_class=:CONTEXT_SPECIFIC,
  @value=
   [#<OpenSSL::ASN1::Sequence:0x00007f8b8e0ef7d8
     @indefinite_length=false,
     @tag=16,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value=
      [#<OpenSSL::ASN1::OctetString:0x00007f8b8e0ef800
        @indefinite_length=false,
        @tag=4,
        @tag_class=:UNIVERSAL,
        @tagging=nil,
        @value="\x00\x04">]>]>
 # 中略
]
[
 # 中略
 #<OpenSSL::ASN1::ASN1Data:0x00007f8b8d118df0
  @indefinite_length=false,
  @tag=4,
  @tag_class=:CONTEXT_SPECIFIC,
  @value=
   [#<OpenSSL::ASN1::Sequence:0x00007f8b8d118e18
     @indefinite_length=false,
     @tag=16,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value=
      [#<OpenSSL::ASN1::OctetString:0x00007f8b8d118e40
        @indefinite_length=false,
        @tag=4,
        @tag_class=:UNIVERSAL,
        @tagging=nil,
        @value="\x00\x04">]>]>,
 # 中略
]

どちらのアプリケーションも00 04がEF.CD(証明書オブジェクト情報)のEF識別子だということが分かります。

EF.CDを読み込む

prog03.rb

# prog03.rb
require "smartcard"
require "openssl"

def puts_response(response)
  puts "status = %04X" % response[:status]
  puts "data = %s" % response[:data].map { |i| "%02X" % i }.join(" ")
end

def decode_asn1(response)
  data = response[:data].reverse_each.drop_while { |i| i == 0xFF }.reverse
  return if data.empty?
  OpenSSL::ASN1.decode_all data.pack("C*")
end

context = Smartcard::PCSC::Context.new
begin
  card = context.card context.readers.first

  [
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02]
  ].each do |aid|
    # SELECTコマンドでアプリケーションをapdu
    選択する = [0x00,
            0xA4,
            0x04,
            0x00,
            0x10,
            *aid,
            0x00]
    card.transmit apdu.pack("C*")

    # SELECTコマンドでEF識別子`00 04`を選択する
    apdu = [0x00,
            0xA4,
            0x02,
            0x0C,
            0x02,
            0x00, 0x04]
    card.transmit apdu.pack("C*")

    # READ BINARYコマンドでファイルを読み込む
    data = []
    offset = 0
    loop do
      apdu = [0x00,
              0xB0,
              (offset & 0x7FFF) >> 8,
              (offset & 0x00FF),
              0x00]
      response = card.transmit apdu.pack("C*")
      response = Smartcard::Iso::IsoCardMixin.deserialize_response response.unpack("C*")
      data.concat response[:data]
      break if response[:data].all? { |e| e == 0xFF }
      break unless response[:status] == 0x9000
      offset += response[:data].size
    end
    pp decode_asn1 data: data
  end
ensure
  context.release
end

prog03.rbを実行して実際に得られたデータ

[
 # 中略
 #<OpenSSL::ASN1::Sequence:0x00007ffdf99aaf70
  @indefinite_length=false,
  @tag=16,
  @tag_class=:UNIVERSAL,
  @tagging=nil,
  @value=
   [#<OpenSSL::ASN1::OctetString:0x00007ffdf99ab038
     @indefinite_length=false,
     @tag=4,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value="\x00\x16">,
    #<OpenSSL::ASN1::Integer:0x00007ffdf99aafe8
     @indefinite_length=false,
     @tag=2,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value=#<OpenSSL::BN 0>>,
    #<OpenSSL::ASN1::ASN1Data:0x00007ffdf99aaf98
     @indefinite_length=false,
     @tag=0,
     @tag_class=:CONTEXT_SPECIFIC,
     @value="\x05\x17">]>
 # 中略
]
[
 # 中略
 #<OpenSSL::ASN1::Sequence:0x00007ffdfa072308
  @indefinite_length=false,
  @tag=16,
  @tag_class=:UNIVERSAL,
  @tagging=nil,
  @value=
   [#<OpenSSL::ASN1::OctetString:0x00007ffdfa072448
     @indefinite_length=false,
     @tag=4,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value="\x00\x16">,
    #<OpenSSL::ASN1::Integer:0x00007ffdfa0723d0
     @indefinite_length=false,
     @tag=2,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value=#<OpenSSL::BN 0>>,
    #<OpenSSL::ASN1::ASN1Data:0x00007ffdfa072380
     @indefinite_length=false,
     @tag=0,
     @tag_class=:CONTEXT_SPECIFIC,
     @value="\x05%">]>
]
 # 中略

これで公開鍵証明書ファイルのEF識別子が00 16であることが判明しました。

公開鍵証明書を読み込む

prog04.rb

# prog04.rb
require "smartcard"
require "openssl"

context = Smartcard::PCSC::Context.new
begin
  card = context.card context.readers.first

  [
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02]
  ].each do |aid|
    # SELECTコマンドでアプリケーションを選択する
    apdu = [0x00,
            0xA4,
            0x04,
            0x00,
            0x10,
            *aid,
            0x00]
    card.transmit apdu.pack("C*")

    # SELECTコマンドでEF識別子`00 16`を選択する
    apdu = [0x00,
            0xA4,
            0x02,
            0x0C,
            0x02,
            0x00, 0x16]
    card.transmit apdu.pack("C*")

    # READ BINARYコマンドでファイルを読み込む
    data = []
    offset = 0
    loop do
      apdu = [0x00,
              0xB0,
              (offset & 0x7FFF) >> 8,
              (offset & 0x00FF),
              0x00]
      response = card.transmit apdu.pack("C*")
      response = Smartcard::Iso::IsoCardMixin.deserialize_response response.unpack("C*")
      data.concat response[:data]
      break if response[:data].all? { |e| e == 0xFF }
      break unless response[:status] == 0x9000
      offset += response[:data].size
    end
    cert = OpenSSL::X509::Certificate.new(data.reverse_each.drop_while { |i| i == 0xFF }.reverse.pack("C*"))
    puts cert.to_text
  end
ensure
  context.release
end

HPKIテストカードからDER符号化された公開鍵証明書データが取得できるので、OpenSSL::X509::Certificate.newインスタンス化できます。

上記のprog04.rbを実行すると下記のような出力が得られます。

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 13023 (0x32df)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=JP, O=Japan Medical Association, OU=Digital Certificate Center, CN=HPKI-01-HPKI_JV2-forNonRepudiation
        Validity
            Not Before: Aug 15 15:00:00 2017 GMT
            Not After : Aug 15 14:59:59 2018 GMT
        Subject: C=JP, CN=JMACombi20413/serialNumber=TESTC20413
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:94:dd:09:40:f4:58:f9:0f:ec:3a:ea:e3:47:33:
 # 中略
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Authority Key Identifier:
                keyid:44:E9:20:05:4D:6D:C4:B7:FA:4B:F0:1B:C6:EA:C8:D6:5B:16:22:F4
                DirName:/C=JP/O=Ministry of Health, Labour and Welfare/OU=Director-General for Policy Planning and Evaluation/OU=MHLW HPKI Root CA V2
                serial:02

            X509v3 Subject Key Identifier:
                9E:E5:71:59:1E:A7:FC:1E:4A:31:F8:7B:30:0B:E3:7F:05:3D:9A:40
            X509v3 Key Usage: critical
                Non Repudiation
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.pki.med.or.jp/repository/crl/crl-sign2.crl

            X509v3 Subject Directory Attributes:
                0402..(..B..1(1&0$."1 ...
*.............Medical Doctor
            X509v3 Certificate Policies: critical
                Policy: 1.2.392.100495.1.5.1.1.0.1
                  CPS: http://www.pki.med.or.jp/certpolicy/

    Signature Algorithm: sha256WithRSAEncryption
         84:ae:95:45:5e:e7:64:8b:0c:6e:20:5f:9f:1f:0d:5c:ae:4a:
 # 中略

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 12927 (0x327f)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=JP, O=Japan Medical Association, OU=Digital Certificate Center, CN=HPKI-01-HPKI_JV2-forAuthentication-forIndividual
        Validity
            Not Before: Aug 15 15:00:00 2017 GMT
            Not After : Aug 15 14:59:59 2018 GMT
        Subject: C=JP, CN=JMACombi20413/serialNumber=TESTC20413
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:c6:f9:06:26:58:5e:11:b7:12:f2:8a:3e:97:0a:
 # 中略
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Authority Key Identifier:
                keyid:62:12:93:82:DE:3C:D7:FF:A8:D3:63:01:D3:01:6A:AE:6C:3B:C0:D4
                DirName:/C=JP/O=Ministry of Health, Labour and Welfare/OU=Director-General for Policy Planning and Evaluation/OU=MHLW HPKI Root CA V2
                serial:03

            X509v3 Subject Key Identifier:
                45:2B:7B:B4:47:89:3D:6C:05:6D:82:4D:4C:C8:80:B8:B4:B0:89:81
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.pki.med.or.jp/repository/crl/crl-auth2.crl

            X509v3 Subject Directory Attributes:
                0402..(..B..1(1&0$."1 ...
*.............Medical Doctor
            X509v3 Certificate Policies: critical
                Policy: 1.2.392.100495.1.5.1.2.0.1
                  CPS: http://www.pki.med.or.jp/certpolicy/

    Signature Algorithm: sha256WithRSAEncryption
 # 中略

それぞれのアプリケーションから正しく公開鍵証明書が取得できました。

電子認証ガイドラインによると、電子認証に使用する証明書はIssuerのCN(Common Name)がHPKI-01-*-forAuthentication-forIndividualであることが定められているため、 使用したHPKIテストカードでは、電子認証に使用するアプリケーション識別子はE8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 02であることが分かります。 また、電子署名に使用するアプリケーション識別子はE8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 01であることが分かりました。

最後に

以上でガイドラインの「A.2.2 JIS X 6320-15 に従った PKI カードアプリケーションの検索と利用」にある「PKI カードアプリケーションの検索」まで実装できました。

今後、次のステップである暗号計算を実装していきたいと思います。

電子レセプトについて調べた話

こんにちは、開発本部の竹内です。最近子どものプリンセスへの強い憧れに若干引いております。

さて先日、TechLunchという社内勉強会で「電子レセプト」について話しましたので、こちらでも簡単に紹介させていただきます。

レセプトとは

ところで、みなさまは「レセプト」についてご存知でしょうか?私はメドレーに入社するまで知りませんでした。

レセプトとは医療機関が支払基金へ診療報酬を請求するための明細書情報のことです。

と言っても、初めて聞かれる方もいらっしゃると思いますので、医療機関におけるお金の流れとともに簡単に説明します。

http://www.ssk.or.jp/kikin.images/kikin_image01.png

(支払基金ってどんなところ?|社会保険診療報酬支払基金より)

医療機関は「診療」の対価として、被保険者等(≒患者)からお金を受け取るわけですが、被保険者の加入する保険や公費によってその額は変わります。負担割合が3割の場合、残りの7割を被保険者が加入する保険組合などへ請求する必要があります。

この保険組合などへの請求を取りまとめ、内容を審査しているのが支払基金と呼ばれる組織で、医療機関は月に一度、前月の患者ごとの診療点数を計算し「レセプト」として支払基金に提出することになります。

http://www.ssk.or.jp/kikin.images/kikin_image03.png

(支払基金ってどんなところ?|社会保険診療報酬支払基金より)

レセプトには、請求する診療点数のほか、医療機関の情報、被保険者の情報(氏名などの基本情報、加入している保険者情報)、診療行為や傷病名に関する情報などが含まれています。

「レセプト」には紙と電子データとありますが、現在は原則として電子レセプトを提出することが求められているそうです(電子レセプト請求に係る猶予措置及び免除措置について|社会保険診療報酬支払基金)。

電子レセプトとレセ電ビューア

電子レセプトについての仕様は支払基金によって公開されています。今回は「電子レセプト作成の手引き」という資料を元に「医科」のレセプトについて調べて発表しました。

発表資料はこちら。 speakerdeck.com

電子レセプトの実体はCSV形式のシンプルなテキストファイルです(拡張子はUKEなので、UKEファイルと呼ぶこともあるようです)。ただ、電子レセプトの仕様を把握したとしても、やはりCSVファイルを見て内容を把握するのは至難の業です。ファイル上では診療行為や医薬品、傷病名はコードとして表現されているため、マスタデータを参照しなければその内容まで理解することはできないからです。

そこで登場するのが、レセ電ビューアというツールで、ORCA Projectによって公開されているフリーの電子レセプトビューアです。「レセ電=電子レセプト」ですね。

www.orca.med.or.jp

(※上記ページでは「レセ電ビューア」と「レセ電ビューワ」が混在していますが、本ブログでは「レセ電ビューア」で統一しています)

レセ電ビューアはWindowsUbuntuで動作し、上述したUKEファイルを読み込み、見やすく表示してくれる便利ツールです。ここからはレセ電ビューアをインストールし、電子レセプトを読み込んで表示するところまでを紹介したいと思います。

レセ電ビューアのインストール

レセ電ビューアはWindowsUbuntu上で動作しますので、まずはUbuntu環境を準備します。私はVirtualBox上にUbuntu環境を用意しました。 公式のインストールマニュアル(ubuntu環境へのレセ電ビューアインストール)に基づいて作業していきます。

# Keyringとapt-lineの追加
$ sudo su
$ wget -q https://ftp.orca.med.or.jp/pub/ubuntu/archive.key
$ apt-key add archive.key
$ wget -q -O \
/etc/apt/sources.list.d/jma-receipt-xenial50.list \
https://ftp.orca.med.or.jp/pub/ubuntu/jma-receipt-xenial50.list
$ apt-get update
$ apt-get dist-upgrade

# レセ電ビューアパッケージインストール
$ apt-get install jma-receview
$ apt-get install jma-receview-server

# レセ電ビューア起動
$ jma-receview

レセ電ビューアの設定

電子レセプトに含まれる診療行為などのコードに対応するマスタデータを参照するため、日レセ(jma-receipt)のDBを利用することができます。今回はDBFile形式でDBに接続します。レセ電ビューアに付属するスクリプトを実行し、jma-receiptのDBから必要なテーブルをダンプすることができます。このファイルをレセ電ビューアに設定することで、電子レセプトの表示がよりわかりやすくなります。

# レセ電ビューアで使うDBFileを作る
# https://ftp.orca.med.or.jp/pub/receview/manual/jma-receview.pdf
# 「2.7.4 DBFile の作成方法」
$ sudo su orca
$ ls -la /usr/share/jma-receview/db/
$ mkdir /var/tmp/dbfile
$ cp /usr/share/jma-receview/db/make_dbfile.sh /var/tmp/dbfile/
$ cd /var/tmp/dbfile/
$ sh ./make_dbfile.sh 20170101
$ ls -lh
合計 6.9M
-rwxr-xr-x 1 orca orca 2.9K  518 14:55 make_dbfile.sh
-rw-r--r-- 1 orca orca 1.2M  518 14:57 tbl_byomei.rdb
-rw-r--r-- 1 orca orca   89  518 14:57 tbl_dbkanri.rdb
-rw-r--r-- 1 orca orca 330K  518 14:57 tbl_hknjainf.rdb
-rw-r--r-- 1 orca orca 9.6K  518 14:57 tbl_labor_sio.rdb
-rw-r--r-- 1 orca orca 343K  518 14:57 tbl_syskanri.rdb
-rw-r--r-- 1 orca orca 5.1M  518 14:57 tbl_tensu.rdb

f:id:medley_inc:20180706112205p:plain

接続設定で「DBFile」を選択し、先ほど作成したDBFileを選択します。

レセ電ビューア近影

f:id:medley_inc:20180706112221p:plain

「ファイル」からUKEファイルを開くと、レセプトに基づいて患者基本情報、保険・公費情報、診療行為情報のほか、紙レセプトのプレビューや患者単位での電子レセプトを表示することができます。また、「編集モード」に切り替えることで患者情報や病名の編集が可能で、編集した内容でレセプトを再出力することもできるようです。

おまけ

ここまで紹介してきたレセ電ビューアですが、調べてみるとどうやらRubyで実装されているようです。これらのコードを読んでいくことで新たな地平を開くことができるかもしれません。

$ dpkg -L jma-receview | grep ruby
/usr/lib/ruby
/usr/lib/ruby/2.3.0
/usr/lib/ruby/2.3.0/jma
/usr/lib/ruby/2.3.0/jma/receview
/usr/lib/ruby/2.3.0/jma/receview/menu.rb
/usr/lib/ruby/2.3.0/jma/receview/intconv.rb
/usr/lib/ruby/2.3.0/jma/receview/base.rb
/usr/lib/ruby/2.3.0/jma/receview/dbfile_lib.rb
/usr/lib/ruby/2.3.0/jma/receview/gui.rb
/usr/lib/ruby/2.3.0/jma/receview/yearconv.rb
/usr/lib/ruby/2.3.0/jma/receview/exception.rb
/usr/lib/ruby/2.3.0/jma/receview/api.rb
/usr/lib/ruby/2.3.0/jma/receview/config.rb
/usr/lib/ruby/2.3.0/jma/receview/image.rb
/usr/lib/ruby/2.3.0/jma/receview/dialog.rb
/usr/lib/ruby/2.3.0/jma/receview/upstart.rb
/usr/lib/ruby/2.3.0/jma/receview/dbslib.rb
/usr/lib/ruby/2.3.0/jma/receview/thread.rb
/usr/lib/ruby/2.3.0/jma/receview/receview.rb
/usr/lib/ruby/2.3.0/jma/receview/sickname_edit.rb
/usr/lib/ruby/2.3.0/jma/receview/dayconv.rb
/usr/lib/ruby/2.3.0/jma/receview/isoimage.rb
/usr/lib/ruby/2.3.0/jma/receview/help.rb
/usr/lib/ruby/2.3.0/jma/receview/other_csv.rb
/usr/lib/ruby/2.3.0/jma/receview/print.rb
/usr/lib/ruby/2.3.0/jma/receview/env.rb
/usr/lib/ruby/2.3.0/jma/receview/generation.rb
/usr/lib/ruby/2.3.0/jma/receview/red2cairo.rb
/usr/lib/ruby/2.3.0/jma/receview/strconv.rb
/usr/lib/ruby/2.3.0/jma/receview/log.rb
/usr/lib/ruby/2.3.0/jma/receview/hokenconv.rb
/usr/lib/ruby/2.3.0/jma/receview/keybind.rb
/usr/lib/ruby/2.3.0/jma/receview/version.rb
/usr/lib/ruby/2.3.0/jma/receview/gtk2_fix.rb
/usr/lib/ruby/2.3.0/jma/receview/preview_widget.rb
/usr/lib/ruby/2.3.0/jma/receview/command.rb

# /usr/bin/jma-receviewもrubyで書かれてました(12000行以上ある…)

まとめ

今回は電子レセプトとレセ電ビューアについて、簡単に紹介しました。

わたしたちの生活とは切り離せない「医療」に関するシステムや仕様は、意外と一般公開されているものもあり、誰でも触れることができます。ただ、動作環境が制限されていたり、インターネット界隈のエンジニアがよく目にする技術とは異なるスタックで構築されていたり、それなりにハードルがあるように感じています。

これらのハードルを下げ、より多くの人が「調べてみよう」「ちょっと触ってみよう」と思うようになれば、医療に関わるシステムや仕様もよりシンプルで使いやすいものになり、ひいては各医療問題の解決・患者体験の改善につながっていくのではないかな、メドレーがつなげていきたいなと思っています。

最後はいいことを言って締めたい性分なのですが、いかがだったでしょうか。

ここまでお読みいただき、ありがとうございました。

お知らせ

メドレーの開発にご興味ある方は、こちらからご連絡ください。 www.medley.jp

7/27に開催されるデブサミ2018Summerに協賛させていただきます。ぜひ遊びにいらしてください。 event.shoeisha.jp

社内勉強会TechLunchで"JavaScript ASTことはじめ"という発表をしました

みなさん、こんにちは。開発本部エンジニアの平木です。こちらのブログの投稿自体はほぼ1年ぶりになりそうな勢いですが、みなさまお元気でしょうか?

弊社で定期的に開催してる社内勉強会TechLunchで自分の順番が回ってきたため、どうしようか迷った末にJavaScript ASTことはじめという発表をしたので、そのことについて書いていきます。

なぜJavaScript ASTについて話そうと思ったのか

現在、弊社のエンジニアメンバーのバックグラウンドで一番多数派なのは「元サーバサイドエンジニア」です。もちろん、業務ではサーバサイド・フロントエンド・ネイティブアプリとバックグラウンドに関わらず、必要に応じて分け隔てなく開発しています。

とはいえ、ちゃんとサービス開発自体はできるとしても、やはり得意な分野以外で基本原理など含めて把握して開発できるかというと、ちょっと難しいところもあります。しかし、そういった基本原理なんかを知っていると、その言語やツールなどの理解が捗るのは確かですよね。

そんな中、弊社で開発している人間がほぼ全て恩恵を受けているはずなのに、具体的にどんな風に動いているのかが一番分かりにくいであろうBabelひいてはJavaScript ASTの話をしたら、まあ興味持って話を聞いてくれるかなーということででこのテーマを選んだ次第です。

どのように伝えるか

自分はJavaScript ASTについてとても詳しいわけではないのですが、以前仕事でacornを使ってコンバータみたいなのを作ったりしていたので、それなりに興味は持っているという人間です。

ですので、どうやって紹介をしようかと悩んだ結果、ほぼ全面的にAST Exploreに頼っていくというスタイルにしました。AST Exploreは本当に最高ですね。前述の仕事をしていたときはこんな便利なツールはなかったんで、ひたすらASTに変換するコード書いては出来たASTを見て、それをトランスフォームさせて結果と睨めっこして試行錯誤するという毎日でした。

ということで、当日のスライドはこちらになります。

speakerdeck.com

スライドで紹介したデモはそれぞれこちらになります。

今回伝えたかったこと

まず、ASTがJavaScriptの発展にとても寄与しているものだということを知ってもらいたかったため、JavaScript ASTの今までの簡単な流れや、現在どのような形で使われているのかの説明をしました。(個人的にNode.jsの誕生とJavaScript ASTの存在が現在のフロントエンドの発展にとても重要だと思っているので)

最初のうちは聞いてる人も「何の話なんだろ…」感がありましたが、やはり実際に自分が使っているツールなどに使われているという説明をしたあとだと、聞いているメンバーも俄然興味が出てきたという雰囲気になった気がします(当社比)。

ASTの文法などは自分が説明するよりは、ちゃんと資料が揃っているので必要な部分以外簡略化しました。逆にちょっと端折りすぎたきらいもありますが、興味を持ったときに何となくでも調べる道標くらいにはなるかなと考えています。

次に知ってもらいたかったのは、やろうと思えばBabelのプラグインなんかもASTで作れちゃいますよということでした。仮にいきなり「Babelプラグイン作りましょう」となったとしても正直あまりピンと来ないと思いますが、どういう原理でプロダクトが動いているのか?が分かると、babel-handbookなどを読んでも理解が進むのではないかと思います。

AST Exploreのこと

このように今回の発表で全面的に活躍したAST Exploreですが、TechLunch中でも軽い説明だけで使ってしまったので、使いかたなど簡単にご紹介していきます。

AST Exploreとは

AST ExploreFelix Klingさんが、2014年頃から開発しているプロダクトです。

余談ですが、Felixさんは現在Facebookで働いていらっしゃるようで、facebook/jscodeshiftreactjs/react-docgenなんかの開発にも携わっていらっしゃる模様。(react-docgenはbabylonを使っているようですが)

ここまで書いてきた通りに、このツールは色々な言語をコピペするだけでASTをツリー形式で分かりやすく表示したり、トランスフォームさせることができたりするというASTを触るには大変便利なツールです。去年のv2.0のアップデートにより、セーブするとgistを匿名で作ってくれてリンクが生成されるなどの便利機能が付きました。

プロジェクトのREADMEに書いていますが、パーサだけであれば、かなりパースできるものが多く、またJavaScript / CSS / 正規表現 / Handlebarsに関してはトランスフォームまでできるようになっています。

READMEから抜粋すると以下のような感じです。

AST Exploreでパースできるもの

  • CSS:
    • cssom
    • csstree
    • postcss + postcss-safe-parser & postcss-scss
    • rework
  • GraphQL
  • Graphviz:
    • redot
  • Handlebars:
    • glimmer
    • handlebars
  • HTML:
    • htmlparser2
    • parse5
  • ICU
  • JavaScript:
    • acorn + acorn-jsx
    • babel-eslint
    • babylon
    • espree
    • esformatter
    • esprima
    • flow-parser
    • recast
    • shift
    • traceur
    • typescript
    • typescript-eslint-parser
    • uglify-js
  • JSON
  • Lua:
    • luaparse
  • Markdown:
    • remark
  • PHP
  • Regular Expressions:
  • Scala
    • Scalameta
  • SQL:
  • WebIDL
  • YAML

実験的だったりするけどパースできるもの

  • ES6: arrow functions, destructuring, classes, …
  • ES7 proposals: async/await, object rest / spread, …
  • JSX
  • Typed JavaScript Flow and TypeScript
  • SASS

パースしたものをトランスフォームできるもの

  • JavaScript
    • babel (v5, v6)
    • ESLint (v1, v2, v3)
    • jscodeshift
    • tslint
  • CSS
    • postcss
  • Regular Expressions
  • Handlebars
    • glimmer

AST Exploreの使い方の簡単な解説

サイトにアクセスするとこのような画面になっているはずです。

f:id:medley_inc:20180622111218p:plain

メイン画面

JavaScriptにフォーカスして解説していきますと、左ペインがASTに変換したいソースコード、右ペインが変換後のASTをツリー構造で見せています。

初期表示時に、左ペインのソースコードをクリックすると該当箇所のASTツリーが展開してハイライトします。また右ペインをポイントするとソースコードの該当箇所がハイライトします。お互いの関係が分かりやすい仕様になっています。

本来JavaScript ASTで生成されるものはJSONオブジェクトになりますが、右ペインの上のTreeJSONのタブを切りかえることによってASTの表示を変更することができます。

ヘッダー部分

ヘッダーに色々な機能がまとまっています。

初期表示では以下のようになっているはずです。

  • Snippet
    • 俗にいうファイルメニュー。
      • 新規作成・(gistへの)セーブ・(gistの)フォーク・シェアがある
  • JavaScript
    • パースする言語選択
      • ここでASTにしたい言語を切り替える
      • 選んだ言語によってTransformが使えなくなる
  • acorn
    • パーサ選択
      • 各言語のパーサを切り替える
  • Transform
    • トランスフォーマ選択
      • 選択した言語にトランスフォーマがあれば選択できるようになる
      • こちらを選択すると2ペインだったのが4ペインになる(後述)
  • default
  • ?
    • ヘルプ
      • GitHubのREADMEに飛ばされるだけです…

JavaScriptのトランスフォーム

先程説明したトランスフォームを選ぶと、メインの画面が4画面になります。

f:id:medley_inc:20180622111427p:plain

今までのソースコードASTツリーは変わりませんが、下に2つペインが追加されます。 左下がトランスフォーマコード、右下がトランスフォームした後のソースコードとなっています。

左下のトランスフォーマを色々触っていくと左上のソースコードが変換されて、右下に表示されるという流れですね。

以下JavaScriptコードのトランスフォームする際のTipsです

  • jscodeshiftを選択するとCtrl + Spaceでjscodeshiftの補完が効くようになります
  • babel-plugin-macroを選ぶとトランスフォーマのコード自体がそのままbabel-pluginとして使えるようになるので、プラグイン作るときに捗るはずです

まとめ

後で参加メンバーに聞いてみましたが、伝えたかったことは、ちゃんと伝わっていた様子だったので安心しました。最後のVue.jsのv1からv2のマイグレーションのデモは紹介した結果、JavaScript AST便利そうという感触になったと思います。

現在弊社のプロダクトで、JavaScript ASTをガッツリと使うようなプロジェクトはないのですが、Babelなどは全プロダクトで使用しており、結構プラグインを多用しているところもあるので、いざというときの基礎知識として覚えておいて損はないはずです。

こういった部分の勉強も欠かさず続けていきたいと改めて思う機会にもなりました。

弊社の開発文化など気になる方は、こちらからどうぞ。 www.medley.jp