choron_support 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +1 -1
- data/Makefile +4 -1
- data/README.md +35 -72
- data/docs/idea.md +24 -0
- data/docs/props.md +235 -0
- data/lib/choron_support/as_props.rb +4 -3
- data/lib/choron_support/props/attributes.rb +381 -0
- data/lib/choron_support/props/base.rb +26 -3
- data/lib/choron_support/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 650ed706fe5c243363f5a83fc8029023897c19a2cfc6b8973abcd85a8ba8b28e
|
4
|
+
data.tar.gz: 96ee27b7f7ac4de9f717ad2ea8ec5076f2d08462f440bc6107175fc6fe1dd9fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ae8dba1af0adfa9dac9adc55056dedcb7c9fc191ed2cbc3d2650eb6d992f1b3974f1d6138f76fea7a5fc616c51220782451abaa85aa12d01f8f7828b1ea0831
|
7
|
+
data.tar.gz: eb17113982a53b1ae1b888ad798ab22433bfb8b4e603cf0aaee767f51fe4e3bdc872cad18f178350bb5b8aa7f370fed6e5a13bec83c344802b09980b518f0113
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
data/Makefile
CHANGED
@@ -12,7 +12,7 @@ run:
|
|
12
12
|
docker-compose up
|
13
13
|
|
14
14
|
web:
|
15
|
-
eval "docker exec -it `docker ps | grep
|
15
|
+
eval "docker exec -it `docker ps | grep choron_support-web- | cut -d' ' -f1` /bin/bash"
|
16
16
|
|
17
17
|
clean:
|
18
18
|
docker system prune
|
@@ -30,5 +30,8 @@ d-build-no-cache:
|
|
30
30
|
install:
|
31
31
|
docker-compose run web bash -c "gem uninstall bundler && gem install bundler -v 2.3.13 && bundle install && yarn install"
|
32
32
|
|
33
|
+
spec-db-create:
|
34
|
+
DB_CREATE=true bundle exec rspec spec/*
|
35
|
+
|
33
36
|
spec-table-create:
|
34
37
|
bundle exec ridgepole --config ./spec/rails/config/database.yml --file ./spec/rails/config/schemafile --apply
|
data/README.md
CHANGED
@@ -27,50 +27,11 @@ ChoronSupport.using :all
|
|
27
27
|
|
28
28
|
### AsProps
|
29
29
|
|
30
|
-
|
30
|
+
モデルをJSON(キーがローキャメルケース)に変換するための仕組みです。
|
31
31
|
|
32
|
-
|
33
|
-
|
32
|
+
* 詳細な使い方は [テストファイル](./spec/choron_support/as_props_spec.rb) を参照ください。
|
33
|
+
* 詳細な実装は [こちら](./lib/choron_support/as_props.rb) です。
|
34
34
|
|
35
|
-
#### 使い方
|
36
|
-
|
37
|
-
* ActiveRecord
|
38
|
-
|
39
|
-
```ruby
|
40
|
-
class User < ApplicationRecord
|
41
|
-
include ChoronSupport::AsProps
|
42
|
-
# id: bigint
|
43
|
-
# name: string
|
44
|
-
end
|
45
|
-
|
46
|
-
# ActiveRecordから利用
|
47
|
-
User.new.as_props
|
48
|
-
#=> { id: nil, name: nil }
|
49
|
-
|
50
|
-
User.create(id: 1, name: "tarou")
|
51
|
-
|
52
|
-
User.find(1).as_props
|
53
|
-
#=> { id: 1, name: "tarou" }
|
54
|
-
|
55
|
-
# ActiveRecord::Relationからでも利用できます
|
56
|
-
users = User.all.as_props
|
57
|
-
#=> [
|
58
|
-
# { id: 1, name: "tarou" },
|
59
|
-
# ]
|
60
|
-
|
61
|
-
class Props::User < ChoronSupport::Props::Base
|
62
|
-
def as_props
|
63
|
-
model
|
64
|
-
.as_json
|
65
|
-
.merge(
|
66
|
-
name: "tanaka #{model.name}"
|
67
|
-
)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
user = User.find(1).as_props
|
72
|
-
#=> { id: 1, name: "tanaka tarou" }
|
73
|
-
```
|
74
35
|
|
75
36
|
### Mask
|
76
37
|
|
@@ -81,15 +42,27 @@ user = User.find(1).as_props
|
|
81
42
|
|
82
43
|
### Domain
|
83
44
|
|
84
|
-
|
45
|
+
モデルの処理をメソッド単位で別クラスに委譲するための仕組みです。
|
46
|
+
クラスメソッド、インスタンスメソッドの両方で利用できます。
|
47
|
+
|
48
|
+
* 詳細な実装と使い方は [こちら](./lib/choron_support/domain_delegate.rb) を確認してください。
|
85
49
|
|
86
50
|
### Forms
|
87
51
|
|
88
|
-
|
52
|
+
ControllerでFormクラスのインスタンスを簡単に生成するための仕組みです。
|
53
|
+
|
54
|
+
* 詳細な実装と使い方はいかを参照してください。
|
55
|
+
* [build_form](./lib/choron_support/build_form.rb)
|
56
|
+
* ControllerからFormクラスのインスタンスを簡単に生成するメソッドです
|
57
|
+
* [ChoronSupport::Forms::Base](lib/choron_support/forms/base.rb)
|
58
|
+
* Formクラスのベースとなるクラスです
|
89
59
|
|
90
60
|
### Query
|
91
61
|
|
92
|
-
|
62
|
+
モデルのscope処理を別クラスに異常するための仕組みです。
|
63
|
+
もともと存在する `queryパターン` を簡単に使えるようにしたものです。
|
64
|
+
|
65
|
+
* 詳細な実装と使い方は [こちら](./lib/choron_support/scope_query.rb) を確認してください。
|
93
66
|
|
94
67
|
## Develop
|
95
68
|
|
@@ -101,46 +74,36 @@ Dockerを起動することで開発環境が整います
|
|
101
74
|
make d-build
|
102
75
|
```
|
103
76
|
|
104
|
-
* Docker
|
77
|
+
* Dockerコンテナの起動
|
105
78
|
|
106
79
|
```bash
|
107
80
|
make run
|
108
81
|
```
|
109
82
|
|
110
|
-
*
|
111
|
-
* spec/spec_helper.rb を開いて下記にあるDBの作成/Tableの作成のフラグを true に書き換えてから、テストを実行してください
|
112
|
-
* `bin/rspec spec`
|
113
|
-
|
83
|
+
* コンテナ内部に入る
|
114
84
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
その中で本Gemは以下の思想をサポートするようにしています
|
119
|
-
|
120
|
-
* レイヤーを多く作らずにModelへ処理を凝集する
|
121
|
-
* Railsがデフォルトで用意してくれている `controllers`, `models`, `views` といったレイヤーのみをできるだけ使い、独自のレイヤーを**あまり**追加しない
|
122
|
-
* Modelの見通しをよくするためにファイル内の処理を委譲させる
|
123
|
-
* 委譲先のクラスはModel以外からは直接呼び出さない(必ずModelにpublicなメソッドを経由させる)
|
124
|
-
|
125
|
-
これによりドメインの知識をModelレイヤーに集めつつ、
|
126
|
-
中規模以上のシステムになってきた際のファットモデルによる問題を解消する取り組みを行います
|
127
|
-
|
128
|
-
### 具体的な取り組み
|
129
|
-
|
130
|
-
Modelの中で行われる処理の中でも、本Gemは以下の処理を簡易に別クラスへ委譲させます
|
85
|
+
```bash
|
86
|
+
make web
|
87
|
+
```
|
131
88
|
|
132
|
-
*
|
133
|
-
* DBへのアクセス・取得
|
134
|
-
* データを表示するための加工(json化)
|
89
|
+
* テスト用のDBおよびテーブルの作成
|
135
90
|
|
91
|
+
※Dockerコンテナ内部で実行してくださ
|
136
92
|
|
137
|
-
|
93
|
+
```bash
|
94
|
+
make spec-db-create
|
95
|
+
make spec-table-create
|
96
|
+
```
|
138
97
|
|
139
|
-
|
98
|
+
* RSpecの実行
|
140
99
|
|
141
|
-
|
100
|
+
```bash
|
101
|
+
bin/rspec spec
|
102
|
+
```
|
142
103
|
|
104
|
+
## 本Gemの思想
|
143
105
|
|
106
|
+
* [こちら](docs/idea.md)を参照ください
|
144
107
|
|
145
108
|
## License
|
146
109
|
|
data/docs/idea.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# 本Gemの思想
|
2
|
+
|
3
|
+
(記載中...)
|
4
|
+
|
5
|
+
## 概要
|
6
|
+
|
7
|
+
Railsにはこれまで多くのリファクタリング手法が、多くの人々から提案されてきました。
|
8
|
+
その中で本Gemは以下の思想をサポートするようにしています
|
9
|
+
|
10
|
+
* レイヤーを多く作らずにModelへ処理を凝集する
|
11
|
+
* Railsがデフォルトで用意してくれている `controllers`, `models`, `views` といったレイヤーのみをできるだけ使い、独自のレイヤーを**あまり**追加しない
|
12
|
+
* Modelの見通しをよくするためにファイル内の処理を委譲させる
|
13
|
+
* 委譲先のクラスはModel以外からは直接呼び出さない(必ずModelにpublicなメソッドを経由させる)
|
14
|
+
|
15
|
+
これによりドメインの知識をModelレイヤーに集めつつ、
|
16
|
+
中規模以上のシステムになってきた際のファットモデルによる問題を解消する取り組みを行います
|
17
|
+
|
18
|
+
### 具体的な取り組み
|
19
|
+
|
20
|
+
Modelの中で行われる処理の中でも、本Gemは以下の処理を簡易に別クラスへ委譲させます
|
21
|
+
|
22
|
+
* ビジネスロジック・ドメインロジック
|
23
|
+
* DBへのアクセス・取得
|
24
|
+
* データを表示するための加工(json化)
|
data/docs/props.md
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
# Propsについて
|
2
|
+
|
3
|
+
## 概要
|
4
|
+
Propsは一言で言うとローキャメルケースのキーを持つJSON変換用のHashです。
|
5
|
+
|
6
|
+
Choronでは画面側の処理をReact + Typescrip の組み合わせで実現しています。
|
7
|
+
このときに、React側のProps(このpropsはReactの世界のpropsです)に、モデルの値や値クラスの値をJSON形式で渡す必要があります。
|
8
|
+
|
9
|
+
本モジュールのPropsは上記のReactへの値の受け渡しをサポートします。
|
10
|
+
|
11
|
+
一般的なJSON化ツールの違いとしては、Javascript側の記法・慣習を優先するため、
|
12
|
+
JSONのキーをローキャメルケース(fullName, isAdult,など)に自動変換します
|
13
|
+
|
14
|
+
## Choronでの使い方
|
15
|
+
|
16
|
+
### 基本的な使い方
|
17
|
+
|
18
|
+
ChoronにはChoronSupportにある
|
19
|
+
* ChoronSupport::Props::Base
|
20
|
+
* ChoronSupport::Props::Attributes
|
21
|
+
の2つのクラス・モジュールを継承&includedしたProps用の基底クラスを用意しています。
|
22
|
+
|
23
|
+
```app/models/props/base.rb
|
24
|
+
class Props::Base < ChoronSupport::Props::Base
|
25
|
+
include ChoronSupport::Props::Attributes
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
そして以下の命名ルールにより、モデルごとのPropsクラスを作成しています
|
30
|
+
* app/models/props/${モデル名}.rb
|
31
|
+
* 例: app/models/props/user.rb
|
32
|
+
これは ChoronSupport::AsProps モジュールがモデル名から自動で props/**/*.rb を探し出してインスタンスを生成してくれる処理に由来します。
|
33
|
+
|
34
|
+
```app/models/props/user.rb
|
35
|
+
class Props::Foo < Props::Base
|
36
|
+
attributes :id, :name, :full_name
|
37
|
+
attributes :full_name, to: :self
|
38
|
+
def full_name
|
39
|
+
"#{model.first_name} #{model.last_name}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
これで準備は完了です。
|
45
|
+
以下のようにController等で利用ができます
|
46
|
+
|
47
|
+
```app/controllers/users_controller.rb
|
48
|
+
class UsersController < ApplicationController
|
49
|
+
def index
|
50
|
+
users = User.all
|
51
|
+
props = {
|
52
|
+
users: users.as_props
|
53
|
+
}
|
54
|
+
|
55
|
+
render react_file(props: props)
|
56
|
+
end
|
57
|
+
|
58
|
+
def show
|
59
|
+
user = User.find(params[:id])
|
60
|
+
props = {
|
61
|
+
user: user.as_props
|
62
|
+
}
|
63
|
+
|
64
|
+
render react_file(props: props)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
サンプルコードからわかるように `#as_props` というメソッドはモデルおよびRelationの両方で利用が可能なように拡張をしています。
|
70
|
+
|
71
|
+
この拡張を使うためには ChoronSupport::AsProps モジュールを ApplicationRecord にinclude する必要があります。
|
72
|
+
|
73
|
+
Choronではすでに実施済です。
|
74
|
+
|
75
|
+
```app/models/application_record.rb
|
76
|
+
class ApplicationRecord < ActiveRecord::Base
|
77
|
+
include ChoronSupport::AsProps
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
もしPropsクラスをわざわざ作らず、モデルのカラムをそのまま全てローキャメルケースで出したい時もあると思います。そのときは
|
82
|
+
|
83
|
+
```
|
84
|
+
user.as_props(:model)
|
85
|
+
```
|
86
|
+
|
87
|
+
と第一引数で `:model` を渡すことで、カラム全てをprops化します。
|
88
|
+
※これは個人情報など思わぬ値も出力してしまう可能性もあるため、安全な処理にだけ使うことを推奨します
|
89
|
+
|
90
|
+
また、キーワード引数を使うことでパラメーターを渡すことも可能です。
|
91
|
+
これを利用すれば、Props側を利用する側から細かいPropsの出力調整ができます。
|
92
|
+
|
93
|
+
```app/models/props/foo.rb
|
94
|
+
class Props::Foo < Props::Base
|
95
|
+
attributes :id, :name
|
96
|
+
attributes :full_name, if: :show_name?
|
97
|
+
def show_name?
|
98
|
+
# #params で as_props 実行時のキーワード引数にアクセスができる
|
99
|
+
params[:show_name].present?
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
これは以下のように利用できます
|
105
|
+
|
106
|
+
```
|
107
|
+
foo = Foo.find(1)
|
108
|
+
foo.as_props(show_name: true)
|
109
|
+
```
|
110
|
+
|
111
|
+
また、この `DSL` を使うことで以下の情報がメタ情報として自動的に出力されます。
|
112
|
+
* type: Props化を行なったクラスの名前(名前空間あり)
|
113
|
+
* modelName: Props化を行なったモデルの名前(名前空間なし)
|
114
|
+
|
115
|
+
```app/models/props/foos/bar.rb
|
116
|
+
class Props::Foos::Bar < Props::Base
|
117
|
+
attributes :id, :name
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
Foos::Bar.new.as_props
|
122
|
+
#=> { id: x, name: "xxx", type: "Foos::Bar", modelName: "Bar" }
|
123
|
+
|
124
|
+
もし `RAILS_ENV=test` のときはさらに Props化を行なったクラスもメタ情報として付与されます。
|
125
|
+
|
126
|
+
Foos::Bar.new.as_props
|
127
|
+
#=> { id: x, name: "xxx", type: "Foos::Bar", modelName: "Bar", propsClassName: "Props::Foos::Bar" }
|
128
|
+
|
129
|
+
このメタ情報を使うことでテストの簡略化も可能となります。
|
130
|
+
|
131
|
+
例えば Controller のテストを以下のように書くことができます。
|
132
|
+
|
133
|
+
```spec/requests/foos_controller_spec.rb
|
134
|
+
describe FoosController, type: :request do
|
135
|
+
describe "GET /foos/:id" do
|
136
|
+
let!(:id) { foo.id }
|
137
|
+
let!(:foo) { create(:foo) }
|
138
|
+
it "詳細画面が表示されること" do
|
139
|
+
is_expected.to eq 200
|
140
|
+
|
141
|
+
react = rendered_react("foo/show")
|
142
|
+
props = react.props
|
143
|
+
# 細かい値の設定はProps側の単体テストで担保しているため、ここでは使われているPropsのみ検証する
|
144
|
+
expect(props[:foo][:propsClassName]).to eq "Props::Foos::Bar"
|
145
|
+
# Choron では専用のマッチャーがあるため以下のように記載も可能
|
146
|
+
expect(props[:foo]).to be_use_props(Props::Foos::Bar)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
### DSLの細かい使い方
|
153
|
+
* ChoronSupport::Props::Attributesのソースコードを参照ください
|
154
|
+
* https://github.com/mksava/choron_support/blob/main/lib/choron_support/props/attributes.rb
|
155
|
+
|
156
|
+
### Propsの設計思想
|
157
|
+
ChoronでのPropsはattributesのDSLでif文の指定が可能です。
|
158
|
+
そうでなくてもpropsメソッドをオーバーライドすることでさらに細かい調整が可能です。
|
159
|
+
|
160
|
+
しかしPropsの設計思想は「1つのPropsで複数のパターンのJSONを作成する」よりも「複数のパターンがある分、Propsクラスを作成する」にあります。
|
161
|
+
|
162
|
+
たとえば「User」には個人情報が含まれいるため、Propsの出力を制御したいときは
|
163
|
+
|
164
|
+
* 一般的な利用
|
165
|
+
* `app/models/props/user.rb`
|
166
|
+
* スタッフなど個人情報にアクセス可能なユーザからの利用
|
167
|
+
* `app/models/props/users/staff.rb`
|
168
|
+
* ログインしているユーザ自身が自分自身の情報を見たいときに利用
|
169
|
+
* `app/models/props/users/current.rb`
|
170
|
+
|
171
|
+
というようにPropsクラスを複数作成することを検討してください。
|
172
|
+
このとき、各Propsクラスは以下のように `#as_props` の第一引数を指定することで利用できます
|
173
|
+
|
174
|
+
```
|
175
|
+
# app/models/props/users/general.rb
|
176
|
+
users = Users.all.as_props(:general)
|
177
|
+
# app/models/props/users/staff.rb
|
178
|
+
users = Users.all.as_props(:staff)
|
179
|
+
# app/models/props/users/current.rb
|
180
|
+
users = Users.all.as_props(:current)
|
181
|
+
```
|
182
|
+
|
183
|
+
## サンプルコード
|
184
|
+
|
185
|
+
* Props を作成するときはこのサンプルコードを優先的に参考にしてください
|
186
|
+
* Sample の部分は適宜作成したいモデル名に変更してください
|
187
|
+
|
188
|
+
### ActiveRecord
|
189
|
+
|
190
|
+
```app/models/sample.rb
|
191
|
+
class Sample < ApplicationRecord
|
192
|
+
end
|
193
|
+
```
|
194
|
+
|
195
|
+
```app/controllers/samples_controller.rb
|
196
|
+
class SamplesController < ApplicationController
|
197
|
+
def index
|
198
|
+
samples = Sample.all
|
199
|
+
props = {
|
200
|
+
samples: samples.as_props(:general)
|
201
|
+
}
|
202
|
+
|
203
|
+
render react_file(props: props)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
```app/models/props/sample.rb
|
209
|
+
class Props::Sample < Props::Base
|
210
|
+
attributes :id, :name, :created_at, :updated_at
|
211
|
+
attributes :is_sample?, to: :self
|
212
|
+
|
213
|
+
def is_sample?
|
214
|
+
true
|
215
|
+
end
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
219
|
+
```app/models/props/samples/general.rb
|
220
|
+
class Props::Samples::Genral < Props::Base
|
221
|
+
attributes :id, :created_at, :updated_at
|
222
|
+
attributes :is_sample?, to: :self
|
223
|
+
|
224
|
+
def is_sample?
|
225
|
+
false
|
226
|
+
end
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
```app/models/props/samples/staff.rb
|
231
|
+
class Props::Samples::Staff < Props::Base
|
232
|
+
self.union = :default
|
233
|
+
attributes :salary
|
234
|
+
end
|
235
|
+
```
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative "props/base"
|
2
|
+
require_relative "props/attributes"
|
2
3
|
require_relative "props/ext/relation"
|
3
4
|
require_relative "props/ext/hash"
|
4
5
|
module ChoronSupport
|
@@ -8,7 +9,7 @@ module ChoronSupport
|
|
8
9
|
# @param [Hash] params その他のパラメータ。camel: false を指定すると自動でキャメライズしない。
|
9
10
|
# @return [Hash]
|
10
11
|
def as_props(type_symbol = nil, **params)
|
11
|
-
serializer = self.__get_props_class(type_symbol,
|
12
|
+
serializer = self.__get_props_class(type_symbol, params)
|
12
13
|
|
13
14
|
skip_camel = (params[:camel] == false)
|
14
15
|
pass_params = params.except(:camel)
|
@@ -21,7 +22,7 @@ module ChoronSupport
|
|
21
22
|
|
22
23
|
private
|
23
24
|
|
24
|
-
def __get_props_class(type_symbol,
|
25
|
+
def __get_props_class(type_symbol, params)
|
25
26
|
case type_symbol
|
26
27
|
when Symbol, String
|
27
28
|
# 名前空間の例: Serialize::Users
|
@@ -43,7 +44,7 @@ module ChoronSupport
|
|
43
44
|
begin
|
44
45
|
props_class = props_class_name.constantize
|
45
46
|
|
46
|
-
props_class.new(self)
|
47
|
+
props_class.new(self, params)
|
47
48
|
rescue *rescue_errors
|
48
49
|
# もしmodelを指定しているときはnilを返し、as_jsonを利用させる
|
49
50
|
if type_symbol == :model
|
@@ -0,0 +1,381 @@
|
|
1
|
+
# 本クラス は include 先のクラスに対してキャメルケースのJSONを設定するためのDSLやメソッドを提供するモジュールです
|
2
|
+
# このモジュールをincludeしたクラスは以下のような形で使うことを想定しています
|
3
|
+
# @examle
|
4
|
+
# [app/models/values/money.rb]
|
5
|
+
# class Values::Money
|
6
|
+
# include ChoronSupport::Props::Attributes
|
7
|
+
# attributes :amount, :amount_with_unit, to: :self
|
8
|
+
# def initialize(amount)
|
9
|
+
# @amount = amount.to_i
|
10
|
+
# end
|
11
|
+
# def amount
|
12
|
+
# @amount
|
13
|
+
# end
|
14
|
+
# def amount_with_unit
|
15
|
+
# "#{amount}円"
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
# Choronではデフォルトで以下のようなBaseクラスが作成されて、読み込みがされています
|
19
|
+
# @example
|
20
|
+
# class Props::Base < ChoronSupport::Props::Base
|
21
|
+
# include ChoronSupport::Props::Attributes
|
22
|
+
# end
|
23
|
+
# [使い方]
|
24
|
+
# @example 最も簡単な例
|
25
|
+
# [app/models/props/user.rb]
|
26
|
+
# class Props::User < Props::Base
|
27
|
+
# # id, full_name, ageを出力させる
|
28
|
+
# # retult: { id: 1, fullName: "John Smith", age: 20 }
|
29
|
+
# attributes :id, :full_name, :age
|
30
|
+
# end
|
31
|
+
# @example メソッドのデリゲート先を指定する
|
32
|
+
# [app/models/props/user.rb]
|
33
|
+
# class Props::User < Props::Base
|
34
|
+
# # result: { fullName: "John Smith" }
|
35
|
+
# attributes :full_name, to: :self
|
36
|
+
# def full_name
|
37
|
+
# "#{model.first_name} #{model.last_name}"
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
# @example 関連先のModelのPropsを結合する
|
41
|
+
# class Props::User < Props::Base
|
42
|
+
# # result: { posts: posts.as_props } => { posts: [{ id: 1, title: "foo" }, { id: 2, title: "bar" }] }
|
43
|
+
# relation :posts
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# これらの各種DSLは複数同時に設定することもできます
|
47
|
+
# @example 複数設定
|
48
|
+
# [app/models/props/user.rb]
|
49
|
+
# class Props::User < Props::Base
|
50
|
+
# # id, full_name, ageを出力させる
|
51
|
+
# attributes :id, :age
|
52
|
+
# attributes :full_name, to: :self
|
53
|
+
# relation :posts
|
54
|
+
# def full_name
|
55
|
+
# "#{model.first_name} #{model.last_name}"
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# default値の設定やifオプションを渡して出力有無を動的に変更もできます
|
60
|
+
# @example default, ifの設定
|
61
|
+
# [app/models/props/user.rb]
|
62
|
+
# class Props::User < Props::Base
|
63
|
+
# # age が nil のときは 0 を出力する
|
64
|
+
# # age が 20 以上のときのみ出力する
|
65
|
+
# attributes :age, default: 0, if: :show_age?
|
66
|
+
# def show_age?
|
67
|
+
# model.age >= 20
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
#
|
72
|
+
# 本DSLを利用するときは基本的には attributes を使って設定するのが良いと思います
|
73
|
+
# 細かな使い方モジュールの該当DSL(self.xxxx)の説明を参照してください
|
74
|
+
module ChoronSupport
|
75
|
+
module Props
|
76
|
+
module Attributes
|
77
|
+
FORMATS = {
|
78
|
+
# HTMLのinput type="date"で使える形式
|
79
|
+
date: "%Y-%m-%d",
|
80
|
+
datetime: "%Y-%m-%dT%H:%M",
|
81
|
+
}.freeze
|
82
|
+
# 型のキャスト指定があってもキャストはしないメソッド
|
83
|
+
CAST_IGNORE_METHODS = [
|
84
|
+
# id は数値のほうが良いため
|
85
|
+
:id,
|
86
|
+
].freeze
|
87
|
+
# デフォルト値を設定しない場合に使う値
|
88
|
+
NO_DEFAULT = Object.new.freeze
|
89
|
+
|
90
|
+
extend ActiveSupport::Concern
|
91
|
+
|
92
|
+
included do
|
93
|
+
# 本moduleはActiveSupportが使える環境でのみ動作します
|
94
|
+
unless defined?(ActiveSupport)
|
95
|
+
raise "ActiveSupport is not defined. Please require 'active_support/all' in your Gemfile"
|
96
|
+
end
|
97
|
+
|
98
|
+
# DSLで設定できる設定値たち ==========================
|
99
|
+
# Props作成時に設定されるキーと値を設定します。
|
100
|
+
# この値はDSLを経由して内部的に設定されていきます
|
101
|
+
class_attribute :settings_attributes
|
102
|
+
# Props作成時に自動で付与される元のクラスの文字列を設定しないときはtrueを設定してください
|
103
|
+
# @example
|
104
|
+
# class Props::Foo < Props::Base
|
105
|
+
# self.skip_meta_mark = true
|
106
|
+
# end
|
107
|
+
class_attribute :skip_meta_mark, default: false
|
108
|
+
# 他のPropsクラスの結果を結合するときに結合先のProps識別子を設定してください
|
109
|
+
# @example
|
110
|
+
# class Props::Foos::General < Props::Base
|
111
|
+
# # すべてのカラムを出す
|
112
|
+
# self.union = :model
|
113
|
+
# # as_props の結果を結合する
|
114
|
+
# self.union = :default
|
115
|
+
# # as_props(:secure) の結果を結合する
|
116
|
+
# self.union = :secure
|
117
|
+
# end
|
118
|
+
class_attribute :union, default: nil
|
119
|
+
# ====================================================
|
120
|
+
|
121
|
+
# Props作成時に設定されるキーと値を設定します。
|
122
|
+
# @param [Symbol] key Propsのキーを指定してください
|
123
|
+
# @param [Symbol] method 値を代入するためのメソッドを指定してください。指定がないときはkeyをメソッドとして扱います
|
124
|
+
# @param [Symbol] to メソッドのデリゲート先を指定してください。指定がないときはmodelをデリゲート先として扱います。:selfを指定すると自身をデリゲート先として扱います
|
125
|
+
# @param [Symbol] cast デリゲート先のメソッドの戻り値に対して、さらにメソッドを実行するときは指定してください。
|
126
|
+
# @param [Object] default 値がnilのときに設定されるデフォルト値を指定してください。指定がないときはnilになります。
|
127
|
+
# @param [Proc | Symbol] if その値を出すときの条件。Symbolだとselfに対してsendを実行します
|
128
|
+
def self.attribute(key, method: nil, to: :model, cast: nil, default: NO_DEFAULT, if: nil)
|
129
|
+
self.settings_attributes ||= []
|
130
|
+
self.settings_attributes << { key:, to:, method: (method || key), cast:, default:, if: }
|
131
|
+
end
|
132
|
+
|
133
|
+
# 一度にまとめて複数のattributeを設定します
|
134
|
+
# 基本的なパラメータの説明はattributeを参照してください
|
135
|
+
# @param [Array<Symbol>] methods 設定するキーと値のペアを指定してください
|
136
|
+
# @example
|
137
|
+
# model = User.new(id: 1, name: "John", email: "JOHN@EXAMPLE", age: nil)
|
138
|
+
# class Props::User::Foo < ChoronSupport::Props::Base
|
139
|
+
# attributes :id, :name, :email, :age
|
140
|
+
# #=> { id: 1, name: "John", email: "JOHN@EXAMPLE", age: nil }
|
141
|
+
# end
|
142
|
+
# @note to, cast, defaultなどのパラメータについて
|
143
|
+
# これらの値は全てのメソッド・キーに対して同じ値が設定されます
|
144
|
+
def self.attributes(*methods, to: :model, cast: nil, default: NO_DEFAULT, if: nil)
|
145
|
+
methods.each do |method|
|
146
|
+
attribute(method, method:, to:, cast:, default:, if:)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Modelに対して関連付けされた別ModelのPropsを結合するためのDSLです
|
151
|
+
# to, cast, default, if については attribute と同じです
|
152
|
+
# @param [Symbol] key Propsのキーを指定してください
|
153
|
+
# @param [Symbol] relation 結合するModelのメソッドを指定してください。指定がないときはkeyをメソッドとして扱います
|
154
|
+
# @example
|
155
|
+
# class Props::User < ChoronSupport::Props::Base
|
156
|
+
# relation :posts
|
157
|
+
# #=> { posts: user.posts.as_props } と同じ結果になる
|
158
|
+
# relation :posts, props: :foo_bar
|
159
|
+
# #=> { posts: user.posts.as_props(:foo_bar) } と同じ結果になる
|
160
|
+
# relation :user_posts, relation: :posts
|
161
|
+
# #=> { user_posts: user.posts.as_props } と同じ結果になる
|
162
|
+
# end
|
163
|
+
def self.relation(key, relation: nil, to: :model, cast: nil, default: NO_DEFAULT, props: nil, if: nil)
|
164
|
+
relation ||= key
|
165
|
+
method = lambda { |model|
|
166
|
+
records = model.send(relation)
|
167
|
+
if props
|
168
|
+
records&.as_props(props) || {}
|
169
|
+
else
|
170
|
+
records&.as_props || {}
|
171
|
+
end
|
172
|
+
}
|
173
|
+
self.attribute(
|
174
|
+
key,
|
175
|
+
method:,
|
176
|
+
to:,
|
177
|
+
cast:,
|
178
|
+
default:,
|
179
|
+
if:,
|
180
|
+
)
|
181
|
+
end
|
182
|
+
|
183
|
+
# self.relation の複数同時に設定ができるversionです
|
184
|
+
# 基本は上記と一緒ですが、relationとkeyは同じである必要があります
|
185
|
+
# @example
|
186
|
+
# class Props::User < ChoronSupport::Props::Base
|
187
|
+
# relations :posts, :comments
|
188
|
+
# #=> { posts: user.posts.as_props, comments: user.comments.as_props } と同じ結果になる
|
189
|
+
# end
|
190
|
+
def self.relations(*keys, to: :model, cast: nil, default: NO_DEFAULT, props: nil, if: nil)
|
191
|
+
keys.each do |key|
|
192
|
+
method = lambda { |model|
|
193
|
+
records = model.send(key)
|
194
|
+
if props
|
195
|
+
records&.as_props(props) || {}
|
196
|
+
else
|
197
|
+
records&.as_props || {}
|
198
|
+
end
|
199
|
+
}
|
200
|
+
self.attribute(
|
201
|
+
key,
|
202
|
+
method:,
|
203
|
+
to:,
|
204
|
+
cast:,
|
205
|
+
default:,
|
206
|
+
if:,
|
207
|
+
)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Propsに設定されるキーと値のペアを返します
|
212
|
+
# @param 各種パラメータは self.attribute に合わせているためそちらを参照してください
|
213
|
+
# @return [Hash] 設定されるPropsのキーと値のペア
|
214
|
+
# @note memo
|
215
|
+
# if が予約語のため options として受け取っています。_ifも検討しましたが全体でキーワードの形を合わせたかったためoptionsの形にしています
|
216
|
+
def attribute(key, method: nil, to: :model, cast: nil, default: NO_DEFAULT, **options)
|
217
|
+
__build_props_attribute__(key, (method || key), to, cast, default, **options)
|
218
|
+
end
|
219
|
+
|
220
|
+
# 一度にまとめて複数のattributeを設定します
|
221
|
+
# パラメータは self.attributes に合わせているためそちらを参照してください
|
222
|
+
def attributes(*methods, to: :model, cast: nil, default: nil, **options)
|
223
|
+
_props = {}
|
224
|
+
methods.each do |method|
|
225
|
+
key = method.to_sym
|
226
|
+
unit_props = __build_props_attribute__(key, method, to, cast, default, **options)
|
227
|
+
|
228
|
+
_props.merge!(unit_props)
|
229
|
+
end
|
230
|
+
|
231
|
+
# ブロックが渡されていれば実行する
|
232
|
+
if block_given?
|
233
|
+
_props = yield(_props)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Classのマークをつける(テスト用)
|
237
|
+
_props.merge!(__build_props_class_mark__)
|
238
|
+
# Modelのマークをつける
|
239
|
+
_props.merge!(__build_props_meta_mark__)
|
240
|
+
|
241
|
+
_props
|
242
|
+
end
|
243
|
+
|
244
|
+
# @override
|
245
|
+
# Props::Base#as_props をオーバーライドしています
|
246
|
+
# 本モジュールを読み込んだクラスではas_propsはオーバーライドせず、DSLとpropsメソッドを上書きしてください
|
247
|
+
# @note 仕様
|
248
|
+
# クラス側で設定されたattribute, attributesを元にPropsを作成します
|
249
|
+
# もしくはオーバーライドされているであろう props の結果を設定します
|
250
|
+
# もし両方を設定しているときはどちらの値も設定されます
|
251
|
+
# キーがかぶっているときはprops側が優先されます
|
252
|
+
# @return [Hash] props
|
253
|
+
def as_props
|
254
|
+
_props = {}
|
255
|
+
|
256
|
+
# 結合先が指定されていればその結合先のpropsを取得する
|
257
|
+
if self.class.union.present?
|
258
|
+
if self.class.union == :default
|
259
|
+
_props.merge!(model.as_props)
|
260
|
+
else
|
261
|
+
_props.merge!(model.as_props(self.class.union))
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# DSLの設定があればそれを設定する
|
266
|
+
self.class.settings_attributes.to_a.each do |settings|
|
267
|
+
_props.merge!(
|
268
|
+
attribute(settings[:key], method: settings[:method], to: settings[:to], cast: settings[:cast], default: settings[:default], if: settings[:if])
|
269
|
+
)
|
270
|
+
end
|
271
|
+
|
272
|
+
# Classのマークをつける(テスト用)
|
273
|
+
_props.merge!(__build_props_class_mark__)
|
274
|
+
# Modelのマークをつける
|
275
|
+
_props.merge!(__build_props_meta_mark__)
|
276
|
+
# Propsがオーバーライドされていればその値で上書きする
|
277
|
+
_props.merge!(self.props)
|
278
|
+
|
279
|
+
_props
|
280
|
+
end
|
281
|
+
|
282
|
+
# @return [Hash] props
|
283
|
+
# @note
|
284
|
+
# オーバーライドして使うことを想定しています
|
285
|
+
def props
|
286
|
+
{}
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
private
|
291
|
+
|
292
|
+
# @param [Array<Symbol>] methods
|
293
|
+
# @param [Symbol] key
|
294
|
+
# @param [Symbol] to メソッドのデリゲート先
|
295
|
+
# @param [Block] blockを渡すと実行結果をブロック引数でわたし、その中の戻り値を結果として返します
|
296
|
+
# @param [Symbol] cast デリゲート先のメソッドの戻り値に対して、さらにメソッドを実行する
|
297
|
+
# 複雑性を増す代わりに集約をさせています
|
298
|
+
def __build_props_attribute__(key, method, to, cast, default, **options)
|
299
|
+
props = {}
|
300
|
+
|
301
|
+
_if = options[:if]
|
302
|
+
if _if.present?
|
303
|
+
result = _if.is_a?(Proc) ? _if.call(model) : send(_if)
|
304
|
+
return {} unless result
|
305
|
+
end
|
306
|
+
|
307
|
+
# javascriptは?をキーとして使えないので削除しつつ、isXxx形式に変換する
|
308
|
+
if key.to_s.end_with?("?")
|
309
|
+
key = key.to_s.gsub("?", "").to_sym
|
310
|
+
key = "is_#{key}".to_sym unless key.start_with?("is_")
|
311
|
+
end
|
312
|
+
|
313
|
+
# valはこの後の工程で書き換えの可能性があるため注意
|
314
|
+
if to == :self
|
315
|
+
if method.is_a?(Proc)
|
316
|
+
val = method.call(self)
|
317
|
+
else
|
318
|
+
val = send(method)
|
319
|
+
end
|
320
|
+
else
|
321
|
+
if method.is_a?(Proc)
|
322
|
+
val = method.call(send(to))
|
323
|
+
else
|
324
|
+
val = send(to)&.send(method)
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
case val
|
329
|
+
when Date
|
330
|
+
val = val.strftime(FORMATS[:date])
|
331
|
+
when ActiveSupport::TimeWithZone, Time
|
332
|
+
# 日付系であればjsで使えるようにhtmlに変換する
|
333
|
+
val = val.strftime(FORMATS[:datetime])
|
334
|
+
else
|
335
|
+
if cast.present? && CAST_IGNORE_METHODS.exclude?(key)
|
336
|
+
val = cast.to_s.split(".").inject(val) do |lval, cast_method|
|
337
|
+
lval.send(cast_method)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
if val.nil? && default != NO_DEFAULT
|
343
|
+
val = default
|
344
|
+
end
|
345
|
+
|
346
|
+
props[key] = val
|
347
|
+
|
348
|
+
props
|
349
|
+
end
|
350
|
+
|
351
|
+
# テストモードのときはどのPropsを実行したかを判定できるように属性をつけたします
|
352
|
+
def __build_props_class_mark__
|
353
|
+
mark = {}
|
354
|
+
if ENV["RAILS_ENV"] == "test"
|
355
|
+
mark[:props_class_name] = self.class.name
|
356
|
+
if self.class.union.present?
|
357
|
+
mark[:union_type_name] = self.class.union
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
mark
|
362
|
+
end
|
363
|
+
|
364
|
+
# どのモデルのPropsかを判定できるように属性をつけたします
|
365
|
+
def __build_props_meta_mark__
|
366
|
+
return {} if self.class.skip_meta_mark
|
367
|
+
|
368
|
+
type_target = begin
|
369
|
+
model
|
370
|
+
rescue StandardError
|
371
|
+
self
|
372
|
+
end
|
373
|
+
|
374
|
+
{
|
375
|
+
type: type_target.class.try(:name).to_s,
|
376
|
+
model_name: type_target.class.try(:name).try(:demodulize).to_s,
|
377
|
+
}
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
@@ -1,17 +1,40 @@
|
|
1
|
+
# 本クラスはChoronSupportを利用しているRailsで継承されることを想定して作成されてます
|
2
|
+
# Choronでは以下のようにBaseクラスがデフォルトで作成されています
|
3
|
+
# @example
|
4
|
+
# [app/models/props/base.rb]
|
5
|
+
# class Props::Base < ChoronSupport::Props::Base
|
6
|
+
# include ChoronSupport::Props::Attributes
|
7
|
+
# end
|
8
|
+
# そして各種モデルのPropsは上記のBaseクラスを継承して作成されています
|
9
|
+
# @example
|
10
|
+
# [app/models/props/user.rb]
|
11
|
+
# class Props::User < Props::Base
|
12
|
+
# attributes :id, :name, :age
|
13
|
+
# end
|
14
|
+
# [app/models/props/users/secure.rb]
|
15
|
+
# class Props::Users::Secure < Props::Base
|
16
|
+
# # secure側はageは非表示
|
17
|
+
# attributes :id, :name
|
18
|
+
# end
|
1
19
|
module ChoronSupport
|
2
20
|
module Props
|
3
21
|
class Base
|
4
|
-
|
22
|
+
# @param [ActiveRecord::Base] model Props対象のモデルのインスタンス
|
23
|
+
# @param [Hash] params その他のパラメータ
|
24
|
+
def initialize(model, params = {})
|
5
25
|
@model = model
|
26
|
+
@params = params
|
6
27
|
end
|
7
28
|
|
29
|
+
# 継承先で実装されることを想定しています
|
30
|
+
# ChoronSupport::Props::Attributes を読み込んでいるときは、そちらでオーバーライドされています
|
8
31
|
def as_props
|
9
32
|
raise NotImplementedError
|
10
33
|
end
|
11
34
|
|
12
35
|
private
|
13
36
|
|
14
|
-
attr_reader :model
|
37
|
+
attr_reader :model, :params
|
15
38
|
end
|
16
39
|
end
|
17
|
-
end
|
40
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: choron_support
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mksava
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-12-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -199,6 +199,8 @@ files:
|
|
199
199
|
- Rakefile
|
200
200
|
- choron_support.gemspec
|
201
201
|
- docker-compose.yml
|
202
|
+
- docs/idea.md
|
203
|
+
- docs/props.md
|
202
204
|
- lib/choron_support.rb
|
203
205
|
- lib/choron_support/as_props.rb
|
204
206
|
- lib/choron_support/build_form.rb
|
@@ -206,6 +208,7 @@ files:
|
|
206
208
|
- lib/choron_support/domains/base.rb
|
207
209
|
- lib/choron_support/forms/base.rb
|
208
210
|
- lib/choron_support/helper.rb
|
211
|
+
- lib/choron_support/props/attributes.rb
|
209
212
|
- lib/choron_support/props/base.rb
|
210
213
|
- lib/choron_support/props/ext/hash.rb
|
211
214
|
- lib/choron_support/props/ext/relation.rb
|