Crystalのmacro紹介 "YAML/mapping", "JSON/mapping"
この記事は、 Crystal Advent Calendar 2017 の8日目の記事です。
crystalには便利なmacroがあります。macroを使えば、記述量も削減でき表現力も向上します。しかし、ただcrystalを書いているだけでは、なかなかそれらのmacroに出会うことができません。
ということで、アドベント・カレンダーの日数稼ぎも兼ねて、macroの紹介をしたいと思います。
そもそも「macroってなんだ」という場合は、下記の本家docsを参照してください。
今回は、 YAML
moduleと JSON
module にある mapping
macroを紹介します。
mapping
crystal-lang.org/api - YAML - mapping crystal-lang.org/api - JSON - mapping
yamlやjsonのデータを簡単にオブジェクトのpropertyにマッピングできます。YAMLにもJSONにもありますが、基本的な動作は同じなので、ここではYAMLを例に紹介します。
yamlの内容をclassのpropertyにmappingするには、以下のようにYAML.mapping
を使用します。
require "yaml" class User YAML.mapping( name: String, age: Int32, ) end yaml = <<-YAML name: "Taro" age: 30 YAML user = User.from_yaml(yaml) p user # => #<User:0x10f43af00 @age=30, @name="Taro"> p user.name # => "Taro" p user.age # => 30
各propertyにはオプションを設定できます。一つずつ見ていきましょう。
type
require "yaml" class User YAML.mapping( # Hashでオプションを設定する name: { type: String, }, ) end yaml = <<-YAML name: "Taro" YAML user = User.from_yaml(yaml) p user.name.class # => String p user.name # => "Taro"
type
は型を指定します。name: {type: String}
のショートカットとして name: String
と書くこともできます。
nilable
require "yaml" class User YAML.mapping( name: { type: String, nilable: true, }, ) end # nameのないyaml yaml = <<-YAML age: 30 YAML user = User.from_yaml(yaml) p user.name.class # => Nil p user.name # => nil
nilable
はnilを許可するかどうかを指定します。false
を指定するとnilを許可しなくなり、そのpropertyが無い場合はYAML::ParseException
が発生します。{type:String, nilable: true}
は {type: String?}
とも書けます。
default
require "yaml" class User YAML.mapping( name: { type: String, nilable: false, default: "default name", }, ) end # nameのないyaml yaml = <<-YAML age: 30 YAML user = User.from_yaml(yaml) p user.name.class # => String p user.name # => "default name"
propertyがnilの場合にdefault値が入ります。default値が何かオブジェクトを生成する場合(例えば [1,2,3]
やSomeObject.new
など)は、それらのインスタンスはyamlのparseの度に新しく生成されます。
key
require "yaml" class User YAML.mapping( name: { type: String, nilable: false, default: "default name", key: "firstname", }, ) end # nameではなくfirstnameがある yaml = <<-YAML firstname: "Taro" age: 30 YAML user = User.from_yaml(yaml) # yamlにはnameは存在しないが、 # keyオプションのおかげでnameにmappingできている p user.name.class # => String p user.name # => "Taro"
key
にはyamlに存在するkey名を指定します。上記の場合、name: { ... , key: "firstname"}
となっているので、「yamlの"firstname"のvalueを、Userクラスの"name" property にmappingする」となります。keyのデフォルト値はproperty名です。
converter
require "yaml" class User YAML.mapping( time: { type: Time, converter: Time::Format.new("%F %T"), }, ) end yaml = <<-YAML time: 2014-10-31 23:37:16 YAML user = User.from_yaml(yaml) p user.time.class # => Time p user.time # => 2014-10-31 23:37:16
valueを変換してtype
の型を返す「変換用オブジェクト」を設定します。「Stringで取り込んでから、後ほど変換する」ということをしなくてよくなります。一気に変換してmappingできるので便利です。
setter/getter
require "yaml" class User YAML.mapping( name: { type: String, setter: false, getter: true, }, ) end yaml = <<-YAML name: "Taro" YAML user = User.from_yaml(yaml) # setterは存在しない p user.name = "Pochi" # => undefined method 'name=' for User # getterは存在する p user.name # => "Taro"
setter/getterを定義するかどうかを指定します。
これらのオプションの他に、JSONにしかないオプションも存在するので、そちらは公式ドキュメントを参照してください。
yamlやjsonの内容がわからない場合
ここまでの説明では、yamlやjsonの内容が決まっている場合のmappingでした。yamlやjsonの内容がわからない場合は、クラスメソッドである .parse
を使います。興味がある方は公式ドキュメントを御覧ください。
crystal-lang.org/api - YAML.parse
crystal-lang.org/api - JSON.parse