TECHSCORE BLOG https://www.techscore.com/blog Webアプリ開発エンジニアのための技術情報サイト[テックスコア] Thu, 23 Apr 2020 08:27:59 +0000 ja hourly 1 https://wordpress.org/?v=5.8.1 TECHSCORE BLOG をリニューアルしました https://www.techscore.com/blog/2020/04/24/techscore-blog-%e3%82%92%e3%83%aa%e3%83%8b%e3%83%a5%e3%83%bc%e3%82%a2%e3%83%ab%e3%81%97%e3%81%be%e3%81%97%e3%81%9f/ Fri, 24 Apr 2020 00:00:12 +0000 https://www.techscore.com/blog/?p=25440 このたびブログをリニューアルしました。新しいブログは TECHSCORE BLOG です(同じタイトルですがはてなブログに移行しました)。

これまではエンジニア個人の技術的関心を中心に記事を公開してきました。
続きを読む...

The post TECHSCORE BLOG をリニューアルしました first appeared on TECHSCORE BLOG.

]]>
このたびブログをリニューアルしました。新しいブログは TECHSCORE BLOG です(同じタイトルですがはてなブログに移行しました)。

これまではエンジニア個人の技術的関心を中心に記事を公開してきました。今後はプロダクト開発にまつわる話をメインテーマとして、シナジーマーケティングのエンジニアリングとエンジニアの姿をお届けしていきます。

こちらの記事はアーカイブとして残し削除することはありません。今後は新しいブログにて記事の公開を行っていきます。

今後ともよろしくお願いいたします。

The post TECHSCORE BLOG をリニューアルしました first appeared on TECHSCORE BLOG.

]]>
パフォーマンスを考慮したIndex定義設計 https://www.techscore.com/blog/2019/12/25/performance_index/ Tue, 24 Dec 2019 23:00:12 +0000 https://www.techscore.com/blog/?p=25308 これは 😺TECHSCORE Advent Calendar 2019😺の25日目の記事です。

横田です。転職して3カ月程ですが前職までは Oracle や SQL Server を中心に触っていました。
続きを読む...

The post パフォーマンスを考慮したIndex定義設計 first appeared on TECHSCORE BLOG.

]]>
これは 😺TECHSCORE Advent Calendar 2019😺の25日目の記事です。

横田です。転職して3カ月程ですが前職までは Oracle や SQL Server を中心に触っていました。本格的にPostgreSQLを使うようになったのが今回始めてのため、SQL Server の言葉を借りて説明している箇所が有ります。予めご了承ください。

今回は、PostgreSQL 11 で実装された付加列Index (※) を中心としたパフォーマンス関連のお話をしたいと思います。商用DBと比較してしまうとOSS-DBに無い機能が多かったりするのですが、近年はパーティショニングやIndex機能の充実化など、溝を埋めていこうとする姿に感銘を受けました。なのでDBを使う側も実装されたものを使いこなせる様になろうと思います。

※「付加列Index」はSQL serverで同機能の呼び名です。機能の詳細は後述しますが、PostgreSQLでは同機能に名前が無いので、この記事では便宜上この機能をPostgreSQLでも「付加列Index」と書いていきます。

【まずはIndexの基本をおさらい】

今回のお話は「Indexって何だろう?」を卒業したところをスタートラインとしています。もしIndexについておさらいしたい方は以下のサイトを読んでみては如何でしょうか。まあ、Indexを簡単に言うと「検索を早くするためのもの」です。

→(参考)PostgreSQL公式サイトの説明(外部サイトに飛びます)

→(参考)B-treeインデックス入門(外部サイトに飛びます)

→(参考)Indexの種類について

 

【Index ( B-Tree ) の基本的なアクセスパスについて】

まず最初に、B-Tree型Indexの基本的なアクセスパスについて以下のオブジェクトを使って説明します。

※SQL serverに読み替えて読んでいる方は、非クラスタ化インデックスを作成したと想定してください。

[ テーブル定義を表したDDL ] ※データは入っている前提でお話します。

CREATE TABLE TEST1 ( x int  ,  y int  ,  z int ) ;
CREATE INDEX TEST1_x1 ON TEST1( x ) ;

(図1) 作成するテーブルのイメージ

このテーブルに対して以下のクエリを発行したとします。
SELECT x , y FROM TEST1 WHERE x = 3 ;

 

この場合、以下の流れになります。(図2)

1)Indexを使って[ x = 3 ] の行を探して、実データテーブルのアドレスを特定します。

2)そのアドレスを使って実際のテーブル行からデータを取得します。

この1~2の流れでデータを探す動作は、SQL serverの言葉を使うとRID Lookup、Oracleの言葉を使うとTABLE ACCESS BY INDEX ROWID と言います。PostgreSQLでの呼び名が解らないので、本記事では便宜上この動作をRID Lookupと呼んで説明をしていきます。RID Lookupが発生すると余計な処理が発生して遅くなると思ってください。

 

(図2) B-Tree Indexの基本的なアクセスパス

 

【カバリングインデックス】

先ほどのSQLではIndexのアクセスパスとしてRID Lookup(図3)が発生していたのですが、Indexスキャン時にノードの値を見れば x列の値は解るので、Indexに無いy列の情報だけ探しに行けば早くなるのかな?という発想が出てきます。

(図3)RID Lookupの流れ

ならばと言う事で、クエリのselect句で問い合わせている列を全部Indexに持たせてしまえば RID Lookupは発生しないのでは?という考え方から生まれたIndexをカバリングインデックスと呼びます。但し、何も考えずただ単にSelect 文で使う列を入れた複数列インデックス(※)を作れば良いのかというと決してそうではなく、カバリングインデックスが使用されて実行計画が Index-only-scan となるためには一定の条件が必要です。

※商用DBで 複合Index と言われているIndexのことです。PostgreSQLでは複数列インデックスと呼びます。

 

▽カバリングインデックスが使用される条件


  1. Indexの種類が Index-only-scan をサポートしていること。
  2. クエリはIndexに格納されている列だけを使用すること。

1.はPostgreSQLのサイトを確認してバージョンと型を確認してみてください。

→(参考)Indexの種類(PostgreSQL 11)

2.は簡単な話で、最初に書いたIndexは x列だけに設定されているので、y列にもIndexを定義すれば良いだけです。つまり以下のIndex を作成すれば良いです。

CREATE INDEX TEST1_x1 ON TEST1( x , y ) ;

「Indexに格納されている列だけを使用する」という条件はそのままの意味で、カバリングインデックスが使用されるパターンは以下の通りです。

[使用する]

SELECT x  FROM TEST1 WHERE x = 3 ;

SELECT x , y FROM TEST1 WHERE x = 3 and = 3 ;

[使用しない]

SELECT x , y , z  FROM TEST1 WHERE x = 3 ;

SELECT x , y FROM TEST1 WHERE x = 3 and = 3 ;

 

なんだ、カバリングインデックスって簡単じゃないか。これでクエリが早くなるな。とほっこりした方は詰めが甘い。では、もう少しPostgreSQLのアーキテクチャを覗いてみましょう。

DBは複数のユーザー、複数のトランザクションが同時に様々な処理を行います。それらのトランザクションの一貫性やデータとしての整合性を担保しているのはロック機構だけではありません。MVCC(MultiVersion Concurrency Control:多版型同時実行制御)と言って、それぞれのトランザクション毎にスナップショットを用意し、それに対して操作を行っています。トランザクションが終わったらトランザクション隔離レベルで定めたルールに従って他のスナップショットと帳尻を合わせるという制御方式が取られています。その処理においてもカバリングインデックスのアクセスパスの無駄を省くよう考えておかないといけません。

→(参考)MVCC(MultiVersion Concurrency Control:多版型同時実行制御)

それを踏まえて、カバリングインデックスが最速で動くための付随条件を考えてみましょう。

▽カバリングインデックスが最速になる条件


  1. 検索された各行が問い合わせのMVCCスナップショットに対して「可視」であること。(※)
  2. 複数列インデックスになる場合、複数列インデックス が効率の良い構成で作成されていること。
  3. 無駄なIndex がないこと。

※PostgreSQL以外で読み替えている方は1は考えなくても良いと思います。

1.検索された各行が問い合わせのMVCCスナップショットに対して「可視」であること。

PostgreSQLならではの仕様なので、少し説明します。PostgreSQLには可視性マップ(Visibility Map)という仕様があります。これはPostgreSQLが追記型アーキテクチャであるが故の仕様(例えばUPDATE文を実行すると元レコードを無効化したうえで、修正値で新しいレコードを追加するという仕様)のため、VACUUMする必要があるかどうかを判断するため、あるいはIndexを読んだ際にテーブルのレコード本体と一致しているか確認するために読みに行く必要があるかどうか、といった判断をする必要が有るために存在します。(図4)

 

(図4) 追記型アーキテクチャで起こる問題

可視性マップは、各ブロックのレコードの可視性状態を保持するファイルで、テーブルヒープの各ページ毎に可視/不可視の情報をビットマップで保持しています。(図5)  実態は拡張子 “_vm” のファイルで、ブロックをVACUUMする必要があるかどうかの判断とIndex-Only Scan(レコード本体を見ない)できるかどうかの判断に使います。

 

(図5) 可視性マップ(Visibility Map)の構造

全てのビットが立っていると、ブロック内の全タプルが全トランザクションに可視である(つまりIndexの情報と実データテーブルは一致している)事が解るので、テーブルファイルにアクセスしなくても、Indexで得たタプルのデータがそのまま使えることが解ります。(図6) パフォーマンス向上のためにも、可能な限りビットを立たせるようにしたほうが良いと思います。業務仕様で更新の多いテーブルでは他の施策を考えたほうが良いでしょう。

 

(図6) 可視性マップの使われ方

こういった無駄な動きをさせないようにするために、可視性マップのフラグを立ててください。というのが1.で言いたかったことです。実際のアクションとしては「VACUUM を実行する」で正解なのですが、” おまじない ” の様にするのではなく、内部動作を理解したうえで行うと「カバリングインデックスが有効になっているのに何故か遅い」という場面でも対処法が判るようになると思います。

2.複数列インデックスになる場合、複数列インデックス が効率の良い構成で作成されていること。

複数列インデックスの特性はPostgreSQLの公式マニュアルにも記載されていますが、定義列の順番が重要で一番最初(一番左)に列を最初に検査します。WHERE句で一番最初の列を使用しなかったりすると、プランナが Table Scan のほうが早いと判断してしまう傾向があるようなので、Index設計はプログラム設計のフェーズでしっかり吟味する必要があると思います。(図7)

 

(図7) 複数列インデックスの使われ方

一番賢い作り方としては、回答を返すだけ列(別にIndexとして機能しなくてもいい列)を末尾の列することだと公式マニュアルにも書いてあります。パフォーマンスを考慮したIndexの一例として書きましたが、上記を考慮してご自身の環境でもテーブル設計を見直しては如何でしょうか。

 

【付加列インデックス】

SQL serverではお馴染みの機能ですが、PostgreSQL 11より実装されました。Index scanだけでクエリの回答をしようという考え方はカバリングインデックスと同じですが、[Indexとして機能させたい列][回答を返す値だけを保持する列]とを分離させたIndexです。(図8)

 

(図8) 付加列インデックスの構造

作り方は簡単で[回答を返す値だけを保持する列]をINCLUDE句に指定すれば良いだけです。

[ サンプルDDL ]
CREATE INDEX TEST1_x1 ON TEST1( x )  INCLUDE( y , z );

[使用例]
SELECT x , y , z FROM TEST1 WHERE x = 3 ;

上記SQL を実行すると、TEST1_x1のリーフノードまで検索されて実データテーブルのアドレス”■”まで辿り着くのですが、そのリーフノードにInclude句で指定した列のアドレスではなく実値として格納されているので、可視性マップを見て全て「可視(ビットが立っている)」であれば、その値を使って結果を返します。 RID Lookupが発生せず高速なスキャンが実現できます。

 

でも万能なものなんてありません

INCLUDE句はPRIMARY KEYの制約として書くことも出来ます。便利ですが一定の制約もあります。INCLUDE句に含めるペイロード列に、インデックス型の最大サイズを超えるタプルを指定すると失敗します。また、INCLUDE句に列を含め過ぎてインデックスサイズが膨張すると、検索が遅くなるという元も子もない状態に陥ります。

また、カバリングインデックスの項でも触れましたが、Index scanの際に必ず可視性マップを確認しに行きますので、そこでフラグが立っていなければ、結局実データテーブルを見に行きますので Indexが肥大化している分、逆に遅くなる可能性があります。

複数列インデックスについても、4列を超えた複数列キー定義は問題があると公式サイトにも書いています。

しっかりと考えて欲しいこと

CRUDのRとCUDを分離した分散構成でもない限り、クエリの高速化は更新処理の速度劣化とトレードオフの関係です。ただ、更新処理はストレージアクセスを先送りできる特性が有るので、クエリの高速化に偏重する考え方でも決して間違いではないのですが、システムの特性を鑑みたバランスが一番大切だと思います。要はDBを理解した上でのプログラム設計の重要性に帰結するのではないでしょうか。

最後に

DBを中心としたパフォーマンスチューニングを10年程経験していますが、前職、前々職の現場で「ある日突然遅くなったのでチューニングしてほしい」という依頼を度々受けたことがあります。統計情報が劣化した?IndexかTableの断片化が激しい?Indexが過多?不足?色々と調べることはありますが、原因究明にDBだけを見ている開発者が多いと感じます。プログラム側でトランザクションの設計は適切ですか?そのコミットタイミングやコミット間隔は適切ですか?システム設計時の想定処理件数に対して、今の件数で処理するとどうなりますか?設計時に運用上のデータ逓増をどれだけ設計に取り入れていますか?という質問に閉口されてもチューニングを依頼された側としては困ってしまいます。

DB内で更新データがどのように流れているのか、どこで管理されているのかを知っていれば、特にコミットタイミングやコミット間隔が適切でない事態は防げるかもしれません。今回はIndexのお話でしたが、機会があればパフォーマンスの劣化を防ぐ設計の考え方についてお話しできればと思います。

 

The post パフォーマンスを考慮したIndex定義設計 first appeared on TECHSCORE BLOG.

]]>
品質とは https://www.techscore.com/blog/2019/12/24/quality-considerations/ Mon, 23 Dec 2019 23:00:53 +0000 https://www.techscore.com/blog/?p=25364 😺TECHSCORE Advent Calendar 2019😺の24日目の記事です。

テストチームを離れて数年、久しぶりに品質系の仕事に携わることになり、改めて「品質とは何か」を考えてみました。
続きを読む...

The post 品質とは first appeared on TECHSCORE BLOG.

]]>
😺TECHSCORE Advent Calendar 2019😺の24日目の記事です。

テストチームを離れて数年、久しぶりに品質系の仕事に携わることになり、改めて「品質とは何か」を考えてみました。

「品質とはなんぞや」と問われたら、どのような回答が返ってくるのでしょうか。
バグの発生度合?、信頼性の高低?、安全性の高低?、操作性の良し悪し?、お値段の高低?
どれも文脈によっては当てはまりそうです。

 

悩んでも答えは出そうにないので、規格の定義やその道の権威の方の考えに頼ることにします。

  • 広辞苑(第7版)では、「品物の性質。しながら。」
  • ISO 9000:2015では「対象に本来備わっている特性の集まりが、要求事項を満たす程度。」
  • JIS X 25010:2013 では「明示された状況下で使用されたとき、明示的ニーズ及び暗黙のニーズをソフトウェア製品が満足させる度合い。」
  • PMBOKでは「一連の固有の特性が要求事項を満足している度合い。」
  • フィリップ・クロスビー氏の定義では「品質とは『要求条件への適合』である。」(ソフトウェア品質知識体系ガイド(第2版)P16)
  • ジェラルド・ワインバーグ氏の定義では「品質は誰かにとっての価値である。」(ソフトウェア品質知識体系ガイド(第2版)P17)

となっていました。

 

これらを見ると、品質とは「要求を満たしている度合」と「満足、価値を生み出している度合」という2つの考え方があるように見受けられますが、ISOなどの規格では要求の妥当性を確認するためのプロセスも定められているため、「満足、価値を生み出している度合」と考えられます。

 

品質が「満足、価値を生み出している度合」とすると、誰にとっての満足、価値を考えるべきなのでしょうか。
真っ先にお客さま(利用者)の存在が浮かびますが、他にも従業員、経営者、株主、取引先などのステークホルダーが存在します。

 

また、最も重要な指標であるお客さま(利用者)の満足、価値をどのような観点で整理するべきでしょうか。
東京理科大学名誉教授の狩野紀昭氏が提唱した「狩野モデル」を用いて整理するのが良さそうです。
これはお客さま(利用者)の満足、価値に影響を与える品質要素を以下のように分類し、優先度を整理するものです。

  • 当たり前品質:充足されていても当たり前と受け取られるが、不充足であれば不満を引き起こす品質要素
  • 魅力的品質:充足されていれば満足を与えるが、不充足であっても仕方ないと受け取られる品質要素
  • 一元的品質:充足されていれば満足を与えるが、不充足であれば不満を引き起こす品質要素
  • 無関心品質:充足されていても不充足であっても満足度には影響を与えない品質要素
  • 逆評価品質:充足されていれば不満を引き起こし、不充足であれば満足を与える品質要素

こうやって整理すると、お客さま(利用者)が気にしないポイントや逆に離反をまねくようなポイントに注力するリスクを軽減できますね。

 

この点をさらに掘り下げて考えたいところですが、それはまたいつか。

The post 品質とは first appeared on TECHSCORE BLOG.

]]>
ITP 1.0 から 2.3+α までの解説 https://www.techscore.com/blog/2019/12/23/itp-1-0-to-2-3-plus-alpha/ Sun, 22 Dec 2019 23:00:23 +0000 https://www.techscore.com/blog/?p=25100 これは TECHSCORE Advent Calendar 2019 の 23 日目の記事です。

こんにちは。松本です。
続きを読む...

The post ITP 1.0 から 2.3+α までの解説 first appeared on TECHSCORE BLOG.

]]>

これは TECHSCORE Advent Calendar 2019 の 23 日目の記事です。

こんにちは。松本です。

社内向けに作成した ITP(Intelligent Tracking Prevention) に関するドキュメントを再編集し、ブログ記事として以下に公開します。対象は、ITP 1.0 から ITP 2.3 に加え、2019/12/10 に発表された Preventing Tracking Prevention Tracking までとなります。

WebKit のソースコードに目を通すところまでは出来ていないので、あくまでも WebKit ブログから読み解ける内容が中心となります。

前提知識(用語など)

まず、ITP に関連して頻出する用語をピックアップして解説します。

WebKit

WebKit(ウェブキット)は、HTML や CSS などで記述されたドキュメントを解釈し、ユーザインタフェース上で視覚的に表現するための、ブラウザエンジン(レンダリングエンジン)と呼ばれるソフトウェアコンポーネントのひとつ。Apple が中心となって OSS として開発し、Safari や PlayStation 4 インターネットブラウザーなどで使用されています。

ITP は Safari の機能だと認識されることも多いようですが、実際は WebKit の機能です。また、iOS デバイス上で App Store を介し配信されるアプリが使用するブラウザエンジンは WebKit で、iOS 版の Google Chrome も WebKit を使用しています(iOS 版以外は Blink を使用)。

しかし、執筆次点では、ITP 機能を利用しているのは Safari のみのようです(一次情報が見つからないのですが)。

document.cookie が扱うクッキー

document.cookie を介して JavaScript で扱えるクッキーは、1st-party ドメインのクッキーです。

RFC 6265 の 5.3. Storage Model にあるように、Set-Cookie にて HttpOnly 属性が付けられたクッキーは、document.cookie で上書きすることはできません(ブラウザの種類やバージョンにより挙動が異なる)。

Session Cookie と Persistent Cookie

呼び名を意識することがあまり無いかもしれませんが、Set-Cookie や document.cookie に Expires 属性を付けないクッキーが Session Cookie で、付けたものが Persistent Cookie です。前者はブラウザを閉じると消えますが、後者はブラウザを閉じても指定した有効期限内は残ります。

eTLD+1

effective top-level domain(.co.jp や .com など)に、ドメインラベルをもうひとつ追加した example.co.jp や example.com といったドメイン(top privately-controlled domain)を指します。www.example.co.jp や www.example.com は eTLD+1 ではありません(これらは eTLD+2)。

リンクデコレーション

リンク先 URL のクエリ文字列などに追加のパラメータなどを仕込むことで、リンク元からリンク先に任意の情報を受け渡す仕組みです。

この仕組みを使うことで、リンク元からリンク先にページ訪問者の識別子を渡すことが可能になり、リンク先側でその識別子を 1st-party クッキーに保存しておけば、3rd-party クッキーを介さないクロスサイトトラッキングを実現できます。

リファラ URL が、リンクデコレーションとして利用されることもあります。

ITP では、次の条件を満たす場合に、リンクデコレーションによるクロスサイトトラッキングであると判定します。

  • リンク元ページが、ITP によりクロスサイトトラッカーとして識別されたドメイン(eTLD+1)である
  • リンク先ページの URL にクエリ文字列、あるいはフラグメントが含まれている

1st-party バウンストラッキング

HTTP リダイレクトを用いて行われるクロスサイトトラッキングです。この手法では、ユーザーを別のページにナビゲートする際、間にトラッカーサイトを挟みます。

例えば、サイト from-domain からサイト to-domain に移動する場合を考えます。通常は、from-domain から to-domain に直接リンクさせます。

from-domain --[Link]--> to-domain

1st-party バウンストラッキングでは、ユーザーが from-domain 上のリンクをクリックするとまず tracker-domain に移動し、そこから to-domain にリダイレクトさせます。この時、1st-party バウンストラッカーである tracker-domain は、ユーザーに関する情報を、自ドメインの 1st-party コンテキストでクッキーとして記録できます。

from-domain --[Link]--> tracker-domain --[Redirect]--> to-domain

パーティションキャッシュ(Partitioned Cache)

3rd-party コンテキストでロードされたリソースに関する WebKit のキャッシュ機能です。

A partitioned cache means cache entries for third-party resources are double-keyed to their origin and the first-party eTLD+1. This prohibits cross-site trackers from using the cache to track users. Even so, our research has shown that trackers, in order to keep their practices alive under ITP, have resorted to partitioned cache abuse.

上の説明文は ITP のブログ記事からの引用です。パーティションキャッシュでは、3rd-party コンテキストでロードされたリソースのキャッシュを、そのリソースの Origin と 1st-party の eTLD+1 の組み合わせごとに隔離して保存します。

例えば、https://www.techscore.com 配下にある /3rd-page.html というリソースを、次の四つのページ配下にて 3rd-party コンテキストとして読み込んだとします。

(1) https://example.com/1st-page-1.html
    |
    +- https://www.techscore.com/3rd-page.html

(2) https://example.com/1st-page-2.html
      |
      +- https://www.techscore.com/3rd-page.html

(3) https://sub.example.com/1st-page-3.html
      |
      +- https://www.techscore.com/3rd-page.html

(4) https://example.org/1st-page-4.html
      |
      +- https://www.techscore.com/3rd-page.html

結果として、https://www.techscore.com/3rd-page.html は、(1) (2) (3) で共通のキャッシュと、(4) のキャッシュの、二つが保存されます。(1) (2) (3) はいずれの 1st-party ドメインも eTLD+1 が example.com で、(4) のみ eTLD+1 が example.org であるからです(.com ではなく .org)。

このパーティションキャッシュは、クロスサイトトラッカードメインであるかどうかに関係なく、全ての 3rd-party コンテキスト内のリソースキャッシュに適用されます。

各バージョンの仕様

以降の解説においては、ユーザーが対象ドメインに対し、1st-party コンテキストで最後にアクセスした日時を UIT(User Interaction Timestamp)と呼ぶこととし、そこを起点としたタイムラインを記載しています。

ITP 2.0 の記事で次のように書かれており、(少なくとも ITP 2.0 以降は)ブラウザを使っていない期間を経過日数としてはカウントしないようです。

The user doesn’t use Safari which means that these three days do not count towards the 30 days before website data purge.

なお、ITP によって規制を受けるクッキーは、基本的にクロスサイトトラッカードメインの Persistent Cookie ですが、2019/12 に発表された Preventing Tracking Prevention Tracking からは、クロスサイトトラッカードメインだけにとどまらない規制がかかるようになりました。

以下、ところどころで ITP 特有の技術用語が出てきますが、それらは記事の後半に説明を掲載していますので、そちらをご参考ください。

ITP 1 系

  • UIT を起点とするタイムライン
  1. UIT から 24 時間以内: クロスサイトトラッカードメインは、3rd-party コンテキストでのクッキーの読み書きが可能。
  2. UIT から 24 時間経過: クロスサイトトラッカードメインは、3rd-party コンテキストでのクッキーの読み書きが不可能に。
  3. UIT から 30 日以降: クロスサイトトラッカードメインに関するクッキーを含めたすべてのデータが削除される。

ITP 1.0(2017/6)

  • タイムライン上 2 で作成されたクッキーは、パーティション化して保存。
  • タイムライン上 3 の削除処理は 1 時間に 1 回のサイクルで実行。3rd-party コンテキストで新たに追加されたデータも同サイクルで削除され続ける。

【参考】

ITP 1.1(2018/3) での変更点

  • タイムライン上 2 で作成されたクッキーは、パーティション化された上で、Session Cookie として存在。
  • タイムライン上 3 で実際に削除されるまでの間、同ドメインに対する新たなクッキー作成はブロック。
  • Storage Access API が導入された。クロスサイトトラッカードメインは、API を介して自身の 1st-party クッキーにアクセス可能に(同時に UIT も更新される)。

【参考】

ITP 2 系

  • UIT を起点とするタイムライン
  1. UIT 時点: クロスサイトトラッカードメインは、3rd-party コンテキストでのクッキーの読み書きが不可能に。
  2. UIT から 30 日以降: クロスサイトトラッカードメインに関するクッキーを含めたすべてのデータが削除される。

ITP 2.0(2018/6) での変更点

  • タイムライン上 1 で作成されたクッキーは、パーティション化された上で、Session Cookie として存在。
  • Storage Access API へのアクセスに、ユーザーの承諾が必要に。承諾されていないドメインからのアクセス時にプロンプトが表示される。ここでの承諾は、タイムライン上 2 の削除処理とともに無効化される。
  • 1st-party バウンストラッカーも、クロスサイトトラッカーとして扱われるように。
  • クロスサイトトラッカーに対する 3rd-party リクエスト時の Referer ヘッダーを Origin にダウングレード。
  • ポップアップを利用したフェデレーションログインによる 3rd-party クッキーへの依存を、一時的措置として動作可能に。

【参考】

ITP 2.1(2019/2) での変更点

  • タイムライン上 1 でクッキーの作成自体が不可能に。パーティションクッキー機能は廃止に。
  • document.cookie を介して作成されたクッキーは有効期限が最大 7 日間に。
  • クロスサイトトラッカードメインのパーティションキャッシュの有効期限が 7 日間に。
  • DNT のサポートが廃止に。
  • ITP 2.0 で一時的に動作可能としたフェデレーションログインシナリオへの一時的措置を廃止に。

【参考】

ITP 2.2(2019/4) での変更点

  • リンクデコレーションと判定されたリンク先ページ上で documento.cookie を介して作成されたクッキーの有効期限が最大 24 時間に。

【参考】

ITP 2.3(2019/9) での変更点

  • リンクデコレーションによるリンク先ページ上での、スクリプト書き込み可能な Cookie 以外のデータ(LocalStorage など)は、最終アクセスから 7 日後に削除されることに。
  • リファラを使ったリンクデコレーションも規制対象になり、document.referrer で読み取れる URL が eTLD+1 にダウングレード。
  • Storage Access API へのアクセスをユーザーが明示的に拒否する場合のみユーザーのアクションが必要になるよう変更。二度、拒否すると、以降はプロンプトは表示されなくなり、アクセスが拒否されたままとなる。
  • ITP がオフの場合の document.hasStorageAccess() では true が返るように。
  • macOS 上 の Safari 13 に ITP デバッグモードが装備された。

【参考】

Preventing Tracking Prevention Tracking(2019/12) での変更点

  • 全てのクロスサイトリクエストの Referer ヘッダーを、Origin にダウングレード。
  • ドメインの分類に関係なく、1st-party コンテキスト上でユーザ操作が一度もないドメインは、3rd-party コンテキストでのクッキーへのアクセスがブロックされるように。また、この状態での document.hasStorageAccess() では false が返るように。

【参考】

ITP 特有の技術に関する補足

機械学習によるドメインの分類

ITP では、アクセスしたドメイン(eTLD+1)がクロスサイトトラッキング機能を有するかどうかを、機械学習モデルを使って分類し、デバイス上に記録します。ここでクロスサイトトラッカーとして分類されたドメインが、ITP によって各種制限を受けることになります。

  • ITP 1.0 - 導入。
  • ITP 2.0 - 1st-party バウンストラッキングを行うドメインも、クロスサイトトラッカーとして扱われるように。

パーティションクッキー(Partitioned Cookies)

ITP 機能の一部として用意された特別なクッキー。クロスサイトトラッカーのクッキーを、パーティションキャッシュと同様に、1st-party の eTLD+1 ごとに隔離保存します。3rd-party コンテキストからのアクセスはできません。

  • ITP 1.0 - パーティションクッキーを導入。
  • ITP 1.1 - パーティションクッキーはすべて Session Cookie として扱われるように。
  • ITP 2.1 - パーティションクッキーを廃止。

Storage Access API

3rd-party の埋め込み(iframe)が、自身が 1st-party コンテキストで作成したクッキーにアクセスする仕組み。iframe タグの sandbox 属性に allow-storage-access-by-user-activation を指定することで利用可能。

Storage Access API は、二つのメソッド document.hasStorageAccess(), document.requestStorageAccess() を持っています。

document.hasStorageAccess() は、ドキュメントがその 1st-party クッキーにアクセス可能であるかを boolean 値で示す Promise を返します。

document.requestStorageAccess() は、アクセスが許可された場合と拒否された場合で処理を切り分ける Promise を返します。

  • ITP 1.1 - Storage Access API を導入。
  • ITP 2.0 - Storage Access API へのアクセスに、ユーザーの承諾が必要に。承諾されていないドメインからのアクセス時にプロンプトが表示される。
  • ITP 2.3 - Storage Access API へのアクセスをユーザーが明示的に拒否する場合のみ、ユーザーのアクションが必要に。二度、拒否すると、以降はプロンプトは表示されなくなり、アクセスが拒否されたままとなる。また、ITP がオフの場合の document.hasStorageAccess() の戻り値が、true に変わった。
  • Preventing Tracking Prevention Tracking(2019/12) - ドメインの分類に関係なく、1st-party コンテキスト上でユーザ操作が一度もないドメインは、document.hasStorageAccess() が false を返すように。

【参考】

最後に

みなさん、良いクリスマスを!

The post ITP 1.0 から 2.3+α までの解説 first appeared on TECHSCORE BLOG.

]]>
Pythonで始めるアルゴレイヴ入門 https://www.techscore.com/blog/2019/12/22/algorave-python/ Sat, 21 Dec 2019 23:00:20 +0000 https://www.techscore.com/blog/?p=23951 これは 😺TECHSCORE Advent Calendar 2019😺の22日目の記事です。
続きを読む...

The post Pythonで始めるアルゴレイヴ入門 first appeared on TECHSCORE BLOG.

]]>
これは 😺TECHSCORE Advent Calendar 2019😺の22日目の記事です。

Introduction(はじめに)

アルゴレイヴとは

アルゴレイヴ(Algorave)というものをご存じでしょうか?アルゴレイヴとはAlgorithm(アルゴリズム)とRave(皆で楽しむこと)を組み合わせた造語です。音楽や映像をプログラミングで即興的に作り上げるライブコーディングという技術を使用し、ライブコーダーが様々なアルゴリズムから生成された音楽を流すイベントのことを指します。2011年イギリスで最初のイベントが開催され、現在世界で広がりつつあります。日本でも度々開催されています(algorave.tokyo)


ライブコーディングとは

ライブコーディングとは即興的にプログラムを書き/編集しながら、音楽や映像を奏でるリアルタイム・パフォーマンスのことです。作りこんだものではなく、偶然性や即興性を楽しむものでパフォーミングアートにおける新たな表現形態として注目されています。

とはいってもなかなか想像しづらいかもしれません。これから紹介するFoxDotの公式サイトでデモ動画が公開されているので一度見てみて下さい。きっと試してみたくなるはずです。
また、PyConJP2019でも紹介されていたのでこちらもご参照ください(Pythonでライブをしよう -FoxDotを使った新時代のPython活用法-)

Requirement(環境)

では早速ライブコーディングを始めてみましょう。ライブコーディング環境としてTidalCyclesやFoxDot,Sonic Piなどが挙げられますが、今回はSuperColliderとFoxDotを使います。SuperColliderとは強力な音響合成エンジンであり、FoxDotはSuperColliderを操作するPythonライブラリです。
図で表すとこのような構成になります。

 Install(インストール)

インストールするものは四種類だけです。
  • Python(3系推奨,2系でも動きます)
  • Git
  • SuperCollider(バージョン3.8以上)
  • FoxDot(pipやAnacondaでインストール可能)

詳しいインストール方法は公式ページをご覧ください

StartUp(立ち上げ)

立ち上げもとても簡単です。三つのステップで立ち上げていきます。
  1. SuperColliderを起動する。
  2. SuperColliderのコンソールからFoxDotと通信するように設定。
    >>> FoxDot.start
  3. コマンドラインを立ち上げFoxDotを起動。
    $ python -m FoxDot
FoxDotのインターフェースが開き準備完了です。

 Getting Started(入門)

音の再生

では早速音を鳴らしてみましょう。FoxDotのコンソールに

Clock.bpm = 60
p1 >> pluck()

と入力し、

CTRL+ENTER
で実行します。すると弦をはじいたような音(プラック音)が鳴るはずです。これは1行目でテンポを指定し、二行目でメロディを奏でています。pluckの引数が指定されていないので4つ打ちで音が鳴っているはずです。また、pluckが音色を指定しています。bassやstar,sawなどに変更してみてください。音色が変わります。

音の停止

音を止めるときには

p1.stop()
で指定した音が停止し、
Ctrl+.
ですべての音が停止します。

音の変更

音の高さや長さ,振幅などを指定することによって音を変更することができます。
最初の引数は音の高さを表し、明示的に命名する必要はありません。
p1 >> pluck([0,2,4], dur=[1,1/2,1/2], amp=[1,3/4,3/4])
degree : 音程が指定できます。(命名する必要なし)
dur : 音の長さが指定できます。(1:四分音符,1/2:八分音符)
amp : 音量が指定できます。

音のグループ化

また複数の値を括弧で囲むことによって音をグループ化することもできます。

b1 >> bass([(0,9),(3,7)], dur=4, pan=(-1,1))
[] : 角括弧でグループ化すると音を分割し連符を表現することができます。
() : 丸括弧でグループ化すると同時に鳴り和音を鳴らすことができます。
pan : 音のLRチャンネル -1:L100%,1:R100%((-1,1)なので一つ目の和音が左から、二つ目は右から聞こえます)

ドラム

次のようにドラムも演奏可能です。

d1 >> play("x-o-")
x : バスドラム
o : スネアドラム
- : クローズドハイハット
= : オープンハイハット
などなどたくさんありますが、今回は省略します。
ピアノと同様、音の変更や同時に再生することも可能です。
d1 >> play("xxox")
d2 >> play("---(-=)", pan=0.5)
このような感じでDTMのように打ち込むことも可能です。
d1 >>("x x x x ")
d2 >>("  o   o ")
d3 >>("=-------")
またピアノと同様グループ化もできます。
d1 >> play("x[--]o(=[-o])")

Pattern(パターン)

アルゴレイヴというからにはアルゴリズムから音楽を生成する必要があります。アルゴリズムを組む上でよくリストの操作などを使うのですがFoxDot特有の書き方もあるので少し見ていきましょう。まず一般的にPythonで用いられるlistですが、もし全ての数に同じ数をかけたい場合どうしますか?
>> print([1,2,3] *2)
結果はどうしょう。きっと想像していたものではないかと思います。もしリスト内の全ての要素に数字をかけたいのであればPythonではfor文やnumpyを使うことが多いですが,FoxDotには"pattern"と呼ばれるクラスがあります。これを用いることによってより柔軟にリストを操作することができます。使用するにはリストの前に"P"と書きます。
>>> print([1,2,3] *2)
[1, 2, 3, 1, 2, 3]
>>> print(P[1,2,3] *2)
P[2, 4, 6]
これを用いることによって例えば2と3を交互にかけたり、なかなか書きづらいコードも簡単に書くことができます。
>>> print(P[1,2,3]*[2,3])
P[2, 6, 6, 3, 4, 9]

Summary(さいごに)

いかがだったでしょうか。今回の記事では導入からFoxDotでのコーディングの基礎まで説明しました。基礎ではありますが、これらを組み合わせることによって面白いパターンを生み出せます。是非皆さんもFoxDotをはじめAlgoraveを盛り上げていきましょう。Thanks☺

The post Pythonで始めるアルゴレイヴ入門 first appeared on TECHSCORE BLOG.

]]>
当社への就職活動とアルバイトを通じて感じたギャップ https://www.techscore.com/blog/2019/12/21/gap-interview-parttime/ Fri, 20 Dec 2019 23:00:11 +0000 https://www.techscore.com/blog/?p=24376 これは 😺TECHSCORE Advent Calendar 2019😺の21日目の記事です。

はじめまして、内定者で大学生の松田と申します。
続きを読む...

The post 当社への就職活動とアルバイトを通じて感じたギャップ first appeared on TECHSCORE BLOG.

]]>
これは 😺TECHSCORE Advent Calendar 2019😺の21日目の記事です。

はじめまして、内定者で大学生の松田と申します。
テックスコアを運営するシナジーマーケティングに、技術職として2020年入社予定で6月からアルバイトをさせていただいています。
私は以下の2点についてお話をさせていただきます。

  • 面接から内々定、その後のアルバイトについて
  • 内々定をいただくまでの会社イメージと6ヵ月のアルバイトを通して感じたギャップ

少しでも社内の技術職についての様子が伝わればと思います。

面接前から現在までの流れ

エントリーから内定までの3か月の流れ

  • 新卒採用サイトで応募
  • 個人面談
  • 説明会
  • 4回の面接 + 面談
  • 合否判定

内定後の流れ

  • 社内BBQイベントに参加
  • アルバイト開始
  • 1回目の研修
  • 10月 内定式
  • 2回目の研修
  • 12月 現在

説明会

  • 行動基準や企業方針などの説明
  • 人事制度の説明
  • 福利厚生の説明
  • 入社後の流れ
  • 就活の仕方など
  • 会社としての社員への考え

説明会の感想

1つ目に印象に残っていることは産休からの復職率の高さとフレックス制度などの社内制度があることで働きやすそうだということです。実際にアルバイトを始めてから社内を見てみますと7時に出社し、16時に帰宅する方や18時前に帰宅する方が多数見受けられることに驚きました。

2つ目に印象に残っていることは人事制度がしっかりしているということです。階級が細かく設定されており評価者も複数いることで公正な判断を行う制度があると感じました。

面接

1次面接から最終面接までの流れです。(※あくまでも私の流れであって一人ひとり異なります)
※採用詳細はこちら

  • 1次面接はエントリーシートを基にした質問+自己紹介+当社への志望動機
  • 2次面接はエントリーシートを基にした質問+自己紹介+課題
  • 3次面接は仮定の状況を設定しこのような場合どのように行動するか、
    どのように決断をするかなどの適性検査のようなことを行う
  • 面談
  • 最終面接はエントリーシートを基にした質問+自己紹介+自己アピール

面談内容

希望の部署の方にお越しいただいて働き方や仕事内容、所属部署の人数や役割など実際に仕事をしていて感じることや今まで聞けなかった会社で使用している技術的なことを1時間ほど質問する時間をいただきました。

面接の感想

印象に残っていることは面接の回数が人によって変わること、エントリーシートを提出すると1次面接に進むこと、異なる面接官に同じ質問をしても返答が異なることです。面接に関してとてもしっかりと準備をしていただいており、雰囲気や面接官の印象だけで判断するのではなく複数人の意見や会社としての判断基準でしっかりと面接の合否判定をしていただいたと当時でも現在でも感じております。

アルバイト

6月~8月

  • Linux環境を使用するためにVirtualBoxにCentOSのインストールを行いSSH認証
  • バージョン管理をするためにGitの操作課題
  • シェルスクリプトの課題
  • データベースについて知るためにPostgreSQLを使用した課題

9月~12月
Synergy!の機能や使い方を知るために以下の課題を行いました。

  • ブランクメールを使用したメルマガ会員組織の構築
  • アンケート機能を利用したアンケート作成
  • JavaScriptを利用したカスタマイズの作成
    (例えばAの項目を選んだ場合だけBの項目に書き込みができるなど)
  • ファイル投稿機能を作成
  • JavaScriptのコードリーディング

アルバイトの感想

アルバイトを始めた6~8月の課題であるVirtualBox、Gitの操作、データベースについては既に使用したことがあったのでスムーズに進むものだと考えていましたがシナジーマーケティングの一員として行うことは1人で開発を行うことよりも正確性や基準を満たした内容が必要なので、必要な時間も完成物も大きく異なるものでした。

1人で開発環境を用意し、開発を行う場合は何を使用してもよく、完成物の確認をするのもほとんど自身で行っていたので動いたら終わりでしたが、会社として作成したものはどのようなことをしたのか記録していき出来上がったものも自分以外が保守をするのでわかりやすいコードを書く必要があります。何故その書き方をしたのか説明でき、書き直すにしてもどの部分に影響するのかを意識しながらコードを書いたり開発環境を整えたりすることを教わりました。

9月から12月では主要製品のSynergy!について色々な課題や実務などを通して学ばせていただいております。Synergy!やwebに必要な言語を教えていただく過程でマーケティングについても学ばせていただきました。

アルバイトを始める前と始めたあとの当社へのギャップ

始めに私の考えと異なっていたことはアルバイトの時にサポートしてくれる方は1人でテストやインプットすることがほとんどだと思っていたのですがグループ(3人)でサポートしてもらい質問がある時や困った時に助けていただける体制があり、課題についてはアウトプットする機会を多くいただける学びやすい環境を用意してもらえたことです。

社員の方々については新卒採用が多く勤続年数は長い方が多いと思っていましたが、中途採用が多数で私が接した方々の勤続年数は5年以内が多いことです。

社内では部署をまたいで適度にコミュニケーションがあり、席に座ってチャットや電話だけを使用するのではなく、用事がある人の席までいって要件を話している姿も見受けられます。社員の方々は趣味をもっている方が多く部署をまたいで社外で同じ活動をされている方が一定数いることに驚きました。

まとめ

この記事が少しでもこれからシナジーマーケティングへエントリーする方の一助になれば幸いです。

The post 当社への就職活動とアルバイトを通じて感じたギャップ first appeared on TECHSCORE BLOG.

]]>
あえて言うほどではない 数値 ⇔ 文字列変換 2019 年人気プログラミング言語トップ 10 編 https://www.techscore.com/blog/2019/12/20/converting-between-numbers-and-strings-2019/ Thu, 19 Dec 2019 23:00:13 +0000 https://www.techscore.com/blog/?p=25105 はじめに

これは 😺TECHSCORE Advent Calendar 2019😺 の 20 日目の記事です。

こんにちは。桂川です。社会人として働き始めて約 9 カ月が経過しました。
続きを読む...

The post あえて言うほどではない 数値 ⇔ 文字列変換 2019 年人気プログラミング言語トップ 10 編 first appeared on TECHSCORE BLOG.

]]>
はじめに

これは 😺TECHSCORE Advent Calendar 2019😺 の 20 日目の記事です。

こんにちは。桂川です。社会人として働き始めて約 9 カ月が経過しました。これまでに複数のプログラミング言語に触れる機会がありました。未経験のプログラミング言語に触れるとしても、基本的な記述方法は勘でわかることが多いです。しかし、やはりプログラミング言語ごとに相違点があるため、 Web などを駆使して調べつつコーディングをしています。

本ブログでは 2012 年 11 月 28 日に公開された「あえて言うほどではない 数値 ⇔ 文字列変換 Java編」という記事があります。この記事では、コーディングをしているときに頻繁に記述するであろう“文字列 ⇔ 数値”の変換について、Java における記述方法を紹介しています。個人的にも Java で“文字列 ⇔ 数値変換”を実装するとき、何度も見てしまう、お世話になっているページです。(ぜひ、みなさまもご覧ください!)

複数のプログラミング言語に触れる機会があった自分としては、Java だけではなく他のプログラミング言語ではどのように記述できるのか、どのような相違点があるのかを把握したいと思いました。このようなモチベーションで、本記事では複数のプログラミング言語における記述について紹介します。この記事が、さまざまなプログラミング言語に触れる方々にとってのコードスニペットのような役目を果たすことができたなら幸いです。

あえて言うほどではない 数値 ⇔ 文字列変換

プログラミング言語

本記事で対象とするプログラミング言語は、ソフトウェア開発のプラットフォームである GitHub が発表した年次レポート Octoverse より、“2019 年人気プログラミング言語トップ 10”( Octoverse における Top languages )とします[1]

2019 年人気プログラミング言語トップ 10

  1. JavaScript
  2. Python
  3. Java
  4. PHP
  5. C#
  6. C++
  7. TypeScript
  8. Shell
  9. C
  10. Ruby

以下、各プログラミング言語における数値 ⇔ 文字列変換を実現するコードを紹介します。

※今回、扱う数値は整数型に限定します。また、変換方法については抜粋しており、すべてを網羅しているわけではございません。以上について、あらかじめご了承くださいませ。

JavaScript

JavaScript はインタラクティブな Web サイトの構築、ゲームの制作にも使用される主要な言語です。変数の型をコンパイルや実行より前に決めないという、動的型付けの言語です。JavaScript における数値 ⇔ 文字列変換は以下のように記述できます。JavaScript の実行環境である Node.js のバージョンは 12.13.1 を使用しました。

$ node -v
12.13.1

数値 ⇒ 文字列

String オブジェクトを使用して数値を文字列に変換する例。

var i = 2019;
var s = String(i);

Number クラスの toString メソッドを使用して数値を文字列に変換する例。

var i = 2019;
var s = i.toString();

空白の文字列を足して数値を文字列に変換する例。

var i = 2019;
var s = i + '';

${} を使用して数値を文字列に変換する例。

var i = 2019;
var s = `${i}`;

文字列 ⇒ 数値

Number オブジェクトを使用して文字列を数値に変換する例。

var s = '2019';
var i = Number(s);

parseInt オブジェクトを使用して文字列を数値に変換する例。

var s = '2019';
var i = parseInt(s);
// もしくは 
var i = parseInt(s, 10);

四則演算によって文字列を数値に変換する例。

var s = '2019';
var i = s - 0;
// もしくは
var i = s * 1;
var i = s / 1;

Python

Python はさまざまな用途で使用されます。特に近年では人工知能を使用したアプリケーションの開発やデータサイエンスの分野で使用される言語として注目されています。動的型付けの言語です。また、プログラミング初学者にとって学習しやすい言語としても認知されています。Python における数値 ⇔ 文字列変換は以下のように記述できます。使用したバージョンは 3.6.8 です。

$ python --version
3.6.8 (default, Oct 7 2019, 12:59:55) 
[GCC 8.3.0]

数値 ⇒ 文字列

組み込み関数 str を使用して数値を文字列に変換する例。

i = 2019
s = str(i)

文字列 ⇒ 数値

組み込み関数 int を使用して文字列を数値に変換する例。

s = '2019'
i = int(s)

そして、unicodedata モジュールで定義されている関数 unicodedata.numeric() を使用することにより、一、十、百、千といった漢数字 1 つの数値への変換が可能です。これはシブいですね。

import unicodedata

s = '百'
i = unicodedata.numeric(s)

Java

Java はオブジェクト指向に基づいた言語です。モバイルおよび Web アプリケーション、ゲーム、データベース駆動型ソフトウェアの開発などに使用されます。変数の型をコンパイルや実行より前にあらかじめ決めるという、静的型付けの言語です。Java における数値 ⇔ 文字列変換は以下のように記述できます。使用したバージョンは 1.8.0_202 です。

$ java -version
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)

数値 ⇒ 文字列

String クラスの valueOf メソッドを使用して数値を文字列に変換する例。

int i = 2019;
String s = String.valueOf(i);

空の文字列に足すことにより数値を文字列に変換する例。

int i = 2019;
String s = "" + i;

Integer クラスのインスタンスを生成して、toString() メソッドを使用して数値を文字列に変換する例。

int i = 2019;
String s = new Integer(i).toString();

Integer クラスの toString メソッドを使用して数値を文字列に変換する例。

int i = 2019;
String s = Integer.toString(i);

文字列 ⇒ 数値

Integer クラスの parseInt メソッドを使用して文字列を数値に変換する例。

String s = '2019';
int i = Integer.parseInt(s);

Integer クラスのインスタンスを生成して、intValue() メソッドを使用して文字列を数値に変換する例。

String s = '2019';
int i = new Integer(s).intValue();

Integer クラスの valueOf メソッドを使用して文字列を数値に変換する例。

String s = '2019';
int i = Integer.valueOf(s);

PHP

PHP は Web 開発に適しており、HTML に埋め込みが可能な言語です。動的型付けの言語です。PHP における数値 ⇔ 文字列変換は以下のように記述できます。使用したバージョンは 7.2.26 です。

$ php -v
PHP 7.2.26 (cli) (built: Dec 17 2019 14:06:22) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

数値 ⇒ 文字列

strval 関数を使用して数値を文字列に変換する例。

$i = 2019;
$s = strval($i);

sprintf 関数を使用して数値を文字列に変換する例。

$i = 2019;
$s = sprintf("%d",$i);

キャスト演算子を使用して数値を文字列に変換する例。

$i = 2019;
$s = (string) $i;

文字列 ⇒ 数値

intval 関数を使用して文字列を数値に変換する例。

$str = "2019";
$num10 = intval($str);

キャスト演算子を使用して文字列を数値に変換する例。

$str = "2019";
$num = (int)$str;

C#

C# は、C から派生した言語です。 Java と似た言語です。静的型付けの言語です。モバイルアプリ、ゲーム、エンタープライズソフトウェアの開発などに使用されます。C# における数値 ⇔ 文字列変換は以下のように記述できます。使用したコンパイラは mcs で、バージョンは 6.6.0.161 です。

$ mcs --version
Mono C# compiler version 6.6.0.161

数値 ⇒ 文字列

ToString メソッドを使用して数値を文字列に変換する例。

int i = 2019;
string s = i.ToString();

Convert クラスの ToString メソッドを使用して数値を文字列に変換する例。Convertクラスで変換する場合には、nullの変換を試みても例外が発生しません。

int i = 2019;
string s = Convert.ToString(i);

文字列 ⇒ 数値

int クラスの Parse メソッドを使用して文字列を数値に変換する例。

string s = "2019";
int i = int.Parse(s);

Convert クラスを使用して文字列を数値に変換する例。

string s = "2019";
int i = Convert.ToInt32(s);

C++

C++ は C から派生した言語です。オペレーティングシステム、ブラウザ、ゲームなどにおける中核的な言語として、広く使用されています。静的型付けの言語です。C++ における数値 ⇔ 文字列変換は以下のように記述できます。対象としたバージョンは C++11 で、使用したコンパイラは g++ でバージョンが 4.8.5 です。

$ g++ --version
g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39)
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

数値 ⇒ 文字列

to_string 関数を使用して数値を文字列に変換する例。

int i = 2019
std::string s = std::to_string(i);

文字列 ⇒ 数値

stoi 関数を使用して文字列を数値に変換する例。

std::string s = "2019";
int i = std::stoi(s);

TypeScript

TypeScript は、JavaScript において課題として考えられている点を補う目的で開発された言語です。JavaScript が動的型付けの言語であるのに対して、静的型付けの言語であるという特徴が挙げられます。これにより、意図していない型の値を変数に代入できないため、実行前に不具合の検出が可能であるというメリットがあります。TypeScript における数値 ⇔ 文字列変換は JavaScript の記述の使用が可能であるということで割愛させていただきます。

Shell

Shell はオペレーティングシステムの操作などに使用できる言語です。Bashなどで拡張された場合はともかく、Shell の変数にはデータ型が定義されておらず、一律で文字列型です。それゆえ、数値 ⇒ 文字列という処理は存在しません。ただし、四則演算など数値の計算が可能です。使用した Shell は Bash でバージョンは 4.4.20 です。

$ bash --version
GNU bash, バージョン 4.2.46(2)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2011 Free Software Foundation, Inc.
ライセンス GPLv3+: GNU GPL バージョン 3 またはそれ以降 <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

文字列を数値として計算

計算を実施する場合は対象の部分を $() で囲み、数値として展開する変数の直前に expr を使用します。この時、四則演算記号の前後にスペースが必要です。※掛け算や括弧を使用する場合は \* のように、記号の前にバックスラッシュ(Windows では円マーク)を記述してエスケープをする必要があります。

s=2019
s=$(expr $s + 1) # 文字列sは計算時のみ一時的に数値として扱われる

C

C は、今回の対象としている Java、PHP、 JavaScript、C#、C++ といった多くの言語のルーツとされている言語です。静的型付けの言語です。C における数値 ⇔ 文字列変換は以下のように記述できます。対象としたバージョンは C11 で、使用したコンパイラは gccでバージョンが 4.8.5 です。

$ gcc --version
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39)
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

数値 ⇒ 文字列

sprintf 関数を使用して数値を文字列に変換する例。

int i = 2019;
char s[256];
sprintf(s,"%d",i);

文字列 ⇒ 数値

atoi 関数を使用して文字列を数値に変換する例。

char* s = "2019";
int i = atoi(s);

他にも atolatollatof という関数もあり、これらを使用することで long 型、long long 型、float 型への変換が可能です。

Ruby

Ruby はまつもとゆきひろ氏により開発されたオブジェクト指向のスクリプト言語です。特徴として、シンプルさが挙げられます。動的型付けの言語です。Ruby における数値 ⇔ 文字列変換は以下のように記述できます。使用したバージョンは 2.0.0 です。

$ ruby --version
ruby 2.0.0p648 (2015-12-16) [x86_64-linux]

数値 ⇒ 文字列

to_s メソッドを使用して数値を文字列に変換する例。

i = 2019
s = i.to_s

文字列 ⇒ 数値

to_i メソッドを使用して文字列を数値に変換する例。

s = "2019"
i = s.to_i

可読性が高いですね。

おわりに

“2019 年人気プログラミング言語トップ 10”を対象に、数値 ⇔ 文字列変換を実現するコードを紹介しました。類似している記述方法もあれば、そうでない記述方法もありましたね。これでどんなプログラミング言語を使うとしても、“数値 ⇔ 文字列変換”はバッチリです…

参照

    1. The State of the Octoverse | The State of the Octoverse celebrates a year of building across teams, time zones, and millions of merged pull requests.(2019年12月16日にアクセス)

The post あえて言うほどではない 数値 ⇔ 文字列変換 2019 年人気プログラミング言語トップ 10 編 first appeared on TECHSCORE BLOG.

]]>
Ruby on Rails 6.0のAction Textを触ってみた。 https://www.techscore.com/blog/2019/12/19/ruby-on-rails-6-action-text/ Wed, 18 Dec 2019 23:00:37 +0000 https://www.techscore.com/blog/?p=23804 これは 😺TECHSCORE Advent Calendar 2019😺の19日目の記事です。

今年の8月にRuby on Rails(以下Rails)の最新バージョンとなる 6.0 がリリースされました。
続きを読む...

The post Ruby on Rails 6.0のAction Textを触ってみた。 first appeared on TECHSCORE BLOG.

]]>
これは 😺TECHSCORE Advent Calendar 2019😺の19日目の記事です。

今年の8月にRuby on Rails(以下Rails)の最新バージョンとなる 6.0 がリリースされました。
RailsはTECHSCOREブログで何度か取り上げているフレームワークです。

いくつか新機能が追加されていますが、
その中で Action Text という機能が面白そうだったため触ってみました。

【実行環境】

OS    :CentOS 7.6 … Windows10上にOracle VM VirtualBoxを導入して実行
Ruby  :2.6.3
Rails :6.0.1

 

Action Text とは

Action Text はリッチテキストを簡単に作成、編集、表示することができるフレームワークを提供します。

(公式ブログの内容を意訳)
https://weblog.rubyonrails.org/2018/10/3/introducing-action-text-for-rails-6/

すなわち「HTMLやCSSの知識がなくても文章の修飾や画像の挿入などができるエディタ」
が提供されるということになります。

リッチテキストエディタ自体は、もともとgem(Rubyのライブラリ)で提供されていたようですが、
今回のバージョンアップで本体に組み込まれるようになりました。
 

Action Text を使ってみる。

それでは、実際にAction Text を使ってみます。

Railsのインストール方法などは、公式ガイドにお任せしますので、
rails new(Railsアプリのひな型を作るコマンド)で生成されたアプリをもとに
プログラムを実装していきます。

# CRUD機能追加(playground はモデル名です)
rails g scaffold playground

# action_textをインストール
rails action_text:install

 

画像を扱うために ImageMagick をOS側にインストールします。
参考:https://imagemagick.org/script/download.php

# libをインストール
yum install https://imagemagick.org/download/linux/CentOS/x86_64/ImageMagick-libs-7.0.9-8.x86_64.rpm

# 本体をインストール
yum install https://imagemagick.org/download/linux/CentOS/x86_64/ImageMagick-7.0.9-8.x86_64.rpm

 

次にRailsでImageMagickを利用するための設定を行います。

Gemfile

# 以下の行がコメントアウトされているので外して保存する。
gem 'image_processing', '~> 1.2'

上記保存後、bundle installをします。

bundle install

 

データベースのマイグレーションをします。

rake db:migrate

 

基本的な設定はできたので、モデルやコントローラ、ビューに設定を書き加えていきます。

app/models/playground.rb

class Playground < ApplicationRecord
    has_rich_text :content
end

app/controllers/playgrounds_controller.rb

def playground_params
  params.require(:playground).permit(:content)
end

app/views/playgrounds/_form.html.erb

# 下記のコードがリッチテキストエリアになりますので、適切な場所に追加します。
<div class="field">
  <%= form.label :content %>
  <%= form.rich_text_area :content %>
</div>

app/views/playgrounds/show.html.erb

# 下記のコードがcontent(=リッチテキスト)を表示する箇所になります。
<%= @playground.content %>

 
 
ここまで実施すれば基本的な設定は完了です。
rails s などのコマンドでサーバを立ち上げて確かめてみましょう。


action_textの検証
※ 画像をクリックすると動きます。


このような感じで、複雑な設定を行わずにリッチテキストを扱うことができました。
 

エディタをちょっといじってみる。

Action Text で提供されるリッチテキストエディタは Trix と呼ばれるものが利用されています。
公式によると見た目をカスタマイズできるらしいので試してみます。



 
action_textをインストールした際 app/assets/stylesheets フォルダ以下に
actiontext.scss ファイルが生成されていますので、こちらを編集していきます。

trixのCSSをもとに作成しています。

app/assets/stylesheets/actiontext.scss

trix-editor {
  border: 5px outset #bbb;
}

trix-toolbar .trix-button--icon-bold::before {
  background-image: url(bold.png); 
}

trix-toolbar .trix-button--icon-italic::before {
  background-image: url(italic.png); 
}

trix-toolbar .trix-button--icon-strike::before {
  background-image: url(strike.png); 
}

trix-toolbar .trix-button--icon-link::before {
  background-image: url(link.png); 
}

※ボタン画像はapp/assets/images 以下に保存しています。
 
試してみた結果がこちらです。
今回は入力欄の枠とボタンの画像を変更してみました。



 

まとめ

今回はRails 6.0 の新機能である Action Text の基本的なところについて触れました。
公式から提供されているだけあって簡単に導入でき、様々な応用が利きそうな機能だと感じました。

ただし今回触れたものはあくまでも基本だけですので、
実務でAction Text を使うためには、さらに理解を深める必要がありそうです。

The post Ruby on Rails 6.0のAction Textを触ってみた。 first appeared on TECHSCORE BLOG.

]]>
AWS CDKを導入して脱YAMLテンプレートを試みる https://www.techscore.com/blog/2019/12/18/aws-cdk/ Tue, 17 Dec 2019 23:00:12 +0000 https://www.techscore.com/blog/?p=24643 これは 😺TECHSCORE Advent Calendar 2019😺の18日目の記事です。

今までCloudFormationテンプレートを直接YAMLで書いていましたが、作成するリソース数が多くなるにつれて記述量の多さに辛さを感じるようになってきました。
続きを読む...

The post AWS CDKを導入して脱YAMLテンプレートを試みる first appeared on TECHSCORE BLOG.

]]>
これは 😺TECHSCORE Advent Calendar 2019😺の18日目の記事です。

今までCloudFormationテンプレートを直接YAMLで書いていましたが、作成するリソース数が多くなるにつれて記述量の多さに辛さを感じるようになってきました。
そんな折AWS CDKのことを知り、試しに簡単な構成をAWS CDKで作成してみることにしました。

利用するAWS CDKのAPIについて

Developer GuideAPI Referenceを見ると、AWS CDKのAPIにはCloudFormationの各リソースタイプ(VPC等)と1対1で対応している低レベルなものと、より高レベルなもの(例えば、VPCやサブネットを作成する際にインターネットゲートウェイやルートテーブル、ルートといった関連リソースを自動的に作成・関連付けしてくれる)があるようですが、利用するAZやサブネットの個数を柔軟にコントロールしたかったので、今回は低レベルAPIの方を試してみたいと思います。

必要なソフトウェアをインストール

Getting Started With the AWS CDKに従って、AWS CDKを実行する環境を準備します。
AWS CDKの言語には、Pythonを選択します。

curl -sL https://rpm.nodesource.com/setup_10.x | sudo bash -
sudo yum install python3
sudo yum install nodejs
sudo npm install -g aws-cdk

今回確認に利用した環境は、以下の通りです。

OS: Amazon Linux release 2 (Karoo)
node: v10.17.0
npm: 6.11.3
Python: 3.7.4
pip: 19.0.3
cdk: 1.18.0

AWSのクレデンシャルとアクセス権限を設定

AWSのクレデンシャルを環境変数に設定しておきます。

export AWS_ACCESS_KEY_ID=Specifies your access key.
export AWS_SECRET_ACCESS_KEY=Specifies your secret access key.
export AWS_DEFAULT_REGION=ap-northeast-1

IAMのアクセス権限には、以下のAWS管理ポリシーをアタッチしておきます。

AWSCloudFormationFullAccess
AmazonEC2FullAccess
AmazonS3FullAccess

作成するリソースの構成

パブリックサブネット(/20)とプライベートサブネット(/20)をAZごとに1つずつ持つVPC(10.0.0.0/16)を作成します。
パブリックサブネットにはNAT Gatewayを配置します。

AWS CDKアプリの雛形を作成

Getting Started With the AWS CDKにあるようにアプリの雛形を作成し、AWS CDKのモジュールをインストールします。
今回はVPCとサブネットを作成しますので、コアモジュールとEC2モジュールをインストールします。
その他のモジュールについてはAWS CDK Python Referenceを参照してください。

mkdir my-network
cd my-network
cdk init --language python
source .env/bin/activate
pip install -r requirements.txt
pip install --upgrade aws-cdk.core
pip install --upgrade aws-cdk.aws_ec2

作成するAWSリソースのコードを追加

雛形を作成した時点では、以下のような構成となっています。

└── my-network
    ├── README.md
    ├── app.py
    ├── cdk.json
    ├── my_network
    │   ├── __init__.py
    │   ├── my_network.egg-info
    │   │   ├── PKG-INFO
    │   │   ├── SOURCES.txt
    │   │   ├── dependency_links.txt
    │   │   ├── requires.txt
    │   │   └── top_level.txt
    │   └── my_network_stack.py
    ├── requirements.txt
    └── setup.py

my_network_stack.py
を開くと、以下のようなコードが記述されています。

from aws_cdk import core


class MyNetworkStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # The code that defines your stack goes here

コメントにあるように、この中に作成するリソースのコードを記述していきます。
作成するリソースの数が多いので、my_resourcesディレクトリを作成してそこに追加したモジュールを呼び出すようにします。

モジュール追加後の構成は以下の通りです。

└── my-network
    ├── README.md
    ├── app.py
    ├── cdk.json
    ├── my_network
    │   ├── __init__.py
    │   ├── my_network.egg-info
    │   │   ├── PKG-INFO
    │   │   ├── SOURCES.txt
    │   │   ├── dependency_links.txt
    │   │   ├── requires.txt
    │   │   └── top_level.txt
    │   └── my_network_stack.py
    │   └── my_resources
    │       ├── __init__.py
    │       ├── availability_zone.py
    │       ├── nat_gateway.py
    │       ├── route.py
    │       ├── subnet.py
    │       └── vpc.py
    ├── requirements.txt
    └── setup.py

追加したモジュールの内容は以下の通りです。

availability_zone.py(アベイラビリティーゾーンの定義)※東京リージョンに限定

class AvailabilityZone:
    def __init__(self, region='ap-northeast-1') -> None:
        self.__region = region

        if self.__region == 'ap-northeast-1':
            self.__names = ['ap-northeast-1a', 'ap-northeast-1c', 'ap-northeast-1d']
        else:
            self.__names = []

    @property
    def names(self) -> list:
        return self.__names

    def name(self, az_number) -> str:
        return self.__names[az_number]

nat_gateway.py(NAT Gateway)

import hashlib
from aws_cdk import (
    core,
    aws_ec2,
)


def create_nat_gateway(scope: core.Construct, vpc: aws_ec2.CfnVPC, subnet: aws_ec2.CfnSubnet) -> aws_ec2.CfnNatGateway:
    vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop()
    subnet_id = [tag['value'] for tag in subnet.tags.render_tags() if tag['key'] == 'Name'].pop()
    id = hashlib.md5(subnet_id.encode()).hexdigest()
    eip = aws_ec2.CfnEIP(scope, f'{vpc_id}/EIP-{id}')
    
    nat_gateway = aws_ec2.CfnNatGateway(scope, f'{vpc_id}/NatGateway-{id}',
        allocation_id=eip.attr_allocation_id,
        subnet_id=subnet.ref,
        tags=[core.CfnTag(
            key='Name',
            value=f'{vpc_id}/NatGateway-{id}',
        )],
    )
    
    return nat_gateway

route.py(ルートテーブル、ルート)

import hashlib
from aws_cdk import (
    core,
    aws_ec2,
)


def create_privagte_route_table(scope: core.Construct, vpc: aws_ec2.CfnVPC, nat_gateway: aws_ec2.CfnNatGateway) -> aws_ec2.CfnRouteTable:
    vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop()
    ngw_id = [tag['value'] for tag in nat_gateway.tags.render_tags() if tag['key'] == 'Name'].pop()
    id = hashlib.md5(ngw_id.encode()).hexdigest()
    route_table = aws_ec2.CfnRouteTable(scope, f'{vpc_id}/RouteTable-{id}',
        vpc_id=vpc.ref,
        tags=[core.CfnTag(
            key='Name',
            value=f'{vpc_id}/RouteTable-{id}',
        )],
    )
    
    aws_ec2.CfnRoute(scope, f'{vpc_id}/Route-{id}',
        route_table_id=route_table.ref,
        destination_cidr_block='0.0.0.0/0',
        nat_gateway_id=nat_gateway.ref,
    )
    
    return route_table


def create_public_route_table(scope: core.Construct, vpc: aws_ec2.CfnVPC, internet_gateway: aws_ec2.CfnInternetGateway) -> aws_ec2.CfnRouteTable:
    vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop()
    igw_id = [tag['value'] for tag in internet_gateway.tags.render_tags() if tag['key'] == 'Name'].pop()
    id = hashlib.md5(igw_id.encode()).hexdigest()
    route_table = aws_ec2.CfnRouteTable(scope, f'{vpc_id}/RouteTable-{id}',
        vpc_id=vpc.ref,
        tags=[core.CfnTag(
            key='Name',
            value=f'{vpc_id}/RouteTable-{id}',
        )],
    )
    
    aws_ec2.CfnRoute(scope, f'{id}/Route-{id}',
        route_table_id=route_table.ref,
        destination_cidr_block='0.0.0.0/0',
        gateway_id=internet_gateway.ref,
    )
    
    return route_table


def create_route_table_association(scope: core.Construct, vpc: aws_ec2.CfnVPC, subnet: aws_ec2.CfnSubnet, route_table: aws_ec2.CfnRouteTable) -> aws_ec2.CfnSubnetRouteTableAssociation:
    vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop()
    subnet_id = [tag['value'] for tag in subnet.tags.render_tags() if tag['key'] == 'Name'].pop()
    id = hashlib.md5(subnet_id.encode()).hexdigest()
    association = aws_ec2.CfnSubnetRouteTableAssociation(scope, f'{vpc_id}/SubnetRouteTableAssociation-{id}',
        route_table_id=route_table.ref,
        subnet_id=subnet.ref,
    )

    return association

subnet.py(サブネット)

import hashlib
import ipaddress
import uuid
from aws_cdk import (
    core,
    aws_ec2,
)
from my_resources import (
    availability_zone,
    vpc,
)


class SubnetGroup:
    def __init__(self, scope: core.Construct, vpc: aws_ec2.CfnVPC, *,
            desired_layers: int=2,
            desired_azs: int=2,
            region: str='ap-northeast-1',
            private_enabled: bool=True,
            cidr_mask: int=20) -> None:
        self._cidr_mask = cidr_mask
        self._desired_azs = desired_azs
        self._desired_layers = desired_layers
        self._private_enabled = private_enabled
        self._region = region
        self._reserved_azs = 5
        self._reserved_layers = 3
        self._scope = scope
        self._vpc = vpc
        
        self._desired_subnet_points = []
        for layer_number in range(self._desired_layers):
            for az_number in range(self._desired_azs):
                self._desired_subnet_points.append([layer_number, az_number])
                
        self._public_subnets = []
        self._private_subnets = []
        
    @property
    def cidr_mask(self) -> int:
        return self._cidr_mask
        
    @property
    def desired_azs(self) -> int:
        return self._desired_azs
        
    @property
    def desired_layers(self) -> int:
        return self._desired_layers
        
    @property
    def desired_subnet_points(self) -> list:
        return self._desired_subnet_points
        
    @property
    def private_enabled(self) -> bool:
        return self._private_enabled
        
    @property
    def private_subnets(self) -> list:
        return self._private_subnets
        
    @property
    def public_subnets(self) -> list:
        return self._public_subnets
        
    @property
    def region(self) -> str:
        return self._region
        
    @property
    def reserved_azs(self) -> int:
        return self._reserved_azs
        
    @property
    def reserved_layers(self) -> int:
        return self._reserved_layers

    @property
    def scope(self) -> core.Construct:
        return self._scope
    
    @property
    def vpc(self) -> aws_ec2.CfnVPC:
        return self._vpc

    def create_subnets(self) -> None:
        nw = ipaddress.ip_network(self.vpc.cidr_block)
        cidrs = list(nw.subnets(new_prefix=self.cidr_mask))
        cidrs.reverse()
        az = availability_zone.AvailabilityZone()
        vpc_id = [tag['value'] for tag in self.vpc.tags.render_tags() if tag['key'] == 'Name'].pop()
        
        for layer in range(self.reserved_layers):
            for az_number in range(self.reserved_azs):
                current = [layer, az_number]
                cidr = str(cidrs.pop())
                if current in self.desired_subnet_points:
                    id = hashlib.md5(f'{layer}-{az_number}'.encode()).hexdigest()
                    subnet = aws_ec2.CfnSubnet(self.scope, f'{vpc_id}/Subnet-{id}',
                        cidr_block=cidr,
                        vpc_id=self.vpc.ref,
                        availability_zone=az.name(az_number),
                        tags=[
                            core.CfnTag(
                                key='Name',
                                value=f'{vpc_id}/Subnet-{id}',
                            ),
                            core.CfnTag(
                                key='Layer',
                                value=f'{layer}',
                            ),
                            core.CfnTag(
                                key='AZNumber',
                                value=f'{az_number}',
                            ),
                        ],
                    )
                    if self.private_enabled and layer > 0:
                        self._private_subnets.append(subnet)
                    else:
                        self._public_subnets.append(subnet)

vpc.py(VPC、インターネットゲートウェイ)

from aws_cdk import (
    core,
    aws_ec2,
)


def create_vpc(scope: core.Construct, id: str, *,
        cidr='10.0.0.0/16',
        enable_dns_hostnames=True,
        enable_dns_support=True) -> aws_ec2.CfnVPC:
    vpc = aws_ec2.CfnVPC(scope, id,
        cidr_block=cidr,
        enable_dns_hostnames=enable_dns_hostnames,
        enable_dns_support=enable_dns_support,
        tags=[core.CfnTag(
            key='Name',
            value=id,
        )]
    )

    return vpc


def create_internet_gateway(scope: core.Construct, vpc: aws_ec2.CfnVPC) -> aws_ec2.CfnInternetGateway:
    vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop()
    
    internet_gateway = aws_ec2.CfnInternetGateway(scope, f'{vpc_id}/InternetGateway',
        tags=[core.CfnTag(
            key='Name',
            value=f'{vpc_id}/InternetGateway',
        )]
    )
    
    aws_ec2.CfnVPCGatewayAttachment(scope, f'{vpc_id}/VPCGatewayAttachment',
        vpc_id=vpc.ref,
        internet_gateway_id=internet_gateway.ref,
    )
    
    return internet_gateway

network_stack.py
を以下のように編集して、追加したモジュールを呼び出します。

from aws_cdk import core
from my_resources import (
    vpc,
    subnet,
    nat_gateway,
    route,
)


class MyNetworkStack(core.Stack):
    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # The code that defines your stack goes here
        vpc_id = 'MyVPC'
        subnet_desired_layers = 2
        subnet_desired_azs = 2
        private_subnet_enabled=True
        
        v = vpc.create_vpc(self, vpc_id)
        igw = vpc.create_internet_gateway(self, v)
        subnet_group = subnet.SubnetGroup(self, v,
            desired_layers=subnet_desired_layers,
            desired_azs=subnet_desired_azs,
            private_enabled=private_subnet_enabled)
        subnet_group.create_subnets()
        
        if subnet_group.public_subnets:
            public_route_table = route.create_public_route_table(self, v, igw)
            for public_subnet in subnet_group.public_subnets:
                route.create_route_table_association(self, v, public_subnet, public_route_table)
        
        if subnet_group.private_subnets:
            private_route_tables = []
            for public_subnet in subnet_group.public_subnets:
                ngw = nat_gateway.create_nat_gateway(self, v, public_subnet)
                private_route_table = route.create_privagte_route_table(self, v, ngw)
                private_route_tables.append(private_route_table)
                
            for private_subnet in subnet_group.private_subnets:
                az_number = [tag['value'] for tag in private_subnet.tags.render_tags() if tag['key'] == 'AZNumber'].pop()
                route.create_route_table_association(self, v, private_subnet, private_route_tables[int(az_number)])

AWS CDKの実行

準備ができましたので、アプリのルートディレクトリ(my-network)でcdkを実行してみます。
まずは、
cdk diff
を実行して作成・変更されるリソースの差分を確認してみます。

cdk diff

Stack my-network
Conditions
[+] Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}]}

Resources
[+] AWS::EC2::VPC MyVPC MyVPC
[+] AWS::EC2::InternetGateway MyVPC--InternetGateway MyVPCInternetGateway
[+] AWS::EC2::VPCGatewayAttachment MyVPC--VPCGatewayAttachment MyVPCVPCGatewayAttachment
[+] AWS::EC2::Subnet MyVPC--Subnet-c7763203e20a64b270352752d6a1e7c6 MyVPCSubnetc7763203e20a64b270352752d6a1e7c6
[+] AWS::EC2::Subnet MyVPC--Subnet-c2eb282156233b5d827219971c8b04c2 MyVPCSubnetc2eb282156233b5d827219971c8b04c2
[+] AWS::EC2::Subnet MyVPC--Subnet-eca26941bc5187d1e2983961edb6dbb6 MyVPCSubneteca26941bc5187d1e2983961edb6dbb6
[+] AWS::EC2::Subnet MyVPC--Subnet-ea66c06c1e1c05fa9f1aa39d98dc5bc1 MyVPCSubnetea66c06c1e1c05fa9f1aa39d98dc5bc1
[+] AWS::EC2::RouteTable MyVPC--RouteTable-e7636240538bdd71bd55872aed605e26 MyVPCRouteTablee7636240538bdd71bd55872aed605e26
[+] AWS::EC2::Route e7636240538bdd71bd55872aed605e26--Route-e7636240538bdd71bd55872aed605e26 e7636240538bdd71bd55872aed605e26Routee7636240538bdd71bd55872aed605e26
[+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-8a80275c4aeaac9a8e6f6f36e18f5f5b MyVPCSubnetRouteTableAssociation8a80275c4aeaac9a8e6f6f36e18f5f5b
[+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-7c555b7a2a9e217d5de327ca36a79a54 MyVPCSubnetRouteTableAssociation7c555b7a2a9e217d5de327ca36a79a54
[+] AWS::EC2::EIP MyVPC--EIP-8a80275c4aeaac9a8e6f6f36e18f5f5b MyVPCEIP8a80275c4aeaac9a8e6f6f36e18f5f5b
[+] AWS::EC2::NatGateway MyVPC--NatGateway-8a80275c4aeaac9a8e6f6f36e18f5f5b MyVPCNatGateway8a80275c4aeaac9a8e6f6f36e18f5f5b
[+] AWS::EC2::RouteTable MyVPC--RouteTable-f178a135091a35c9541fbf72fb1c8dc1 MyVPCRouteTablef178a135091a35c9541fbf72fb1c8dc1
[+] AWS::EC2::Route MyVPC--Route-f178a135091a35c9541fbf72fb1c8dc1 MyVPCRoutef178a135091a35c9541fbf72fb1c8dc1
[+] AWS::EC2::EIP MyVPC--EIP-7c555b7a2a9e217d5de327ca36a79a54 MyVPCEIP7c555b7a2a9e217d5de327ca36a79a54
[+] AWS::EC2::NatGateway MyVPC--NatGateway-7c555b7a2a9e217d5de327ca36a79a54 MyVPCNatGateway7c555b7a2a9e217d5de327ca36a79a54
[+] AWS::EC2::RouteTable MyVPC--RouteTable-765dc824da4c4f28ce886a61c6b54742 MyVPCRouteTable765dc824da4c4f28ce886a61c6b54742
[+] AWS::EC2::Route MyVPC--Route-765dc824da4c4f28ce886a61c6b54742 MyVPCRoute765dc824da4c4f28ce886a61c6b54742
[+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-ad32a7208c1d822e4f35b34060485714 MyVPCSubnetRouteTableAssociationad32a7208c1d822e4f35b34060485714
[+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-c73221f09ade1614a962f2c9c60cd682 MyVPCSubnetRouteTableAssociationc73221f09ade1614a962f2c9c60cd682

定義したリソースが追加の対象となっていることが確認できましたので、
cdk deploy
を実行して実際にリソースを作成します。

cdk deploy

my-network: deploying...
my-network: creating CloudFormation changeset...
...
 ✅  my-network

cdk deploy
の実行完了後、AWSマネジメントコンソールでCloudFormationスタックを見ると、my-networkスタックでリソースが作成されていることが確認できました。

最後に

今回はAWS CDKの低レベルAPIを試してみましたが、プログラミング言語のループ構造を利用して同じリソースを複数作成できることだけでも、YAMLで直接記述するのと比べてコード量を大幅に減らすことができ、非常に魅力的なものだと感じました。
CloudFormationでリソースを作成したことがあればAPIは直感的に利用できるものとなっており、導入までの敷居は比較的低いのではないかと思います。
今後は本番環境での利用も想定して使っていきたいと思います。

The post AWS CDKを導入して脱YAMLテンプレートを試みる first appeared on TECHSCORE BLOG.

]]>
OpenAPI Generator で OAuth2 アクセストークン発行のコードまで生成してみる https://www.techscore.com/blog/2019/12/17/openapi-generator-oauth2-accesstoken/ Mon, 16 Dec 2019 23:00:26 +0000 https://www.techscore.com/blog/?p=25128 これは TECHSCORE Advent Calendar 2019 の17日目の記事です。
続きを読む...

The post OpenAPI Generator で OAuth2 アクセストークン発行のコードまで生成してみる first appeared on TECHSCORE BLOG.

]]>
これは TECHSCORE Advent Calendar 2019 の17日目の記事です。

OpenAPI Generator のコード生成について

OpenAPI GeneratorOpenAPI Specification の定義ファイルがあれば、API クライアントやサーバのスタブのコードを自動生成してくれるという便利な代物です。

ただ、生成されたコードがそのまま使えるとは言えません。例えば、OAuth2 で保護されている API にアクセスする場合、アクセストークンの発行が必要ですが、OpenAPI Generator のクライアントコード生成ではアクセストークン発行のコードは生成してくれません。
Java のクライアントコードを生成した場合、README に以下のようなサンプルコードが出力されますが、 setAccessToken してね、ということくらいしか書かれておらず、アクセストークンの発行の部分には触れられていません。
(Java のクライアントコード生成では、デフォルトで OkHttp 3 を利用したコードが生成されます。OkHttp 3 のクライアントコードの場合、一応アクセストークン発行のコードは生成されますが、Apache Oltu という既にプロジェクト終了したライブラリを使ったコードなので、あまり実案件では使いたくありません)

public static void main(String[] args) {
        ApiClient defaultClient = Configuration.getDefaultApiClient();
        defaultClient.setBasePath("https://mail.paas.crmstyle.com/e");
        
        // Configure OAuth2 access token for authorization: mail_oauth
        OAuth mail_oauth = (OAuth) defaultClient.getAuthentication("mail_oauth");
        mail_oauth.setAccessToken("YOUR ACCESS TOKEN");

        DefaultApi apiInstance = new DefaultApi(defaultClient);
        String user = "user_example"; // String | user name
        DeliverySettingRequest deliverySettingRequest = new DeliverySettingRequest(); // DeliverySettingRequest | 
        try {
            DeliveryResponse result = apiInstance.createMailDeliverySetting(user, deliverySettingRequest);
            System.out.println(result);
        } catch (ApiException e) {
            System.err.println("Exception when calling DefaultApi#createMailDeliverySetting");
            System.err.println("Status code: " + e.getCode());
            System.err.println("Reason: " + e.getResponseBody());
            System.err.println("Response headers: " + e.getResponseHeaders());
            e.printStackTrace();
        }
    }

また、Spring WebFlux をライブラリとしてクライアントコード生成も可能ですが、これに関してはアクセストークン発行はおろか、そのままビルドも通らないという状況です。

今回は、 OpenAPI Generator で Spring WebFlux のクライアントコードを OAuth2 のアクセストークン発行付きで生成するところまでを試してみました。
実行環境は以下の通りです。OpenAPI Generator のバージョンは、現時点の最新安定版の 4.2.2 を使いました。

$ java -version
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

サンプルコードはこちらにあります。
OpenAPI の定義ファイルは、Synergy! メールAPI をベースにしたものを使っています。

Groovy を使う

OpenAPI Generator が生成するコードを変更したい場合、普通なら Github リポジトリ からソースコードを落としてきて、Java のコードを修正して Maven でビルドして..という感じになると思いますが、 Clone する手間や Maven をインストールしたりちょっと面倒です。

Groovy を使うことで、必要なクラスは @Grab を指定することで実行時に解決でき、Maven は不要になるので、Groovy を使います。私は普段 Mac で開発しているため、Homebrew で Groovy をインストールしました。

手始めとして、JavaClientCodegen を継承したクラスを実行する Groovy スクリプトを作ります。

@Grab(group = 'org.openapitools', module = 'openapi-generator-cli', version = '4.2.2')
import org.openapitools.codegen.*
import org.openapitools.codegen.languages.*

class TechscoreJavaClientCodegen extends JavaClientCodegen {

  static main(String[] args) {
    OpenAPIGenerator.main(args)
  }

  TechscoreJavaClientCodegen() {
    super()
  }

  String name = "techscore-codegen"
}

上記をファイル名 techscore-client-codegen.groovy として保存後、以下のコマンド実行で、output フォルダに Spring WebFlux のクライアントコードが生成されます。

groovy \
  ./techscore-client-codegen.groovy \
  generate \
  -i ./openapi.yml \
  -g TechscoreJavaClientCodegen \
  -o ./output \
  --library webclient

テンプレートのカスタマイズ

上記で生成されるクライアントコードは、前述したとおりそのままではビルドが通らない、アクセストークン発行のコードが生成されないなどといった状態です。

生成されるファイルを修正・追加する

ビルドが通らないのは、生成される build.gradle に Spring 関係の依存関係について一切記載が無いことが原因です。
これを解消するために、OpenAPI Generator はテンプレートからコード生成するため、OpenAPI Generator のテンプレートをカスタマイズする必要があります。

Java のクライアントコードのテンプレートは、こちら から取得できます。
これをコピーして、ファイルを修正・追加することで生成されるコードを変更することができます。
以下のようにして、template ディレクトリ配下にコピーします。

mkdir -p {repodir,template} && cd repodir
git init
git remote add origin https://github.com/OpenAPITools/openapi-generator.git
git config core.sparsecheckout true
vi .git/info/sparse-checkout ← modules/openapi-generator/src/main/resources/Java/ を追記 
git fetch origin
git checkout v4.2.2
git pull origin v4.2.2
cp -pR modules/openapi-generator/src/main/resources/Java/* ../template/
cd ../ && rm -Rf repodir

template ディレクトリの中は以下のような構成になっています。拡張子 .mustcache は OpenAPI Generator が利用しているテンプレートエンジン Mustache のテンプレートファイルです。

$ tree -L 3 template
template
├── auth
│   ├── ApiKeyAuth.mustache
│   ├── Authentication.mustache
│   ├── HttpBasicAuth.mustache
│   ├── HttpBearerAuth.mustache
│   ├── OAuth.mustache
│   └── OAuthFlow.mustache
├── libraries
│   ├── feign
│   (略)
│   └── webclient
│       ├── auth
│       ├── ApiClient.mustache
│       ├── README.mustache
│       ├── api.mustache
│       ├── api_test.mustache
│       └── pom.mustache
├── ApiClient.mustache
(略)
├── build.gradle.mustache
├── build.sbt.mustache
(略)
└── xmlAnnotation.mustache

クライアントコード生成時、--library webclient (Spring WebFluxのコード生成)を指定すると、 libraries/webclient のテンプレートが使われることになります。
今回の場合、 libraries/webclient/build.gradle.mustache を作成すれば、build.gradle を変更できます。
build.gradle.mustache の内容はこちらを参照ください。
以下のように -t オプションを追加してテンプレート指定することでテンプレートの差し替えが可能です。

groovy \
  ./techscore-client-codegen.groovy \
  generate \
  -i ./openapi.yml \
  -g TechscoreJavaClientCodegen \
  -o ./output \
  -t ./template \
  --library webclient

アクセストークン発行のコードを生成するためには、新しくテンプレートファイルを追加する必要があります。
今回は、Spring WebFlux のクライアントコードのため、WebClient を DI できるように、テンプレートを作成し libraries/webclient/WebClientConfig.mustache に登録しました。
Synergy! メールAPI ではアクセストークン発行時に scope と audience をリクエストボディに含める必要があるため、そのような実装をしています。また、アクセストークンはある程度キャッシュを利用するようにしています。実装内容はリンク先を参照いただければと思います。

また、ApiClient.mustache, api.mustache も、上記 WebClient を DI できるように少しずつ内容を変更しています。こちらも内容はリンク先を参照ください。

ただし、新しいテンプレートを追加しても、それだけではテンプレートを読み取ってくれません。Groovy に以下の Override メソッドを追加して、テンプレートを読み取るようにします。

@Override
  public void processOpts() {
    final String invokerFolder = (sourceFolder + '/' + invokerPackage).replace(".", "/");
    final String apiFolder = (sourceFolder + '/' + apiPackage).replace(".", "/");
    super.processOpts()

    if (WEBCLIENT.equals(getLibrary())) {
      // add WebClientConfig
      supportingFiles.add(new SupportingFile("WebClientConfig.mustache", invokerFolder, "WebClientConfig.java"))
    }
  }

Spring Boot の設定ファイル application.yml を追加する

先ほど触れませんでしたが、上記の WebClientConfig.mustache を元に生成される WebClientConfig.java は、Spring Boot の定義ファイルである application.yml から OAuth2 関連の設定内容を読み取ることを前提としています。(この辺りや、この辺りです。)

application.yml もテンプレートファイルが存在しないため、先ほど同様テンプレートファイルを作成して、それを読み取るコードを Groovy スクリプトに追記する必要があります。

{{#authMethods}}{{#isOAuth}}
spring:
  security.oauth2.client:
    provider:
      paas:
        token-uri: https://auth.paas.crmstyle.com/oauth2/token
    registration:
      paas:
        authorization-grant-type: client_credentials
        client-id: your-client-id
        client-secret: your-client-secret
        scope:
          - mail:send
          - mail:result
        audience: https://mail.paas.crmstyle.com{{/isOAuth}}{{/authMethods}}
logging:
  level:
    reactor.netty: DEBUG

@Override
  public void processOpts() {
    final String invokerFolder = (sourceFolder + '/' + invokerPackage).replace(".", "/");
    final String apiFolder = (sourceFolder + '/' + apiPackage).replace(".", "/");
    super.processOpts()

    if (WEBCLIENT.equals(getLibrary())) {
      // add WebClientConfig
      supportingFiles.add(new SupportingFile("WebClientConfig.mustache", invokerFolder, "WebClientConfig.java"))
      // add application.yml ★追記
      supportingFiles.add(new SupportingFile("application.yml.mustache", projectFolder + '/resources', "application.yml"))
    }
  }

SecuritySchemes を読み取って置換する

先ほどの application.yml のテンプレートは、アクセストークン発行 URL やスコープ、audience を固定で記載していました。(以下の★マークをつけている部分です。)

{{#authMethods}}{{#isOAuth}}
spring:
  security.oauth2.client:
    provider:
      paas:
        token-uri: https://auth.paas.crmstyle.com/oauth2/token ★ここ
    registration:
      paas:
        authorization-grant-type: client_credentials
        client-id: your-client-id
        client-secret: your-client-secret
        scope: ★ここ
          - mail:send
          - mail:result
        audience: https://mail.paas.crmstyle.com ★ここ{{/isOAuth}}{{/authMethods}}
logging:
  level:
    reactor.netty: DEBUG

どうせなら、OpenAPI の定義ファイルの SecuritySchemes から読み取った内容で置換したいです。

securitySchemes:
    mail_oauth:
      type: oauth2
      description: mail auth
      flows:
        clientCredentials:
          tokenUrl: "https://auth.paas.crmstyle.com/oauth2/token" ★これに動的に差し替えたい
          scopes: ★これに動的に差し替えたい
            mail:send: "delivery"
            mail:result: "get the delivery results"
          x-audience: "https://mail.paas.crmstyle.com" ★これに動的に差し替えたい

アクセストークン発行 URL やスコープは、デフォルトの状態で OpenAPI 定義ファイルから内容を読み取ってくれます。
application.yml のテンプレートを以下のように変更することで、差し替えが可能です。

{{#authMethods}}{{#isOAuth}}
spring:
  security.oauth2.client:
    provider:
      paas:
        token-uri: {{tokenUrl}} ★これで差し替え可能
    registration:
      paas:
        authorization-grant-type: client_credentials
        client-id: your-client-id
        client-secret: your-client-secret
        scope: ★これで差し替え可能
        {{#scopes}}
          - "{{scope}}"
        {{/scopes}}
        audience: https://mail.paas.crmstyle.com ★まだ置換できない {{/isOAuth}}{{/authMethods}}
logging:
  level:
    reactor.netty: DEBUG

audience に関しては、 OpenAPI の標準仕様には含まれていない内容となり、Specification Extensions として定義する必要があるため、デフォルトでは OpenAPI Generator では読み取ってくれません。

OpenAPI Generator での SecuritySchemes の読み取りは、DefaultCodegen#fromSecurity が行なっています。
DefaultCodegen は、 JavaClientCodegen の一番祖先の継承元です。
内容を見ると、SecuritySchemes から読み取った内容を、CodegenSecurity というオブジェクトに詰めていっているように見えます。

/**
     * Convert map of OAS SecurityScheme objects to a list of Codegen Security objects
     *
     * @param securitySchemeMap a map of OAS SecuritySchemeDefinition object
     * @return a list of Codegen Security objects
     */
    @SuppressWarnings("static-method")
    public List fromSecurity(Map<String, SecurityScheme> securitySchemeMap) {
        if (securitySchemeMap == null) {
            return Collections.emptyList();
        }

        List codegenSecurities = new ArrayList(securitySchemeMap.size());
        for (String key : securitySchemeMap.keySet()) {
            final SecurityScheme securityScheme = securitySchemeMap.get(key);

            CodegenSecurity cs = CodegenModelFactory.newInstance(CodegenModelType.SECURITY);
            cs.name = key;
            cs.type = securityScheme.getType().toString();
            cs.isCode = cs.isPassword = cs.isApplication = cs.isImplicit = false;
            cs.isBasicBasic = cs.isBasicBearer = false;
            cs.scheme = securityScheme.getScheme();

            if (SecurityScheme.Type.APIKEY.equals(securityScheme.getType())) {
                cs.isBasic = cs.isOAuth = false;
                cs.isApiKey = true;
                cs.keyParamName = securityScheme.getName();
                cs.isKeyInHeader = securityScheme.getIn() == SecurityScheme.In.HEADER;
                cs.isKeyInQuery = securityScheme.getIn() == SecurityScheme.In.QUERY;
                cs.isKeyInCookie = securityScheme.getIn() == SecurityScheme.In.COOKIE;  //it assumes a validation step prior to generation. (cookie-auth supported from OpenAPI 3.0.0)
            } else if (SecurityScheme.Type.HTTP.equals(securityScheme.getType())) {
                cs.isKeyInHeader = cs.isKeyInQuery = cs.isKeyInCookie = cs.isApiKey = cs.isOAuth = false;
                cs.isBasic = true;
                if ("basic".equals(securityScheme.getScheme())) {
                    cs.isBasicBasic = true;
                } else if ("bearer".equals(securityScheme.getScheme())) {
                    cs.isBasicBearer = true;
                    cs.bearerFormat = securityScheme.getBearerFormat();
                }
            } else if (SecurityScheme.Type.OAUTH2.equals(securityScheme.getType())) {
                cs.isKeyInHeader = cs.isKeyInQuery = cs.isKeyInCookie = cs.isApiKey = cs.isBasic = false;
                cs.isOAuth = true;
                final OAuthFlows flows = securityScheme.getFlows();
                if (securityScheme.getFlows() == null) {
                    throw new RuntimeException("missing oauth flow in " + cs.name);
                }
                if (flows.getPassword() != null) {
                    setOauth2Info(cs, flows.getPassword());
                    cs.isPassword = true;
                    cs.flow = "password";
                } else if (flows.getImplicit() != null) {
                    setOauth2Info(cs, flows.getImplicit());
                    cs.isImplicit = true;
                    cs.flow = "implicit";
                } else if (flows.getClientCredentials() != null) {
                    setOauth2Info(cs, flows.getClientCredentials());
                    cs.isApplication = true;
                    cs.flow = "application";
                } else if (flows.getAuthorizationCode() != null) {
                    setOauth2Info(cs, flows.getAuthorizationCode());
                    cs.isCode = true;
                    cs.flow = "accessCode";
                } else {
                    throw new RuntimeException("Could not identify any oauth2 flow in " + cs.name);
                }
            }

            codegenSecurities.add(cs);
        }
    }

fromSecurity からさらに、 DefaultCodegen#setOAuth2Info という private メソッドが呼ばれています。
実装を見ると、OAuth Flow Object の内容を、CodegenSecurity に詰めているようです。

private void setOauth2Info(CodegenSecurity codegenSecurity, OAuthFlow flow) {
        codegenSecurity.authorizationUrl = flow.getAuthorizationUrl();
        codegenSecurity.tokenUrl = flow.getTokenUrl();

        if (flow.getScopes() != null && !flow.getScopes().isEmpty()) {
            List<Map<String, Object>> scopes = new ArrayList<Map<String, Object>>();
            int count = 0, numScopes = flow.getScopes().size();
            for (Map.Entry<String, String> scopeEntry : flow.getScopes().entrySet()) {
                Map<String, Object> scope = new HashMap<String, Object>();
                scope.put("scope", scopeEntry.getKey());
                scope.put("description", escapeText(scopeEntry.getValue()));

                count += 1;
                if (count < numScopes) {
                    scope.put("hasMore", "true");
                } else {
                    scope.put("hasMore", null);
                }

                scopes.add(scope);
            }
            codegenSecurity.scopes = scopes;
        }
    }

さらに読み進めると、OAuthFlow に getExtensions というメソッドがあり、
CodegenSecurity に、 vendorExtensions というプロパティが用意されていることが分かります。
Specification Extensions の内容を OAuthFlow#getExtensionsから読み取って、 CodegenSecurity の vendorExtensions にセットすれば、テンプレートファイルの置換が可能となるのではと推測できます。

実際に Groovy スクリプトを以下のように変更してみて試してみます。setOAuth2Info は private スコープなので、内容を DefaultCodegen からそのまま持ってきて getExtension するコードを追記しています。
fromSecurity が setOAuth2Info を呼ぶ作りになっているため、fromSecurity も内容は変更せずオーバーライドしています。

@Grab(group = 'org.openapitools', module = 'openapi-generator-cli', version = '4.2.2')
import org.openapitools.codegen.*
import org.openapitools.codegen.languages.*
import io.swagger.v3.oas.models.security.*;

class TechscoreJavaClientCodegen extends JavaClientCodegen {

  static main(String[] args) {
    OpenAPIGenerator.main(args)
  }

  TechscoreJavaClientCodegen() {
    super()
  }

  String name = "techscore-codegen"
  
  @Override
  public void processOpts() {
    final String invokerFolder = (sourceFolder + '/' + invokerPackage).replace(".", "/");
    final String apiFolder = (sourceFolder + '/' + apiPackage).replace(".", "/");
    super.processOpts()

    if (WEBCLIENT.equals(getLibrary())) {
      // add WebClientConfig
      supportingFiles.add(new SupportingFile("WebClientConfig.mustache", invokerFolder, "WebClientConfig.java"))
      // add application.yml
      supportingFiles.add(new SupportingFile("application.yml.mustache", projectFolder + '/resources', "application.yml"))
    }
  }

  @Override
  @SuppressWarnings("static-method")
  public List fromSecurity(Map<String, SecurityScheme> securitySchemeMap) {
      if (securitySchemeMap == null) {
          return Collections.emptyList();
      } 
      (略)
      return codegenSecurities;
  }

  private void setOauth2Info(CodegenSecurity codegenSecurity, OAuthFlow flow) {
        codegenSecurity.authorizationUrl = flow.getAuthorizationUrl();
        codegenSecurity.tokenUrl = flow.getTokenUrl();

        if (flow.getScopes() != null && !flow.getScopes().isEmpty()) {
            List<Map<String, Object>> scopes = new ArrayList<Map<String, Object>>();
            int count = 0, numScopes = flow.getScopes().size();
            for (Map.Entry<String, String> scopeEntry : flow.getScopes().entrySet()) {
                Map<String, Object> scope = new HashMap<String, Object>();
                scope.put("scope", scopeEntry.getKey());
                scope.put("description", escapeText(scopeEntry.getValue()));

                count += 1;
                if (count < numScopes) {
                    scope.put("hasMore", "true");
                } else {
                    scope.put("hasMore", null);
                }

                scopes.add(scope);
            }
            codegenSecurity.scopes = scopes;
        }
        // Specification Extensions をセット
        if (flow.getExtensions() != null) {
          Map<String, Object> extensions = flow.getExtensions();
          codegenSecurity.vendorExtensions = extensions;
        }
    }
}

次に application.yml のテンプレートファイルを以下のように変更します。

{{#authMethods}}{{#isOAuth}}
spring:
  security.oauth2.client:
    provider:
      paas:
        token-uri: {{tokenUrl}}
    registration:
      paas:
        authorization-grant-type: client_credentials
        client-id: your-client-id
        client-secret: your-client-secret
        scope:
        {{#scopes}}
          - "{{scope}}"
        {{/scopes}}
        {{#vendorExtensions}} ★ 変更
        audience: {{x-audience}}{{/vendorExtensions}}{{/isOAuth}}{{/authMethods}}
logging:
  level:
    reactor.netty: DEBUG

この状態で Groovy を実行すると、SecuritySchemes から内容を読み取った src/main/resources/application.yml を得ることができます。

spring:
  security.oauth2.client:
    provider:
      paas:
        token-uri: https://auth.paas.crmstyle.com/oauth2/token
    registration:
      paas:
        authorization-grant-type: client_credentials
        client-id: your-client-id
        client-secret: your-client-secret
        scope:
          - "mail:send"
          - "mail:result"
        audience: https://mail.paas.crmstyle.com
logging:
  level:
    reactor.netty: DEBUG

終わりに

今回は Java + Spring WebFlux の例で OpenAPI Generator のクライアントコード生成のカスタマイズについて説明しました。ClientCodegen を継承したクラスを作り、テンプレートを修正・追記することで、他のサポートされている言語のクライアントコードも自分の好きなように変更することが可能と思います。(OpenAPI Generator のコードは読み解く必要がありますが)
皆さんも是非一度試してみてはいかがでしょうか。

The post OpenAPI Generator で OAuth2 アクセストークン発行のコードまで生成してみる first appeared on TECHSCORE BLOG.

]]>