圧倒亭グランパのブログ

30年後の自分にもわかるように書くブログ

Crystalのmacro紹介 "YAML/mapping", "JSON/mapping"

この記事は、 Crystal Advent Calendar 2017 の8日目の記事です。

crystalには便利なmacroがあります。macroを使えば、記述量も削減でき表現力も向上します。しかし、ただcrystalを書いているだけでは、なかなかそれらのmacroに出会うことができません。

ということで、アドベント・カレンダーの日数稼ぎも兼ねて、macroの紹介をしたいと思います。

そもそも「macroってなんだ」という場合は、下記の本家docsを参照してください。

Macros · GitBook

 

今回は、 YAML moduleと JSON module にある mapping macroを紹介します。

mapping

crystal-lang.org/api - YAML - mapping
crystal-lang.org/api - JSON - mapping

yamljsonのデータを簡単にオブジェクトの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

nilablenilを許可するかどうかを指定します。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にしかないオプションも存在するので、そちらは公式ドキュメントを参照してください。

 

yamljsonの内容がわからない場合

ここまでの説明では、yamljsonの内容が決まっている場合のmappingでした。yamljsonの内容がわからない場合は、クラスメソッドである .parse を使います。興味がある方は公式ドキュメントを御覧ください。

crystal-lang.org/api - YAML.parse

crystal-lang.org/api - JSON.parse

 

crystalのyamljsonの標準ライブラリは便利なので、ぜひ使ってみてください。