んなまにのメモ帳

気が向いたときに更新されます。

Rustでserdeを使ってJSONのシリアライズとデシリアライズを試した。

はじめに

Rustでは、serdeというクレートを使ってデータのシリアライズ、デシリアライズができるらしいのでやり方を調べてみました。

serdeで対応できるデータフォーマットは、複数ありそれぞれがモジュール化されています。 JSONを使いたい場合は、serde_jsonを追加で読み込んで利用するようです。

準備

適当なプロジェクトを作成して、serdeserde_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でひとまず、

  1. JSONを構造体に変換
  2. 構造体をJSONに変換

が動くことが確認できました。

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_strJSON文字列を構造体に変換できる
  • 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}]

変換するときにキー名を変更する方法は、他にも色々と用意されているようです。

serde.rs

まとめ

  • RustでJSONを扱うときは、serdeとserde_jsonというクレートが使える
  • JSON文字列をRustの構造体にマッピングできる。
  • JSONと構造体間でキー名の変換などをやりたい場合は、#[serde(rename_all = )]などを使うとできる

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.jsoutput: '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を実行します。 途中質問があるので、以下を選択しました。

  1. Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
  2. 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ディレクトリが作成されるということがわかりました。

nextjs.org

ということで、プロジェクト内の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の初期画面が開きました。

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直後に@openedfalseで初期化します。何も開いていない状態です。 また、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の構文をざっくり勉強した時のメモです。

まずは、Hello world

print メソッドで標準出力に出力します。

print "Hello ruby world.\n"

変数

代入すれば、変数になります。
頭に$がつくとグローバル変数になります。

# 変数
#####################
$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で回して改行を排除したものを配列に追加していたけれど、割とシンプルな方法で各行の値をとる事ができた。