Rustでserdeを使ってJSONのシリアライズとデシリアライズを試した。
はじめに
Rustでは、serdeというクレートを使ってデータのシリアライズ、デシリアライズができるらしいのでやり方を調べてみました。
serdeで対応できるデータフォーマットは、複数ありそれぞれがモジュール化されています。 JSONを使いたい場合は、serde_jsonを追加で読み込んで利用するようです。
準備
適当なプロジェクトを作成して、serde
とserde_json
をプロジェクトに追加します。
$ cargo new serde-json-sample $ cd serde-json-sample $ cargo add serde --features=derive $ cargo add serde_json
Cargo.tomlのdependenciesは以下のような感じになりました。
[dependencies] serde = { version = "1.0.189", features = ["derive"] } serde_json = "1.0.107"
使ってみる
以下のようなJSON文字列をRustから読み込むことを考えます。
[ { "Name": "Alice", "Age": 19 }, { "Name": "Bob", "Age": 18 } ]
試してみる その1(とりあえずパースしてみる)
以下のような、main.rsでひとまず、
が動くことが確認できました。
use serde::{Deserialize, Serialize}; #[allow(non_snake_case)] #[derive(Deserialize, Serialize)] struct Member { Name: String, Age: u8, } fn main() { let json_string = r#" [ { "Name": "Alice", "Age": 19 }, { "Name": "Bob", "Age": 18 } ] "#; let members: Vec<Member> = serde_json::from_str(&json_string).unwrap(); for member in members.iter() { println!("name: {}, age: {}", member.Name, member.Age); } println!("{}", serde_json::to_string(&members).unwrap()); }
ポイント
- JSONの内容をマッピングできるように、
#[derive(Deserialize, Serialize)]
でMember構造体にシリアライザーとデシリアライザーを実装する serde_json::from_str
でJSON文字列を構造体に変換できるserde_json::to_string
で構造体をJSON文字列に変換できる
実行結果は以下。
$ cargo run -q name: Alice, age: 19 name: Bob, age: 18 [{"Name":"Alice","Age":19},{"Name":"Bob","Age":18}]
この方法では、構造体のメンバー名はがJSONのキー名に依存してしまい、#[allow(non_snake_case)]
の指定が必要でした。
試してみる その2(Snake caseとPascal Caseの自動変換をする)
Member構造体に、#[serde(rename_all = "PascalCase")]
を指定すると、JSONのキーをPascalCase、構造体のメンバー名は、snake_caseとして認識してくれました。
修正したコードの差分は以下です。
diff --git a/src/main.rs b/src/main.rs index 7fd87f4..a862c93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ use serde::{Deserialize, Serialize}; -#[allow(non_snake_case)] #[derive(Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] struct Member { - Name: String, - Age: u8, + name: String, + age: u8, } fn main() { @@ -24,7 +24,7 @@ fn main() { let members: Vec<Member> = serde_json::from_str(&json_string).unwrap(); for member in members.iter() { - println!("name: {}, age: {}", member.Name, member.Age); + println!("name: {}, age: {}", member.name, member.age); } println!("{}", serde_json::to_string(&members).unwrap());
実行結果は変わらずです。
$ cargo run -q name: Alice, age: 19 name: Bob, age: 18 [{"Name":"Alice","Age":19},{"Name":"Bob","Age":18}]
変換するときにキー名を変更する方法は、他にも色々と用意されているようです。
まとめ
AWS Amplify に空のNext.jsプロジェクトをデプロイしようとしたらエラーになった
Next.jsもAmplifyも使ったことがないですが、簡単にアプリをデプロイできるらしいので、以下のサイトを参考に空っぽのNext.jsアプリをAmplify Hostingにデプロイしてみようとしたときの記録です。
参考: https://docs.amplify.aws/guides/hosting/nextjs/q/platform/js/
概要としては、create-next-app
で作成したプロジェクトの設定が足りなかったためか、next build
した際にビルド済みのファイルが格納されるout
というディレクトリが作成されず、Amplifyでのデプロイに失敗しました。
next.config.js
にoutput: 'export'
を指定することで解決しました。
お急ぎの方は、「アプリを公開する」まで読み飛ばしてください。
環境など
- OS: Arch Linux (2023年10月ころに更新した)
- Node: v20.7.0
- npm: 10.1.0
- Next.js: 13.5.5
Amplifyのインストール
$ npm install -g @aws-amplify/cli
amplifyの設定
$ amplify configure
ブラウザでIAMユーザーの登録画面が開くので、AdministratorAccess-Amplify
ポリシーをアタッチしたユーザーを作成し、アクセスキーを作成します。
Next.jsプロジェクトの作成
対話形式で以下のように選択して作りました。
$ npx create-next-app@latest nextjs-amplify-example ✔ Would you like to use TypeScript? … No / Yes → Yesを選択 ✔ Would you like to use ESLint? … No / Yes → Yesを選択 ✔ Would you like to use Tailwind CSS? … No / Yes → Noを選択 ✔ Would you like to use `src/` directory? … No / Yes → Yesを選択 ✔ Would you like to use App Router? (recommended) … No / Yes → Yesを選択 ✔ Would you like to customize the default import alias (@/*)? … No / Yes → Noを選択
作成したプロジェクトに入り、amplifyを初期化します。
$ cd nextjs-amplify-example $ amplify init
プロジェクト名を入力し、amplifyの実行時に利用するAWSのプロファイルを選択する。(先程作成したIAMユーザーのプロファイル)
おそらくAmplifyがプロジェクトから読み取って以下の設定が自動生成されるようですが、Distribution Directory Path
だけout
という値に変更します。
参考にした以下のURLでも、そのようにする旨の記載がありました。
https://docs.amplify.aws/guides/hosting/nextjs/q/platform/js/
Note: It is recommended to run this command from the root of your app directory ? Enter a name for the project nextjsamplifyexample The following configuration will be applied: Project information | Name: nextjsamplifyexample | Environment: dev | Default editor: Visual Studio Code | App type: javascript | Javascript framework: react | Source Directory Path: src | Distribution Directory Path: out →この設定だけ値をoutに変更する | Build Command: npm run-script build | Start Command: npm run-script start ...
もし、Enterを連打して進んでしまった場合は、あとからamplify configure project
を実行して、対話形式の設定でDistribution Directory Path
の値にout
を設定できます。
プロジェクトにAmplify Hostingを追加
amplify add hosting
を実行します。
途中質問があるので、以下を選択しました。
Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
Manual deployment
$ amplify add hosting ✔ Select the plugin module to execute · Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment) ? Choose a type Manual deployment You can now publish your app using the following command: Command: amplify publish
アプリを公開する
amplify add hosting
時にamplify publish
せよと言われるので、それに従ったところ以下のエラーが出ました。
$ amplify publish ... ○ (Static) automatically rendered as static HTML (uses no initial props) ✖ Zipping artifacts failed. This is often due to an invalid distribution directory path. Run "amplify configure project" to check if your Distribution Directory is pointing to a valid path. 🛑 Please ensure your build artifacts path exists. Resolution: Please report this issue at https://github.com/aws-amplify/amplify-cli/issues and include the project identifier from: 'amplify diagnose --send-report' Learn more at: https://docs.amplify.aws/cli/project/troubleshooting/ ...
Distribution Directory
も設定しているはずですが、その値がおかしいのか再設定せよと言われているようです。
色々確認したところ、参考にしたチュートリアルではNext.jsのbuildを実行したときに、Amplifyにデプロイする時に展開されるものが一式、out
というディレクトリに入っていることを想定しているようでしたが、今回作成したプロジェクトではbuildコマンドは成功しているものの、out
ディレクトリが作成されていませんでした。
更に調査すると、Next.jsのビルド時のオプションでSPAにするには、next.config.js
の設定でoutput: 'export'
を設定するとout
ディレクトリが作成されるということがわかりました。
ということで、プロジェクト内のnext.config.js
を以下のように修正します。
/*next.config.js*/ /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', //この行を追加 } module.exports = nextConfig
再度、amplify publish
を実行するとデプロイに成功しました。
$ amplify publish ... ✔ Zipping artifacts completed. ✔ Deployment complete! https://dev.abcdef12345678.amplifyapp.com
生成されたブラウザでURLにアクセスすると、無事Next.jsの初期画面が開きました。
Ruby ブロックを受け取るメソッドを作る方法
ブロックdo 〜 end
や{|hoge| hoge.bar }
を受け取って何かするメソッドを自分で作る方法。
%w(hoge foo bar).each do |s| puts s end
のようなメソッドを自分で作りたくなる時は意外と多いので、覚えられるようにメモしておこうと思います。
自前のメソッドでブロックを実行する方法
主に、以下2通りがあるようです。
- ブロック引数
&block
を使う方法 yield
を使う方法
ブロック引数を使う方法
メソッドの引数の一番後ろに&
で始まる仮引数を定義すると、これでブロックを受け取ることができます。
&
がついているとブロックをProcオブジェクトに展開されるので、block.call
でブロックを実行することができます。
Proc
オブジェクトについては、まだよくわかってないので、今度調べたいと思います。
def execute_block(&block) block.call end execute_block do puts 'Hello' end # ↓出力 # Hello # => nil
ブロックに対して引数を渡すには、call
メソッドに引数を渡します。
def execute_block_with_args(&block) a = 'Hello' b = 'world' block.call(a, b) end execute_block_with_args do |hello, world| puts "#{hello}, Ruby #{world}!" end # ↓出力 # Hello, Ruby world! # => nil
call
に渡した値が、ブロックの引数||
に囲われた変数に渡されます。
yieldを使う方法
yieldを使うと、ブロック変数の宣言の省略ができる。
def execute_block yield('Hello', 'world') end execute_block do |a, b| puts "#{a}, Ruby #{b}!" end # ↓出力 # Hello, Ruby world! # => nil
ブロックは渡されたか?
yeild
を使う場合、&block
ブロック引数を省略できるため、メソッド自体がブロックが渡されたかどうかは、メソッドの中からはわかりません。
そのためかどうかは知りませんが、block_given?
というメソッドが用意されています。
block_given?
は、ブロックが渡された場合にtrue
になるため、メソッド内でブロックの有無によって分岐させることができます。
def give_hello_if_block_given if block_given? yield 'hello' else puts 'Please give me block!' end end give_hello_if_block_given do |a| puts "Block was given #{a}." end # ↓出力 # Block was given hello. # => nil give_hello_if_block_given # ↓出力 # Please give me block! # => nil
ブロックを渡すメリット
自作のメソッドでブロックを渡せると何が嬉しいかというと、あるオブジェクトを開いて使い終わったら必ずリソースを解放したい
みたいなときに便利だと思います。
以下に、とりあえず何かを開いた状態か開いていない状態かを管理するクラスSomething
を作りました。
Something
は、new直後に@opened
をfalse
で初期化します。何も開いていない状態です。
また、open
メソッドとclose
メソッドを持っており、@opened
の状態を書き換えることができ、opened?
で今の状態を問い合わせることができます。
class Something def initialize @opened = false end def open @opened = true puts 'Opened something' end def close @opened = false puts 'Closed something' end def opened? @opened end end
これを操作して、特定のスコープの範囲内でのみ、Something
を開き、範囲外になったらSomething
を閉じるような使い方をしたいと思います。
普通にやると以下のように、open
→ 何かやる
→ close
ですが、これはクローズのし忘れなどを招きやすくなります。
something = Something.new p something.opened? # ↓出力 # false # => false something.open # ↓出力 # Opened something # => nil p something.opened? # ↓出力 # true # => true something.close # ↓出力 # Closed something # => nil p something.opened? # ↓出力 # false # => false
以下のようにブロックを受け取るメソッドを作ると、そのブロックの範囲内でオブジェクトを開くことができるようになります。
def with_open_something(something) # メソッド開始時にオープンする処理をやる something.open if block_given? yield something end ensure # メソッドが終わる前に、とりあえずクローズする something.close end
上で
something = Something.new p something.opened? # ↓出力 # false # => false with_open_something(something) do |s| p something.opened? end # ↓出力 # Opened something # true # Closed something p something.opened? # ↓出力 # false # => false
with_open_something
を実行した際に、open
メソッドの実行に続けて、ブロック内の処理が実行され、その後close
するという動きになっていることがわかります。
まとめ
ブロックの使い方を実行しながら確認してみました。
Procオブジェクトが密接に絡んでくるようなので、Procを理解できるともっと色々できるかもしれません。今度調べてみたいと思います。
オープンしてクローズするパターンを試してみましたが、ある処理の前後の処理を共通化したい
みたいなときにも使えて便利だと思います。
RubyのArray#delete, Hash#delete, String#deleteの違い
delete メソッドは、Array、String、Hashクラスに用意されていて、動きを整理しておきたい。
Array#delete
引数に一致する配列の要素を削除し、削除したオブジェクトを返す。
メソッド名に!が付いていないけど、元の配列に対して変更を加えるため、破壊的な操作をするメソッドになっている。
>> a = %i(a b c d) => [:a, :b, :c, :d] >> a.delete :b => :b >> a => [:a, :c, :d]
Hash#delete
引数に一致するkeyの要素を削除し、削除したvalue値を返す。
こちらも配列同様、メソッド名に!が付いていないけど、元のHashに対して変更を加えるため、破壊的な操作をするメソッドになっている。
>> h = {a: 'hoge', b: 'huga'} => {:a=>"hoge", :b=>"huga"} >> h.delete :b => "huga" >> h => {:a=>"hoge"}
String#delete, String#delete!
引数として渡した文字列に含まれる全ての文字を、文字列から削除する。
戻り値は、削除後の文字列を返す。
破壊的なメソッドのdelete!もある。
# delete >> s = "hello, world" => "hello, world" >> s.delete("l") => "heo, word" >> s.delete("lo") => "he, wrd" >> s => "hello, world" # delete! >> s = "hello, world" => "hello, world" >> s.delete!("lo") => "he, wrd" >> s => "he, wrd"
まとめ
- ArrayやHashとStringのdeleteメソッドは元のオブジェクトに影響を与えるかどうかという点で微妙に動きが異なる
- String#deleteは削除後の文字列を返すが、Array#deleteやHash#deleteは、削除したオブジェクトを返す
chopとchompの違い
`chop`と`chomp`の違いについて調べた。
chop(chop!)
文字列の末尾の文字を1文字削除した文字列を返すメソッド。
ずっと改行を削除するメソッドだと思っていましたが、文字も削除されるということを最近知った。。。
末尾が"\r\n"の場合は両方の文字が削除される。
>> "hoge".chop => "hog" >> "hoge\r\n".chop => "hoge"
chomp(chomp!)
chopと違い、改行以外の文字は削除されない。
>> "hoge".chomp => "hoge" >> "hoge\r\n".chomp => "hoge"
まとめ
- 文字列から改行を削除したいときは、chompを使う
- とりあえず末尾の文字を消したいときは、chopを使う
Rubyにざっくり入門する
FizzBuzzを書くまでRubyの構文をざっくり勉強した時のメモです。
変数
代入すれば、変数になります。
頭に$がつくとグローバル変数になります。
# 変数 ##################### $msg = "Hello ruby world.\n" # グローバル変数 msg = "Hello ruby world.\n" # ローカル変数 # 変数を出力する ##################### print "#{$msg}" print "#{msg}" print $msg, "------\n", msg
条件分岐
if "条件" then "条件がtrueの時の処理" endの形で書きます。
複数の条件を指定する場合は、次の条件をelsifで繋げて書きます。
thenは省略できます。
# 条件分岐 ##################### # if if true then print "if true.\n" end # elseを通る時 if false then #通らない else print "if false.\n" end # 複数の条件を設定した時 if false # thenは省略できる #通らない elsif true print "else if true.\n" else #通らない end
繰り返し処理
while文は条件がtrueの間繰り返します。
# while文 sum = 0 count = 1 while count <= 10 sum = sum + count count += 1 end print "1~10の合計は#{sum}です。\n"
FizzBuzzしてみる
ここまでわかるとFizzBuzz問題が書けます。
# FizzBuzz ##################### # 1から始まるnまでの数字を以下のルールで出力する # 3で割り切れるならFizz # 5で割り切れるならBuzz # 3と5両方で割り切れるならFizzBuzzを出力する n = 100 i = 1 while i <= n if i % 15 == 0 print "FizzBuzz" elsif i % 3 == 0 print "Fizz" elsif i % 5 == 0 print "Buzz" else print i end if i != n print "," end i += 1 end print "\n"
おわり
Ruby 標準入力から複数行読み取りたい
Ruby で標準入力から複数行読み込む方法を調べた。
環境はMac OSX Yosemite バーション 10.10.5
Ruby 2.3.1
読み取りに使えるメソッド
標準入力から読み取るには、$stdinオブジェクトを使う。
$stdinオブジェクトには以下のようなメソッドがある。
- $stdin.gets
- $stdin.readline
- $stdin.readlines
- $stdin.read
標準入力から1行読み取る場合はgetsやreadlineが使える。
ターミナルを起動してirbで試す。
$ irb >> $stdin.gets hoge => "hoge\n" >> $stdin.readline hoge => "hoge\n"
EOF(End Of File)まで複数行読み込みたい場合は、readlinesを使うとEOFまでの行が配列として返って来る。
$ irb >> $stdin.readlines hoge foo bar # ここで、Ctrl+Dを入力 => ["hoge\n", "foo\n", "bar\n"]
ファイルを丸ごと読み取りたい場合は、readを使う。
readに引数として読み取るバイト数を指定すると、指定されたバイト数分を標準入力から読み取る。
バイト数が未指定の場合、EOFまでの読み取った値を文字列で返す。
$ irb >> $stdin.read hoge foo bar # ここで、Ctrl+Dを入力 => "hoge\nfoo\nbar\n"
複数行の読み取り
プログラミングコンテストの問題を解くときとか、別のコマンドの出力を受け取るようなスクリプトを作るときに標準入力から複数行読み取って処理する事が多い。
こういうとき、getsを使った方法を良く見かける。
while line = gets line.chomp! # 改行を削除 print "line = #{line}\n" end
line = gets で標準入力から1行読み取り変数lineに格納する。whileの条件部分で読み取りと代入が繰り返しごとに実行される。
lineの中身によって次の繰り返しを実行するか判定している。
lineに何かしら入っていれば次の処理が実行され、lineがnilならループから抜けることになる。
getsはEOFを読み取るとnilを返すから、EOFが入力されたときループを抜ける事になる。
上の処理を適当なファイル名で保存して実行すると以下のようになる。
$ ruby standard_input.rb hoge line = hoge foo line = foo bar line = bar # ここで、Ctrl+Dを入力
whileを使う方法だと入力の読み取りと処理がくっついてしまい、処理を追加するとコードが読みづらくなっていくので読み取った値が配列で返ってきた方が嬉しい。
readlinesを使えば各行が配列で返って来るが、配列の各要素に改行コードが入って他のメソッドに入力値を渡す事を考えると邪魔くさく感じる。
readとsplitを使うと各行を配列で取り出す事ができ、入力と処理を分けられる。
p $stdin.read.split("\n")
実行すると入力値した行が配列になって返ってくる。
$ ruby standard_input.rb hoge foo bar # ここで、Ctrl+Dを入力 ["hoge", "foo", "bar"]
いつもwhileで回して改行を排除したものを配列に追加していたけれど、割とシンプルな方法で各行の値をとる事ができた。