nestされたresourcesでのcontrollerの指定

nestされたresoucesでのController

1対多などの関連を持つmodelをそれぞれ直接操作したい時があります。
その場合は、それぞれのmodelに対してcontrollerを作成し、CRUDなどの操作を行います。

それとは別に1対多のリソースの関係をURLに反映したい場合もあります。
例えば、author has many postsだった場合、特定のauthorが持つpostsの一覧は以下のようなURLになると思います。

1
/authors/:author_id/posts

このURLを実現するために、routingの設定は以下のようにします。

1
2
3
resources :authors do
resources :posts
end

ただ、この設定をrails routesで確認すると、先ほど記載したURL(authorにぶら下がったposts)のControllerは、posts#indexとなってしまい、authorにぶら下げないpostsと同じControllerになってしまいます。

対処法

このままだとposts_controller.rbにいろんなメソッドを追加しないといけなくなってしまうので、authorにぶらさがるpostsを処理するControlerは別で定義したいと思います。

ディレクトリの作成

app/controllersの下にauthorsというディレクトリを作成します。このディレクトリの配下にauthorにぶら下がるリソースのControllerを作成していきます。

Controllerの作成

先ほど作成したauthorsの下にposts_controller.rbを作成します。
app/controllers配下のposts_controller.rbの違いとしては、namespaceを持っているということです。

1
2
3
4
module Authors
class PostsController < ApplicationController
end
end

routingの設定

現在のroutingの設定に、作成したControllerを指定します。

1
2
3
resources :authors do
resources :posts, controller: "authors/posts"
end

まとめ

以上でnestされたresourcesに対応するControllerを設定することができました。
こういったmodelの関連を表すURLはよく作成するので忘れないようにしたいと思います。

npm startでエラー 'React' must be in scope when using JSX

背景

Reactの本を買ったので、一年半ぶりにReactの記事を書きます。

書籍に書いてある通りcreate-react-appを実行して作成されたアプリケーションのディレクトリに移動します。

1
2
3
$ npx create-react-app my-react
$ cd my-react
$ npm start

すると、ブラウザに表示された画面は真っ白で、コンソールには

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> my-react@0.1.0 start
> react-scripts start
Starting the development server...
Failed to compile.

./src/App.js
Line 7:5: 'React' must be in scope when using JSX react/react-in-jsx-scope
Line 8:7: 'React' must be in scope when using JSX react/react-in-jsx-scope
Line 9:9: 'React' must be in scope when using JSX react/react-in-jsx-scope
Line 10:9: 'React' must be in scope when using JSX react/react-in-jsx-scope
Line 11:16: 'React' must be in scope when using JSX react/react-in-jsx-scope
Line 13:9: 'React' must be in scope when using JSX react/react-in-jsx-scope

Search for the keywords to learn more about each error.

と表示されてしまいました。

原因

調べてみると、src/App.jsの先頭行に以下の記述が必要だったようです。

1
import React from 'react';

一旦この行を追加してから実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ npm start

> my-react@0.1.0 start
> react-scripts start
Starting the development server...
Compiled successfully!

You can now view my-react in the browser.

Local: http://localhost:3000/
On Your Network: http://192.168.11.7:3000/

Note that the development build is not optimized.
To create a production build, use npm run build.

確かに問題なく動作しました。
であれば、なぜその行の記述がなかったのでしょうか。

JSX Transform

React17から導入されたJSX Transfromでは、先ほど記述したimport文が不要になるというメリットがあるようです。
なにが変わったかというと、トランスパイル後のコードでReactを読み込まなくても動作するようになるということのようです。

では、なぜ、今回その行を追加しないとエラーになってしまったのでしょうか。

ESLint

調べてみるとESLintの設定でエラーを抑制するように書かれていました。
package.jsonを変更しながら、ruleの箇所を追加したり、extendsを変更したりしました。
しかし、結果的に修正できませんでした。

npm install react-scripts@latest

最後の方法として、react-scriptsを最新版にすることでした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ npm install react-scripts@latest
npm WARN deprecated @babel/plugin-proposal-private-methods@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.
npm WARN deprecated @babel/plugin-proposal-numeric-separator@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.
npm WARN deprecated @babel/plugin-proposal-nullish-coalescing-operator@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.
npm WARN deprecated @babel/plugin-proposal-class-properties@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.
npm WARN deprecated rollup-plugin-terser@7.0.2: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser
npm WARN deprecated @babel/plugin-proposal-optional-chaining@7.21.0: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.
npm WARN deprecated domexception@2.0.1: Use your platform's native DOMException instead
npm WARN deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead
npm WARN deprecated workbox-cacheable-response@6.6.0: workbox-background-sync@6.6.0

added 567 packages, removed 766 packages, changed 382 packages, and audited 1555 packages in 1m

254 packages are looking for funding
run `npm fund` for details

8 vulnerabilities (2 moderate, 6 high)

To address all issues (including breaking changes), run:
npm audit fix --force

Run `npm audit` for details.

deprecatedは一旦置いておいて、その後にnpm startします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ npm start

> my-react@0.1.0 start
> react-scripts start

(node:79721) [DEP_WEBPACK_DEV_SERVER_ON_AFTER_SETUP_MIDDLEWARE] DeprecationWarning: 'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:79721) [DEP_WEBPACK_DEV_SERVER_ON_BEFORE_SETUP_MIDDLEWARE] DeprecationWarning: 'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
Starting the development server...
Compiled successfully!

You can now view my-react in the browser.

Local: http://localhost:3000
On Your Network: http://192.168.11.7:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

webpack compiled successfully

無事動作しました。

何が変わったのか

何が変わったのかをみてみると、package.jsonの差分は1行だけでした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git diff package.json
diff --git a/package.json b/package.json
index 28afd0a..bd487ff 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-scripts": "5.0.1",
+ "react-scripts": "^5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {

package-lock.jsonをみると、結構変わっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
$ git diff package-lock.json
diff --git a/package-lock.json b/package-lock.json
index ba7b477..f91a96e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,7 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-scripts": "5.0.1",
+ "react-scripts": "^5.0.1",
"web-vitals": "^2.1.4"
}
},
@@ -257,9 +257,9 @@
}
},
"node_modules/@babel/helper-define-polyfill-provider": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz",
- "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==",
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz",
+ "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==",
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -3427,9 +3427,9 @@
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="
},
"node_modules/@rushstack/eslint-patch": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz",
- "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw=="
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.0.tgz",
+ "integrity": "sha512-Jh4t/593gxs0lJZ/z3NnasKlplXT2f+4y/LZYuaKZW5KAaiVFL/fThhs+17EbUd53jUVJ0QudYCBGbN/psvaqg=="
},
"node_modules/@sinclair/typebox": {
"version": "0.24.51",
@@ -5359,9 +5359,9 @@
}
},
"node_modules/autoprefixer": {
- "version": "10.4.16",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
- "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSY
Jx5GKcpRCzBh7RH4/0dnY+uQ==",
+ "version": "10.4.17",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
+ "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3B
hA+nLo5tX8Cu0upUsGKvKbmg==",
"funding": [
{
"type": "opencollective",
@@ -5377,9 +5377,9 @@
}
],
"dependencies": {
- "browserslist": "^4.21.10",
- "caniuse-lite": "^1.0.30001538",
- "fraction.js": "^4.3.6",
+ "browserslist": "^4.22.2",
+ "caniuse-lite": "^1.0.30001578",
+ "fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
@@ -5593,12 +5593,12 @@
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz",
- "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==",
+ "version": "0.4.8",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz",
+ "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==",
"dependencies": {
"@babel/compat-data": "^7.22.6",
- "@babel/helper-define-polyfill-provider": "^0.4.4",
+ "@babel/helper-define-polyfill-provider": "^0.5.0",
"semver": "^6.3.1"
},
"peerDependencies": {
@@ -5625,12 +5625,27 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
+ "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz",
+ "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.22.6",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "debug": "^4.1.1",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.14.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
"node_modules/babel-plugin-polyfill-regenerator": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz",
- "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==",
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz",
+ "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==",
"dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.4.4"
+ "@babel/helper-define-polyfill-provider": "^0.5.0"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@@ -5965,9 +5980,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001576",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz",
- "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==",
+ "version": "1.0.30001579",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz",
+ "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==",
"funding": [
{
"type": "opencollective",
@@ -8427,9 +8442,9 @@
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
},
"node_modules/follow-redirects": {
- "version": "1.15.4",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
- "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
+ "version": "1.15.5",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
+ "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
@@ -13960,9 +13975,9 @@
}
},
"node_modules/postcss-modules-local-by-default": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz",
- "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz",
+ "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==",
"dependencies": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^6.0.2",
@@ -15407,12 +15422,12 @@
}
},
"node_modules/safe-array-concat": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
- "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz",
+ "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==",
"dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.2.1",
+ "call-bind": "^1.0.5",
+ "get-intrinsic": "^1.2.2",
"has-symbols": "^1.0.3",
"isarray": "^2.0.5"
},
@@ -16611,9 +16626,9 @@
}
},
"node_modules/terser": {
- "version": "5.26.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz",
- "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==",
+ "version": "5.27.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
+ "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",

多すぎてどこが影響しているのかわかりません…

まとめ

ESLintの問題だと思って色々試していましたが、そうじゃなかったかもしれません。
まだ初心者なので、実際の原因はわかりませんが、こういう解決方法もあるのだということを学びました。

参考文献

Ruby 3.3.0のインストールエラー

背景

Ruby3.3.0が2023年12月にリリースされました。
Railsを使った簡単なアプリを作成するために、せっかくなので、最新の3.3.0を使おうと思って、rbenvでインストールしようとしました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ rbenv install 3.3.0
ruby-build: using openssl@1.1 from homebrew
==> Downloading ruby-3.3.0.tar.gz...
-> curl -q -fL -o ruby-3.3.0.tar.gz https://cache.ruby-lang.org/pub/ruby/3.3/ruby-3.3.0.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 21.0M 100 21.0M 0 0 15.6M 0 0:00:01 0:00:01 --:--:-- 15.6M
==> Installing ruby-3.3.0...
ruby-build: using readline from homebrew
-> ./configure "--prefix=$HOME/.anyenv/envs/rbenv/versions/3.3.0" --with-openssl-dir=/usr/local/opt/openssl@1.1 --enable-shared --with-readline-dir=/usr/local/opt/readline --with-ext=openssl,psych,+
-> make -j 8
*** Following extensions are not compiled:
psych:
Could not be configured. It will not be installed.
Check /var/folders/vs/21c14yw15wngfp0fn50h2zkw0000gp/T/ruby-build.20240116222001.35871.p9E1v4/ruby-3.3.0/ext/psych/mkmf.log for more details.

BUILD FAILED (macOS 13.6 on x86_64 using ruby-build 20231225-2-g3b79c29)

You can inspect the build directory at /var/folders/vs/21c14yw15wngfp0fn50h2zkw0000gp/T/ruby-build.20240116222001.35871.p9E1v4
See the full build log at /var/folders/vs/21c14yw15wngfp0fn50h2zkw0000gp/T/ruby-build.20240116222001.35871.log

psychというextensionがコンパイルできなかったとして、エラーになってしまいました。
ログを見るとわかると思うのですが、インテルMacを利用しています。

原因

エラーメッセージをよく見ると、

1
2
3
4
*** Following extensions are not compiled:
psych:
Could not be configured. It will not be installed.
Check /var/folders/vs/21c14yw15wngfp0fn50h2zkw0000gp/T/ruby-build.20240116222001.35871.p9E1v4/ruby-3.3.0/ext/psych/mkmf.log for more details.

より詳細な情報がmkmf.logに記載されていると書かれているので、それを見てみます。
Rubyインストール前のrbenvでのコンパイル処理のディレクトリは/var/folders/あたりを利用しているんですね。

mkmf.log

1
2
3
4
5
6
pkg_config: checking for pkg-config for yaml-0.1... -------------------- not found

package configuration for yaml-0.1 is not found
--------------------

find_header: checking for yaml.h... -------------------- no

yaml.hが見つからないとなってエラーになっています。
ヘッダファイルが見つからない場合はだいたいlibヘッダファイル名のライブラリがインストールされていないのが原因です。

対策

調べてみるとRuby3.2.0以降ではlibyamlが必須になっているようでした。
homebrewでインストールします。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ brew install libyaml
Running `brew update --auto-update`...
(省略)
==> Downloading https://ghcr.io/v2/homebrew/core/libyaml/manifests/0.2.5
################################################################################### 100.0%
==> Fetching libyaml
==> Downloading https://ghcr.io/v2/homebrew/core/libyaml/blobs/sha256:b49e62f014b3e7d85a16
################################################################################### 100.0%
==> Pouring libyaml--0.2.5.ventura.bottle.tar.gz
🍺 /usr/local/Cellar/libyaml/0.2.5: 10 files, 330KB
==> Running `brew cleanup libyaml`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).

インストールできました。
再度Ruby3.3.0のインストールを行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ rbenv install 3.3.0
ruby-build: using openssl@1.1 from homebrew
==> Downloading ruby-3.3.0.tar.gz...
-> curl -q -fL -o ruby-3.3.0.tar.gz https://cache.ruby-lang.org/pub/ruby/3.3/ruby-3.3.0.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 21.0M 100 21.0M 0 0 6361k 0 0:00:03 0:00:03 --:--:-- 6371k
==> Installing ruby-3.3.0...
ruby-build: using readline from homebrew
ruby-build: using libyaml from homebrew
-> ./configure "--prefix=$HOME/.anyenv/envs/rbenv/versions/3.3.0" --with-openssl-dir=/usr/local/opt/openssl@1.1 --enable-shared --with-readline-dir=/usr/local/opt/readline --with-libyaml-dir=/usr/local/opt/libyaml --with-ext=openssl,psych,+
-> make -j 8
-> make install
==> Installed ruby-3.3.0 to /Users/user/.anyenv/envs/rbenv/versions/3.3.0

NOTE: to activate this Ruby version as the new default, run: rbenv global 3.3.0

無事インストールできました。
エラーが起こったときとconfigureオプションも異なっていて、libyamlがインストールされたことで、--with-libyaml-dir=/usr/local/opt/libyamlが追加されています。

まとめ

久しぶりにRubyのインストールを行いましたが、躓いてしまいました。
3.2.0からの変更点だったということで、今更ここで躓く方はそれほど多くないと思います。
やはり新しい情報は常にキャッチアップしていかないといけないと思いました。

git diffで^Mが表示された時の対応方法(改行コードをCRLFからLFに変換)

背景

Gitを使っていると他の方が作成したファイルを変更することも多々あります。

今回のそのケースで、変更後git diffで変更点を確認すると、

1
2
3
+        },^M
+ {^M
}

という形で見慣れた^Mが表示されてしまいました。

最初はコピペ元が良くないのかと思っていたのですが、そうではなかったです。原因と対策を以下に記載します。

原因

原因は改行コードの不一致でした。元のファイルの改行コードはCRLFでわたしの環境ではLF(Mac)だったため、CRが足りずに^Mという表示がされてしまったようです。

対策

元のファイルの改行コードをLFに変換します。vscodeを使って行います。

まず、該当のファイルを開きます。ウインドウの右下に今開いているファイルの文字コードや改行コード(CRLF)が表示されています。

改行コードをクリックすると、ウインドウの上部に改行コードの選択と表示されて、LFとCRLFを選択できるようになります。そこでLFを選択し、保存します。

見た目は何もかわっていないのですが、git diffで確認すると、全ての行が変更されているのがわかります。

その後、変更を加えても^Mは表示されなくなりました。

まとめ

改行コードの違いについて理解しているものの、^Mが表示されるとなんでこうなった?と軽めのパニック的な感じになってしまいます。Windowsを使っている方と一緒にプロジェクトに取り組む場合はよく発生すると思います。

今後はこういったことのないようにこの記事を思い出して対応しようと思います。

ジェネレータで特定のディレクトリ配下にcontrollerを作成する

背景

APIの実装する場合など、pathにプレフィックスがつくことが多い(apiv1など)。その場合に、rails g controllerでそのディレクトリ配下にコントローラーを作成する方法がわからなかかったので調べてみました。

パスの指定方法

作成方法は2通りあるようです。
(今回は、app/controllers/api/v1/配下にtests_controller.rb作ることとします。)

/で区切るパターン

1
$ rails g controller api/v1/tests

namespace(::)で区切るパターン

1
$ rails g controller api::v1::tests

不要なファイルを生成しないための方法

今回はAPIなので、viewファイルなどは不要でした。なので、generatorのオプションでviewファイルを生成しないようにできないかを調べてみました。helpを見てみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Usage:
rails generate controller NAME [action action] [options]

Options:
[--skip-namespace], [--no-skip-namespace] # Skip namespace (affects only isolated engines)
[--skip-collision-check], [--no-skip-collision-check] # Skip collision check
[--skip-routes], [--no-skip-routes] # Don't add routes to config/routes.rb.
[--helper], [--no-helper] # Indicates when to generate helper
# Default: true
-e, [--template-engine=NAME] # Template engine to be invoked
# Default: erb
-t, [--test-framework=NAME] # Test framework to be invoked
# Default: rspec

Rspec options:
[--request-specs], [--no-request-specs] # Generate request specs
# Default: true
[--controller-specs], [--no-controller-specs] # Generate controller specs
[--view-specs], [--no-view-specs] # Generate view specs
[--routing-specs], [--no-routing-specs] # Generate routing specs
[--helper-specs], [--no-helper-specs] # Indicates when to generate helper specs

Runtime options:
-f, [--force] # Overwrite files that already exist
-p, [--pretend], [--no-pretend] # Run but do not make any changes
-q, [--quiet], [--no-quiet] # Suppress status output
-s, [--skip], [--no-skip] # Skip files that already exist

Description:
Generates a new controller and its views. Pass the controller name, either
CamelCased or under_scored, and a list of views as arguments.

To create a controller within a module, specify the controller name as a
path like 'parent_module/controller_name'.

This generates a controller class in app/controllers and invokes helper,
template engine, assets, and test framework generators.

Example:
`bin/rails generate controller CreditCards open debit credit close`

CreditCards controller with URLs like /credit_cards/debit.
Controller: app/controllers/credit_cards_controller.rb
Test: test/controllers/credit_cards_controller_test.rb
Views: app/views/credit_cards/debit.html.erb [...]
Helper: app/helpers/credit_cards_helper.rb

今回は--no-helper--no-request-specsを指定してみます。

1
2
3
4
5
$ rails g controller api/v1/tests --no-helper --no-request-specs
create app/controllers/api/v1/tests_controller.rb
invoke erb
create app/views/api/v1/tests
invoke rspec

APIなのでviewsディレクトリも不要なのですが、ジェネレータのオプションで生成を制御できないようだったので、生成後削除しました。

まとめ

今回はジェネレータを使って特定のディレクトリ配下にcontrollerを作成する方法について調べてみました。
ヘルプを見ればわかる話でしたが、2通りの方法があることがわかりました。
また、ジェネレータ実行時のオプションについても調べました。

ジェネレータは便利ですし、カスタムジェネレータの作成もできます。
いろいろ便利なので、深く掘って理解したいと思います。

letter_openerの設定

ローカル開発環境でメール受信

ローカル開発環境でのメール受信はletter_openerを使います。プロジェクトの初期設定で必須になると思いますが、忘れがちなのでメモします。

gemの追加

letter_opener_webというgemがあるので追加します。Gemfileを編集します。

1
2
3
4
group :development do
gem "letter_opener_web"
end
(その他のgemは省略しています)

bundle installします

1
$ docker compose run --rm web ./bin/bundle install

インストールができました。GemfileとGemfile.lockをcommitしておきます。

routesの設定

http://localhost:3000/letter_openerでアクセスできるようconfig/routes.rbに以下の行を追加します

1
2
3
Rails.application.routes.draw do
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
end

ActionMailerの設定

最後にActionMailerの設定をします。config/environments/development.rbに以下の2行を追加します。

1
2
3
config.action_mailer.default_url_options = { host: "localhost:3000"}

config.action_mailer.delivery_method = :letter_opener_web

config.action_mailerが記載されている付近に追加します。

1行目の設定はこの設定がないと、メールにURLを記載する場合のドメインがわからずエラーになってしまいます。

2行目の設定は、letter_opener_webでメールを送るという設定で、この設定がないとhttp://localhost:3000/letter_openerにアクセスしてもメールが見れません(メール送信のログが表示されて送られているように見えるがどこにも届かない)

まとめ

Railsの開発環境構築はいろいろ設定することが多いですが、設定する機会が少なく忘れてしまいます。出だしで躓かないようメモしておきました。

annotateの設定

テーブルのスキーマがわからなくなるときの対応

Railsで開発を行なっているときに、まだ馴染みのないモデルに遭遇すると、どういったカラムがあるかなど、スキーマがわからずに困ることがよくあります。

db/schema.rbを確認すればわかるのですが、モデルのファイルを見ながらdb/schema.rbを見るのは不便です。

何かしらの方法でテーブルのスキーマを確認しつつモデル内でロジックを実装したい、その場合に利用すると便利なgemがannotateです。

gemの追加

annotateというgemがあるので追加します。Gemfileを編集します。

1
2
3
4
group :development do
gem "annotate"
end
(その他のgemは省略しています)

bundle installします

1
2
3
4
$ docker compose run --rm web ./bin/bundle install
...
Fetching annotate 3.2.0
Installing annotate 3.2.0

インストールができました。GemfileとGemfile.lockをcommitしておきます。

annotateの設定ファイルの作成

annotateの設定ファイルを生成するgeneratorがあるので実行します。

1
2
$ docker compose run --rm web ./bin/rails g annotate:install
create lib/tasks/auto_annotate_models.rake

annotateのカスタマイズ

カスタマイズに関しては、先程作成したタスクのAnnotate.set_defaultsメソッドの引数を編集することで行います。

わたしが変更した内容を記載します。

position_in_xxxx

xxxxにはclassfactoryなどが入ります。それぞれのファイルのどの部分にannotateによるドキュメントを追加するか、という設定になります。
コードの後ろに追加した方が見通しがいいかなと思い、afterに変更しています(デフォルトはbefore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
       'routes'                      => 'false',
'models' => 'true',
- 'position_in_routes' => 'before',
- 'position_in_class' => 'before',
- 'position_in_test' => 'before',
- 'position_in_fixture' => 'before',
- 'position_in_factory' => 'before',
- 'position_in_serializer' => 'before',
+ 'position_in_routes' => 'after',
+ 'position_in_class' => 'after',
+ 'position_in_test' => 'after',
+ 'position_in_fixture' => 'after',
+ 'position_in_factory' => 'after',
+ 'position_in_serializer' => 'after',
'show_foreign_keys' => 'true',

with_comment

trueだとカラム名の横にそのカラムに設定されているコメントが追加されます。ですが、カラムを削除する際に、annotateが正しく動作しなかったので、falseに設定します。

1
2
3
4
5
6
       'wrapper_open'                => nil,
'wrapper_close' => nil,
- 'with_comment' => 'true'
+ 'with_comment' => 'false'
)
end

動作確認

ここで正しく動作するか確認します。

1
2
3
$ docker compose run --rm web ./bin/bundle exec annotate

Annotated (ファイル数): ファイル名, ....

このように表示されればOKです。ファイル数にはコメントが追加されたファイル数、ファイル名にはコメントが追加されたファイルのパスが表示されます。

まとめ

annotateはいつも使っている便利なgemです。慣れている人なら馴染み深いものではありますが、始めたばかりの人にとっては知らないけど知っているととても便利なものという感じかと思います。ぜひ使ってみてください。

参考URL

RSpecの初期設定

RSpecの初期設定

RailsのプロジェクトでテストフレームワークはRSpecを使っています。

プロジェクト開始時に設定するのですが、あまり設定することもないので、毎回調べて時間がかかってしまいます。なので、備忘録として手順を記載します。

gemの追加

まずgemを追加します。FactoryBotも利用するのでついでに追加します。

1
2
3
4
group :development, :test do
gem "rspec-rails"
gem "factory_bot_rails"
end

bundle installします

1
$ docker compose run --rm web ./bin/bundle install

インストールができました。GemfileとGemfile.lockをcommitしておきます。

RSpecの設定

RSpecの設定ファイル等を生成するgeneratorがあるので実行します。

1
2
3
4
5
$ docker compose run --rm web ./bin/rails g rspec:install
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb

出力のカスタマイズ

出力をドキュメント形式にするために、.rspecに以下の行を追加します。

1
2
3
$ vi .rspec
--require spec_helper
--format documentation

(2行目を追加)

動作確認

ここで正しく動作するか確認します。

1
2
3
4
5
$ docker compose run --rm web bundle exec rspec
No examples found.

Finished in 0.00087 seconds (files took 0.171 seconds to load)
0 examples, 0 failures

このように表示されればOKです。

binstubを作成する

毎回bundle exec rspecとタイプするのは面倒なので、binstubを作成して./bin/rspecとシェルの補完機能も使いながらタイプ数を減らしたいと思います。

1
$ docker compose run --rm web bundle binstubs rspec-core

bin/rspecというファイルが作成されました。

今度はbinstubを利用して実行してみます。

1
2
3
4
5
$ docker compose run --rm web ./bin/rspec 
No examples found.

Finished in 0.0012 seconds (files took 0.15613 seconds to load)
0 examples, 0 failures

同じように実行できました。

RSpecを利用するよう設定

generatorで自動生成するファイルをRSpecのファイルにしたいので、利用するテストフレームワークがRSpecだということをRailsに伝えます。

1
2
3
4
5
6
7
8
9
$ vi config/application.rb
...(省略)
config.generators do |g|
g.test_framework :rspec,
view_specs: false,
helper_specs: false,
routing_specs: false
end
...(省略)

上記のコードを追加することで、generatorがRSpecのファイルを生成してくれます。

まとめ

今後もテストフレームワークはRSpecを利用していくと思うので、導入手順をまとめました。

参考図書にリンクを記載したeveryday-railsですが、著者の伊藤さんが日本語版独自の改訂を続けてくれています。一度購入するとその後の改訂版は無料で受け取ることができます。とてもためになる書籍なので読んでみることを強くおすすめします。

参考図書

docker compose buildでpermission deniedが出た時の対応

docker compose buildでエラー発生

久しぶりにDockerfileを編集したので、イメージをビルドしようと思い、以下のコマンドを実行しました

1
$ docker compose build web

(docker-compose.yml上でwebというサービスが設定されている前提です)

何もなければこのまま終わるはずですが、エラーが発生して実行できてないようです。

1
2
$ docker compose build web
open /User/user/.docker/buildx/current: permission denied

対処法

いろいろ調べたのですが、手軽な対処法がありました。環境変数を設定し、buildxを利用しないようにするというものです。docker compose buildを実行するターミナルで以下のコマンドを実行します。

1
$ export DOCKER_BUILDKIT=0

docker compose buildを試します。

1
2
$ docker compose build web
buildが実行できるログ

まとめ

buildxはDocker Desktopを利用していると自動的に有効になるようです。原因がわからない場合は一旦buildxを無効にするのが簡単な対応方法です。

参考図書

host.docker.internalとは?

ローカル開発環境かどうかの判定

普段Railsを使って開発していると、環境の判定はRails.envで行うことができる。ローカル開発環境ではもちろん

1
2
3
irb(main):001:0> Rails.env
=> "development"
irb(main):002:0>

となる。

また、Rails.env == "development"としなくても

1
2
3
4
irb(main):002:0> Rails.env.development?
=> true
irb(main):003:0> Rails.env.production?
=> false

このように判定してくれるメソッドがある。

ところが、そうでないWebアプリケーションフレームワークもあるようで(ほんとにその機能がないかは定かではない)、ローカル開発環境かどうかの判定に

1
if 'host.docker.internal' in os.environ['S3_URL']:

という条件分岐を利用しているコードに出会った。

察しのいい方はわかると思いますが、S3にアクセスする際に、ローカルで稼働しているMinIOコンテナにアクセスすべきか、S3にアクセスすべきかの条件分岐のif文です。

この条件文が正しく動作せず苦労しました。

host.docker.internalについて

host.docker.internalというドメインが初見だったので調べてみると、Docker Desktop for xxで利用できる、コンテナから参照する場合のhostを指すドメインだということです。わたしの環境はDocker Desktop for Macを利用しているので利用できるはずです。

参考: コンテナからホスト上のサービスに対して接続したい

なるほど、であれば、コンテナからアクセスする際にhost.docker.internal:9000にアクセスすれば、コンテナからMinIOへアクセスできるはずですね。

host.docker.internalを利用する

先程のコードのようにS3_URLをhttp://host.docker.internal:9000とし、アプリケーションからファイルをPUTしてみました。

すると、正しくファイルをPUTできました。

host.docker.internalにアクセスできない場合

いろいろ調べてみるとhost.docker.internalというドメインにアクセスできないケースがあるようです。

その場合の対処法としては、docker-compose.ymlにextra_hostsの設定を行います。

extra_hosts

extra_hostsの説明は以下にありました。

https://docs.docker.jp/compose/compose-file/#extra-hosts

/etc/hostsに対してホストのマッピングを追加することができるようです。

対処法の例としては、hosts.docker.internalにアクセスするコンテナに対して

1
2
extra_hosts:
- "host.docker.internal:host-gateway"

を追加してくださいとのことです。host-gatewayはホストのアドレスになるようですね。

/etc/hostsの確認

extra_hostsを設定した状態でコンテナの/etc/hostsファイルを確認します

1
2
3
4
5
6
7
8
9
10
$ docker compose run --rm app /bin/bash
bash-4.2# cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
192.168.65.2 host.docker.internal
172.18.0.6 af51d5a15783

host.docker.internalのレコードが追加されていました。

まとめ

コンテナからホストにアクセスする際にはhost.docker.internalというドメインでアクセスできる。

host.docker.internalでアクセスできない場合は、extra_hostsを利用して明示的に/etc/hostsファイルにレコードを追加する。

参考図書