choron_support 0.1.8 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e930829f733348afe1938ef857477f85bcc15695e67a31a491e50cd65cb3cc42
4
- data.tar.gz: 41287fc1cdd25345988f03b2ba780e777e8d478413e7f78a907b7931ad362535
3
+ metadata.gz: 650ed706fe5c243363f5a83fc8029023897c19a2cfc6b8973abcd85a8ba8b28e
4
+ data.tar.gz: 96ee27b7f7ac4de9f717ad2ea8ec5076f2d08462f440bc6107175fc6fe1dd9fe
5
5
  SHA512:
6
- metadata.gz: 426090be576fe116a04f6c939c89741868dd5b0b38e495d0fd6b1d613f02af280b8ac6046b0123504323fe2cbb6f7b938e6742863d7dc594626eb4436ccf38a4
7
- data.tar.gz: fb03ba53c19263bffcb80b3aad9853c87cffd53410a8c3246b512625ca549fa7af15ec618ce2fca50d32a055a5fd92572b61fd8cc5e48008a51542b2ad71f476
6
+ metadata.gz: 5ae8dba1af0adfa9dac9adc55056dedcb7c9fc191ed2cbc3d2650eb6d992f1b3974f1d6138f76fea7a5fc616c51220782451abaa85aa12d01f8f7828b1ea0831
7
+ data.tar.gz: eb17113982a53b1ae1b888ad798ab22433bfb8b4e603cf0aaee767f51fe4e3bdc872cad18f178350bb5b8aa7f370fed6e5a13bec83c344802b09980b518f0113
data/Gemfile CHANGED
@@ -9,4 +9,4 @@ gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
11
 
12
- gem "rubocop", "~> 1.21"
12
+ gem "rubocop", "~> 1.21"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- choron_support (0.1.8)
4
+ choron_support (0.1.9)
5
5
  activesupport
6
6
 
7
7
  GEM
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 _web_ | cut -d' ' -f1` /bin/bash"
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
- `#as_props` はオブジェクトやモデルをhashに変換するものです。
30
+ モデルをJSON(キーがローキャメルケース)に変換するための仕組みです。
31
31
 
32
- 名前の `props` の由来は `React` からきています。
33
- 由来の通り、RailsからJS側へ値を渡す際にオブジェクトをJSON化するために作られました。
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
- * TODO
45
+ モデルの処理をメソッド単位で別クラスに委譲するための仕組みです。
46
+ クラスメソッド、インスタンスメソッドの両方で利用できます。
47
+
48
+ * 詳細な実装と使い方は [こちら](./lib/choron_support/domain_delegate.rb) を確認してください。
85
49
 
86
50
  ### Forms
87
51
 
88
- * TODO
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
- * TODO
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
- * テスト用のDBおよびテーブルの作成 & RSpecの実行
111
- * spec/spec_helper.rb を開いて下記にあるDBの作成/Tableの作成のフラグを true に書き換えてから、テストを実行してください
112
- * `bin/rspec spec`
113
-
83
+ * コンテナ内部に入る
114
84
 
115
- ## 本Gemの思想
116
-
117
- Railsにはこれまで多くのリファクタリング手法が、多くの人々から提案されてきました。
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
- 以下、TODO
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, **params)
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, **params)
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
- def initialize(model)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChoronSupport
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.9"
5
5
  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.8
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-06-03 00:00:00.000000000 Z
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