書いた人: kwatch
YAML (YAML Ain’t Markup Language) とは、データシリアライゼーション用のフォーマットです。読み書きのしやすさを考慮して設計されたため、構造化されたデータを表現するためによく用いられます。「構造化されたデータを表現する」という点では XML と似ていますが、XML と比べて読みやすい、書きやすい、わかりやすいという特徴があります。
今回は、YAML および JSON 用のスキーマバリデータである Kwalify について説明します。Kwalify を使うと、YAML ドキュメントの内容が正しいかどうかを簡単に検証することができます。XML 用のスキーマと比べると Kwalify の機能は少ないですが、最低限の機能は揃っており、またスクリプトで簡単に拡張できるようになっています。
なお本連載では Ruby ユーザにとってのわかりやすさを優先し、用語として「配列」と「ハッシュ」を使ってきました。しかし今回はスキーマを説明するため、YAML の用語である「シーケンス」と「マッピング」を使って説明します。YAML の「シーケンス」は Ruby の「配列」に、また「マッピング」は「ハッシュ」にそれぞれ対応します。ご注意ください。
Kwalify は、YAML 用のスキーマおよびスキーマバリデータです。スキーマとはドキュメントのデータ構造を記述したものであり、スキーマバリデータとはドキュメントがスキーマに沿って正しく書かれているかどうかを検証するツールです。
ドキュメントが正しく記述されているかどうかを調べることは、とても重要です。例をあげると、
など、利用範囲は多岐に渡ります。
スキーマバリデータがあると、YAML ドキュメントが正しく書かれているかどうかを検証できます。例えば次のような YAML ドキュメントがあった場合、内容が正しくないことを検出し、それを報告してくれます。
こういった内容の検証は自分でプログラムを書いてもできますが、スキーマバリデータを使うともっと簡単に検証できます。またスキーマだけでは検証しにくいような複雑なことも、Kwalify では Ruby スクリプトを使って検証できるようになっています (もっというと「宣言的に書きやすいことは宣言的に、宣言的に書きにくいことは手続き的に」書けるようになっています)。
XML では主なスキーマとして DTD、XML Schema、RelaxNG の3つがあり、どれもよく使われています。それに比べると YAML 用のスキーマは貧弱で、過去にいろいろ議論や提案があったようですが、今のところちゃんとした実装があるのは (筆者が知る限り) Kwalify だけです。
Kwalify では以下のような検証ができます。
Kwalify ではスキーマを YAML で表現します (XML Schema や RelaxNG がスキーマを XML で表現するのと同じです)。そのため、アンカーとエイリアスを使ってスキーマ定義を共有したり再帰的に定義することが簡単にできます。また Kwalify では、スキーマ定義の検証も Kwalify 自身で行っています。
また Kwalify は Ruby と Java の実装があります。本稿では Ruby 版を解説しますが、Java 版でも動作は同じです。Java 版については Kwalify ユーザーズガイドを参照してください。
なお Kwalify は「50%-5% の法則」という精神に基づいています。これは「5% の労力で目的の 50% がカバーできる」という発想であり、「80%-20% の法則」(パレートの法則) をより極端にしたものです。そのため Kwalify は RelaxNG や XML Schema などと比べるとできることは少ないですが、シンプルでかつ全体を理解するのも簡単です。また日常使う分には十分なだけの機能を持っています。
Kwalify のインストールについて説明します。対象となるバージョンは Kwalify 0.5.1 です。
なおインストール例は一般的な UNIX 環境で bash を使った場合とします。
Kwalify の使い方を簡単に説明しておきます。
Kwalify の使い方は主に2つあります。スキーマファイルを指定して YAML ドキュメントを検証する場合と、スキーマファイル自身を検証する場合です。
また以下のようなコマンドオプションが指定できます。
Kwalify では、スキーマ定義も YAML 形式で記述します。
このセクションでは、Kwalify のスキーマについて説明します。
文字列のシーケンスをあらわすスキーマは次のようになります。
schema01.yaml : 文字列のシーケンス
\{\{*type: seq*\}\} # データ型はシーケンス
\{\{*sequence:*\}\} # シーケンスの要素は
- type: str # データ型が文字列
ポイントは次のとおりです。
次の例は、上のスキーマに適合する YAML ドキュメントです。ドキュメントがスキーマに適合することを、一般に「妥当である (valid)」といいます。
valid01.yaml : 妥当な YAML ドキュメント
Kwalify を使って、実際に検証してみましょう。「valid.」と表示されれば成功です。
実行結果 :
次に、上のスキーマに適合しない YAML ドキュメントの例を示します。ドキュメントがスキーマに適合しないことを、一般に「妥当でない (invalid)」といいます。
invalid01.yaml : 妥当でない YAML ドキュメント
これを、Kwalify を使って検証してみます。「INVALID」と表示されて、妥当でないことがわかります。
実行結果 :
妥当でなかった箇所は「/1」のように YPath 形式で示されます。この場合だと、シーケンスにおける 2 番目の要素 (YPath では先頭が 0 から始まるため) が文字列ではないと表示されています。
なお Kwalify では、妥当でなかった箇所の行番号をデフォルトでは表示しません。これは、Syck の YAML パーサが YAML ドキュメントをパースしたあとに行番号の情報を捨ててしまうためです。コマンドオプション「-l」をつけると、パース後も行番号を保持する独自の YAML パーサを使って、エラー箇所の行番号を表示します。
この YAML パーサは所詮「なんちゃってパーサ」であり、あまり信用できるものではありません。しかし本稿で説明するくらいのドキュメントなら問題なくパースできるようですので、以降では「-l」オプションをつけて行番号を表示させてみます。
マッピングを表すスキーマは次のようになります。
schema02.yaml : マッピングを表すスキーマ
\{\{*type: map*\}\} # マッピング型
\{\{*mapping:*\}\}
"name":
type: str # 文字列型
required: yes # 必須である
"email":
type: str # 文字列型
pattern: /@/ # パターンとして「@」を含む
"age":
type: int # 整数型
"birth":
type: date # 日付型
ポイントは次のとおりです。
妥当な YAML ドキュメントの例は次のようになります。
valid02.yaml : 妥当な YAML ドキュメント
検証結果は次のとおりです。
実行結果 :
妥当でない YAML ドキュメントの例は次のようになります。
invalid02.yaml : 妥当でない YAML ドキュメント
検証すると次のようなエラーがでます。
実行結果 :
マッピングを要素とするようなシーケンスを考えます。これを表すスキーマの例は次のようになります。
schema03.yaml :
\{\{*type: seq*\}\} # シーケンス型
\{\{*sequence:*\}\} # シーケンスの要素
- \{\{*type: map*\}\} # マッピング型
\{\{*mapping:*\}\} # マッピングの要素
"name":
type: str
required: true
"email":
type: str
妥当な YAML ドキュメントの例は次のようになります。
valid03.yaml : 妥当な YAML ドキュメント
検証結果は次のとおりです。
実行結果 :
妥当でない YAML ドキュメントの例は次のようになります。
invalid03.yaml : 妥当でない YAML ドキュメント
検証すると次のようなエラーがでます。
実行結果 :
今度は逆に、シーケンスを要素とするマッピングの例です (実際には、「マッピングを要素とするシーケンスを要素とするマッピング」の例です)。
schema04.yaml : シーケンスを要素とするマッピングのスキーマ
\{\{*type: map*\}\} # マッピング型
\{\{*mapping:*\}\} # マッピングの要素
"company":
type: str
required: yes
"email":
type: str
"employees":
\{\{*type: seq*\}\} # シーケンス型
\{\{*sequence:*\}\} # シーケンスの要素
- type: map # マッピング型
mapping: # マッピングの要素
"code":
type: int
required: yes
"name":
type: str
required: yes
"email":
type: str
妥当な YAML ドキュメントの例は次のようになります。
valid04.yaml : 妥当な YAML ドキュメント
検証結果は次のとおりです。
実行結果 :
妥当でない YAML ドキュメントの例は次のようになります。
invalid04.yaml : 妥当でない YAML ドキュメント
検証すると次のようなエラーが出ます。
実行結果 :
「unique:」を使うと、シーケンスやマッピングの要素に対して一意制約をつけることができます。これは SQL の unique 制約と同じです。 次の例は、マッピングの要素とシーケンスの要素に一意制約をつけた例です。
schema05.yaml : 一意制約を使ったスキーマ
type: seq
sequence:
- type: map
required: yes
mapping:
"name":
type: str
required: yes
\{\{*unique: yes*\}\} # マッピングの要素に対して一意制約
"email":
type: str
"groups":
type: seq
sequence:
- type: str
\{\{*unique: yes*\}\} # シーケンスの要素に対して一意制約
妥当な YAML ドキュメントの例は次のようになります。
valid05.yaml : 妥当な YAML ドキュメント
検証結果は次のとおりです。
実行結果 :
妥当でない YAML ドキュメントの例は次のようになります。
invalid05.yaml : 妥当でない YAML ドキュメント
検証すると次のようなエラーがでます。
実行結果 :
Kwalify のスキーマで指定できる制約は次のとおりです。
「assert:」‥‥値が守るべき条件を式で指定します。このとき、値は「val」という変数で指定し、例えば「assert: val < 0 | 10 < val」のように指定します。なおこの機能は実験的であり、将来変更される可能性があります。 |
Kwalify では、制約が集まったひとかたまりをルールと読んでいます (図1参照)。シーケンスまたはマッピングを使うことでルールをネストさせることができ、スキーマはネストしたルールで表されます。またルールに名前をつけておくと、Ruby スクリプトからそのルールを参照できます (これについては後ほど説明します)。
次の例は、様々な制約を使ったスキーマの例です。
schema06.yaml : 様々な制約を使ったスキーマ
name: address-book # 名前
desc: アドレス帳 # 説明
type: seq
sequence:
- type: map
mapping:
"name":
type: str
required: yes # 必須項目
"email":
type: str
required: yes # 必須項目
pattern: /@/ # パターン
"password":
type: str
length: { max: 16, min: 8 } # 長さ指定
"age":
type: int
range: { max: 30, min: 18 } # 範囲指定
# or assert: 18 <= val && val <= 30 # 条件式
"blood":
type: str
enum: # 列挙
- A
- B
- O
- AB
"birth":
type: date
"memo":
type: any # データ型は任意
妥当でない YAML ドキュメントの例は次のようになります。
invalid06.yaml : 妥当でない YAML ドキュメント
検証すると次のようなエラーが出ます。
実行結果
YAML では標準で以下のような機能が使用できます。
このセクションでは、これらの機能を使ったスキーマ定義のテクニックを紹介します。
YAML には、データに「印」をつけておき、他の場所からそのデータを参照することができます。これをアンカーとエイリアスといいます。
例えば次のような YAML ドキュメントがあるとします。
- \{\{*&a*\}\} # アンカー
name: foo
parent: ~
- name: bar
parent: \{\{**a*\}\} # エイリアス
- name: baz
parent: \{\{**a*\}\} # エイリアス
これは次のような Ruby スクリプトと同じです。
foo = \{\{*a*\}\} = { "name"=>"foo", "parent"=>nil }
bar = { "name"=>"bar", "parent"=>\{\{*a*\}\} }
baz = { "name"=>"baz", "parent"=>\{\{*a*\}\} }
[ foo, bar, baz ]
アンカーとエイリアスを使うと、ひとつのルールを複数の箇所で共有することができます。次の例では著作者についてのルールを翻訳者でも共有しています。
schema07.yaml : ルールを共有するスキーマ
desc: 書籍リスト
type: seq
sequence:
- type: map
mapping:
"title":
type: str
required: yes
"author": \{\{*&persons*\}\} # アンカー
type: seq
sequence:
- type: str
"translator": \{\{**persons*\}\} # エイリアス
"publisher":
type: str
"year":
type: int
妥当な YAML ドキュメントの例は次のようになります。
valid07.yaml : 妥当な YAML ドキュメントの例
検証結果は次のとおりです。
実行結果 :
アンカーとエイリアスを使うと、再帰的なルールを定義することもできます。木構造のようなデータを表現するときに便利です。
次の例では、タスクが複数のサブタスクから構成され、かつタスクとサブタスクが同じルールを共有することで、再帰的な定義となっています。
schema08.yaml : 再帰的なルールの定義
\{\{*&task*\}\} # アンカー
desc: 作業分解図
type: map
mapping:
"name": { type: str, required: yes }
"assigned": { type: str }
"deadline": { type: date }
"subtasks":
type: seq
sequence:
- \{\{**task*\}\} # エイリアス
妥当な YAML ドキュメントの例は次のようになります。
valid08.yaml : 妥当な YAML ドキュメント
検証結果は次のとおりです。
実行結果 :
YAML では、マッピングのデフォルト値を指定できます。キーとして「=」を使用すると、その値がマッピングのデフォルト値として使われます。
例えば次の YAML ドキュメントがあるとします。
A: 10
B: 20
\{\{*=*\}\}: -1 # デフォルト値
これは次の Ruby コードと同じです。
map1= {"A"=>10, "B"=>20}
\{\{*map1.default = -1*\}\}
YAML のこの機能を使って、Kwalify ではマッピングにおいて一致するキーの名前が見つからなかった場合のデフォルトルールを指定できるようになっています。これは、キー名が事前にはわからない場合に使用します。
例えば、アプリケーションの設定ファイルを YAML 形式で書くことにしたとします。設定ファイルには、ユーザが独自のプロパティ名とその値を指定できるようにします。
通常であれば、次のように名前と値を別々に定義するでしょう。
これを、次のように「名前: 値」にすることで、より簡潔な記述にしたいとします。
このとき問題になるのは、マップのキーが何になるか、事前にはわからないことです。これではスキーマ定義を書くことができません。
このようなときは、キーに「=」を指定することでデフォルトのルールを指定します。こうすることで、任意のキーにマッチするようになります。
schema09.yaml : デフォルトのルールを指定したスキーマ
type: map
mapping:
"properties":
type: map
mapping:
\{\{*=*\}\}: # 任意のキーにマッチ
type: any # 値はどんなデータ型でもよい
また他のキーと共に使用することもできます。次の例は、アドレス帳に任意のキーと値を追加できるようにした例です。
schema10.yaml : ユーザが自由にキーと値を追加できるアドレス帳
type: seq
sequence:
- type: map
mapping:
"name":
type: str
required: yes
"email":
type: str
"birth":
type: date
\{\{*=*\}\}: # キーが「name」「email」「birth」以外の場合
type: any # 値はどんなデータ型でもよい
デフォルトルールは、いわばどんなキーにもマッチするルールです。そのため、これを使うとキーのタイプミスが発見できなくなります。それを考慮したうえでお使いください。
YAML では、「<<」を使って複数のマッピングをマージする機能があります。またマージしたマッピングでキーと値の上書や追加ができます。
例えば次のような YAML ドキュメントがあるとします。
- &m1
A: 10
B: 20
- \{\{*<<*\}\}: *m1 # マージ
A: 15 # 上書き
C: 30 # 追加
これは次の Ruby コードと同じです。
m1 = {"A"=>10, "B"=20}
m2 = {}
\{\{*m2.update(m1)*\}\} # マージ
m2["A"] = 15 # 上書き
m2["C"] = 30 # 追加
[m1, m2]
また複数のマッピングをマージすることができます。 例えば次のような YAML ドキュメントがあるとします。
- &m1
A: 10
B: 20
- &m2
C: 30
D: 40
- \{\{*<<: [*m1, *m2]*\}\} # 複数のマッピングをマージ
これは次の Ruby コードと同じです。
m1 = {"A"=>10, "B"=>20}
m2 = {"C"=>10, "D"=>20}
m3 = {}
\{\{*m3.update(m1)*\}\} # マージ
\{\{*m3.update(m2)*\}\} # マージ
[m1, m2, m3]
YAML のこの機能を使うと、Kwalify でルールのマージや差分定義(上書きと追加)ができます。次の例では、グループについてのルールに追加・上書きすることで、ユーザについてのルールを定義しています。
schema11.yaml : ルールのマージと差分定義を行う例
type: map
mapping:
"group":
type: map
mapping:
"name": \{\{*&name*\}\}
type: str
required: yes
"email": \{\{*&email*\}\}
type: str
pattern: /@/
required: no
"user":
type: map
mapping:
"name":
\{\{*<<: *name*\}\} # マージ
\{\{*length: { max: 16 }*\}\} # 追加
"email":
\{\{*<<: *email*\}\} # マージ
\{\{*required: yes*\}\} # 上書き
次のような、妥当でない YAML ドキュメントで試してみましょう。
invalid11.yaml : 妥当でない YAML ドキュメントの例
検証してみると、次のようなエラーがでます。
実行結果 :
このセクションでは、Ruby スクリプトから Kwalify を使う方法を説明します。
Ruby スクリプトにおいて、Kwalify のライブラリを使って YAML ドキュメントを検証するには、Kwalify::Validator クラスを使います。
script1.rb : Kwalify ライブラリを使って検証するサンプル
require 'kwalify'
## スキーマファイルを読み込み、バリデータを作成する。
## スキーマ定義にエラーがあると、例外 Kwalify::SchemaError が発生する。
schema = YAML.load_file('schema.yaml')
\{\{*validator = Kwalify::Validator.new(schema)*\}\}
## YAML ドキュメントを読み込み、バリデータで検証する。
## エラーがあれば Kwalify::ValidationError の配列が、なければ空の配列が返される。
document = YAML.load_file('document.yaml')
\{\{*errors = validator.validate(document)*\}\}
if !errors || errors.empty?
puts "valid."
else
errors.each do |error|
puts "[#{error.path}] #{error.message}"
end
end
この例でわかるように、Kwalify では YAML ドキュメントをパースするときに検証するのではなく、読み込んだあとの YAML ドキュメントに対して検証を行います。そのため、Syck の YAML パーサのように行番号をパース中にしか保持しないパーサだと、エラー箇所の行番号がわかりません。
コマンドラインオプション「-l」と同じようにエラー箇所を行番号で表示したい場合は、Syck の YAML パーサではなく Kwalify::YamlParser を使用する必要があります。ただしこのパーサは実験的であり、YAML の仕様をすべて満たしているわけではありませんので注意してください。
script2.rb : Kwalify::YamlParser を使って行番号を表示するサンプル
## Kwalify::Parse を使って YAML ドキュメントを読み込む。
## (このパーサは行番号を保持している。)
str = ARGF.read()
\{\{*parser = Kwalify::Parser.new(str)*\}\}
\{\{*document = parser.parse()*\}\}
## スキーマファイルを読み込み (これは Syck を使ってもよい)、
## バリデータを作成して検証する。
schema = YAML.load_file('schema.yaml')
\{\{*validator = Kwalify::Validator.new(schema)*\}\}
\{\{*errors = validator.validate(document)*\}\}
## エラーがあれば、行番号を設定して表示する。
## 行番号を設定するには、Kwalify::Parser が必要。
if !errors || errors.empty?
puts "valid."
else
\{\{*parser.set_errors_linenum(errors)*\}\} # YPath をもとに行番号を設定
errors.sort.each do |err| # 行番号でソートし、表示する
print "line %d: path %s: %s" % [err.linenum, err.path, err.message]
end
end
また読み込んだあとの YAML ドキュメントは配列とハッシュとスカラーの組み合わせであり、これは JSON と同じです。つまり JSON を読み込んで、それを Kwalify で検証することが可能です。ほかにも、CGIパラメータの検証などに使用できるでしょう。
なお Kwalify のバリデータはステートレスであり、ひとつのバリデータで複数の YAML ドキュメントを検証することができます。YAML ドキュメントごとにバリデータを生成する必要はありません。
スキーマファイル自身が正しいかどうかを検証するバリデータを「メタバリデータ」と呼んでいます。メタバリデータは Kwalify::MetaValidator.instance() または Kwalify.meta_validator() で取得できます。また検証の仕方はふつうのバリデータと同じです。
script3.rb : スキーマファイル自身を検証するサンプル
## メタバリデータを取得する
require 'kwalify'
\{\{*meta_validator = Kwalify::MetaValidator.instance()*\}\}
## スキーマファイルを検証する
schema = File.load_file('schema.yaml')
\{\{*errors = meta_validator.validate(schema)*\}\}
if !errors || errors.empty?
puts "valid."
else
errors.each do |error|
puts "- [#{error.path}] #{error.message}"
end
end
Kwaify では、Kwalify::Validator#validate_hook() というフックメソッドが用意されています。これは、Kwalify::Validator#validate() から呼ばれるメソッドであり、プログラマーがサブクラスで拡張できます。
フックメソッドを用いると、複数の項目にまたがるような検証など、Kwalify のスキーマでは検証しにくいような内容を検証できます。 例えば次の例では、「アンケートで『悪い』と答えた人はその理由を書かなければならない」という検証をしていますが、これは「アンケートの答え」によって「理由」が必須かそうでないかが変わっています。
script4.rb : フックメソッドの例
#!/usr/bin/env ruby
require 'kwalify'
require 'yaml'
## Kwalify::Validator のサブクラスを定義
class AnswersValidator < Kwalify::Validator
## スキーマ定義を読み込む
@@schema = YAML.load_file('answers.schema.yaml')
def initialize()
super(@@schema)
end
## Validator#validate() から呼び出されるフックメソッド
def \{\{*validate_hook(value, rule, path, errors)*\}\}
# ルールの名前が 'Answer' である場合だけ実行
case \{\{*rule.name*\}\}
when \{\{*'Answer'*\}\}
# 解答が 'bad' であれば、理由の記入が必須である
if value['answer'] == 'bad'
if value['reason'] == nil || value['reason'].empty?
msg = "reason is required when answer is 'bad'."
\{\{*errors << Kwalify::ValidationError.new(msg, path)*\}\}
end
end
end
end
end
## YAML ドキュメントを読み込む
## (エラー行番号を表示するために、Kwalify::YamlParser を使う。)
input = ARGF.read()
parser = Kwalify::YamlParser.new(input)
document = parser.parse()
## バリデータを作成し、検証する
validator = AnswersValidator.new
errors = validator.validate(document)
## エラーがあれば行番号つきで表示する
if !errors || errors.empty?
puts "Valid."
else
parser.set_errors_linenum(errors)
errors.sort.each do |error|
puts " - line #{error.linenum}: [#{error.path}] #{error.message}"
end
end
スキーマは次のようになります。「name:」を使ってルールに名前をつけ、それを上のスクリプトで参照しているのがポイントです。
answers.schema.yaml : スキーマ
type: map
mapping:
answers:
type: seq
sequence:
- type: map
\{\{*name: Answer*\}\} # ルールに名前をつける
mapping:
"name":
type: str
required: yes
"answer":
type: str
required: yes
enum:
- good
- not bad
- bad
"reason":
type: str
妥当でない YAML ドキュメントの例は次のようになります。
answers.data.yaml : 妥当でない YAML ドキュメント
スクリプトによる検証結果は次のとおりです。
実行結果 :
スキーマによる検証とプログラムによる検証を比べると、一般的に次のような特徴があります。
Kwalify では、宣言的に書きやすいことは宣言的に、宣言的に書くのが難しいことは手続き的に書くことができるため、両者のいいとこどりができます。
なお Kwalify のバリデータはステートレスとなっていますが、フックメソッドの中でインスタンス変数を使う場合はステートフルになるので注意してください。
Kwalify のアーカイブには、スキーマのサンプルがいくつか用意されています。
これとは別に、前回作成した creatable ツールのスキーマを定義してみました。ポイントは次の通りです。
Kwalify ではスキーマを YAML 形式で定義するので、アンカーとエイリアスを使ってこのようなことが簡単にできます。
creatable.schema.yaml : creatable のためのスキーマ
type: map
mapping:
"defaults":
type: map
mapping:
"columns":
type: seq
sequence:
- \{\{*&column-rule*\}\} # アンカー
type: map
mapping:
"name": { type: str, required: yes }
"desc": { type: str }
"type": { type: str }
"width": { type: int }
"primary-key": { type: bool }
"serial": { type: bool }
"not-null": { type: bool }
"unique": { type: bool }
"enum":
type: seq
sequence:
- type: scalar
\{\{*"ref": *column-rule*\}\} # エイリアスで再帰的な定義
"tables":
type: seq
sequence:
- type: map
mapping:
"name": { type: str, required: yes }
"class": { type: str }
"desc": { type: str }
"columns":
type: seq
sequence:
- \{\{**column-rule*\}\} # エイリアスでルールを共有
前回のデータファイル (datafile.yaml) を使って検証してみましょう。
実行例 :
本稿では、YAML 用のスキーマバリデータである Kwalify について説明しました。Kwalify は次のような特徴を持ちます。
Kwalify は決して高機能ではありませんし、RelaxNG のような数学的なバックグラウンドを持つわけでもありません。しかし普通に使う分には十分な機能を持っていますし、なによりスクリプトでいくらでも拡張できるため、大変便利です。
なお本当は XML のスキーマ (DTD, XML Schema, RelaxNG) と Kwalify とを比較してみたかったのですが、筆者の力不足でできませんでした。機会があればチャレンジしたいと思います。
名前:kwatch。三流プログラマー。もし世の中に「ソフトウェア構造計算書」なんてものが存在したらほとんどのプロジェクトでは偽造されるんやろなーと思ったが、バグを「仕様です」なんて言い切ってる時点で偽造してるのと変わらんやんと気づき、やや鬱。最近のお気に入りは「チャングム」。