choron_support 0.1.9 → 0.1.10

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: 650ed706fe5c243363f5a83fc8029023897c19a2cfc6b8973abcd85a8ba8b28e
4
- data.tar.gz: 96ee27b7f7ac4de9f717ad2ea8ec5076f2d08462f440bc6107175fc6fe1dd9fe
3
+ metadata.gz: 5f5e95a92c449d959b9e9522a05e244461e98fb078b5405c93ceb873fdc5c49b
4
+ data.tar.gz: cec6ee644f7813d21e24039aadd30a6047e3a708a48cced1412f593c68be626a
5
5
  SHA512:
6
- metadata.gz: 5ae8dba1af0adfa9dac9adc55056dedcb7c9fc191ed2cbc3d2650eb6d992f1b3974f1d6138f76fea7a5fc616c51220782451abaa85aa12d01f8f7828b1ea0831
7
- data.tar.gz: eb17113982a53b1ae1b888ad798ab22433bfb8b4e603cf0aaee767f51fe4e3bdc872cad18f178350bb5b8aa7f370fed6e5a13bec83c344802b09980b518f0113
6
+ metadata.gz: a77b9097a63624fc09fdea17886eb85861c37272058938739692b812fd728d658602020c1b56f0ac3d2ed7f08e51f3b9b234dac28d23e9590620b81ac6794e08
7
+ data.tar.gz: 5ceff8def9787ebf4bdadf656173669f5ef1271cf27e08b41ec22b122b134f1f3e19ef70f59674a3a714eb4061e617fd381ea0a84a8c542b2bb6f9803fc02cba
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- choron_support (0.1.9)
4
+ choron_support (0.1.10)
5
5
  activesupport
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -25,13 +25,9 @@ ChoronSupport.using :all
25
25
 
26
26
  * 必要に応じて各種モジュールをincludeすることで利用できます
27
27
 
28
- ### AsProps
29
-
30
- モデルをJSON(キーがローキャメルケース)に変換するための仕組みです。
31
-
32
- * 詳細な使い方は [テストファイル](./spec/choron_support/as_props_spec.rb) を参照ください。
33
- * 詳細な実装は [こちら](./lib/choron_support/as_props.rb) です。
28
+ ### Props
34
29
 
30
+ * [Props ドキュメント](./docs/props.md)を参照ください
35
31
 
36
32
  ### Mask
37
33
 
data/docs/props.md CHANGED
@@ -11,225 +11,25 @@ Choronでは画面側の処理をReact + Typescrip の組み合わせで実現
11
11
  一般的なJSON化ツールの違いとしては、Javascript側の記法・慣習を優先するため、
12
12
  JSONのキーをローキャメルケース(fullName, isAdult,など)に自動変換します
13
13
 
14
- ## Choronでの使い方
14
+ ## 使い方
15
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
16
+ ```ruby
17
+ class Props::Samples::Foo < Props::Base
18
+ attribute :id
19
+ attribute :full_name
118
20
  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
21
 
213
- def is_sample?
214
- true
215
- end
216
- end
22
+ sample = Sample.new(id: 100, full_name: "mksava")
23
+ sample.as_props(:foo)
24
+ #=> { id: 100, fullName: "mksava", type: "Foo", modelName: "Foo" }
217
25
  ```
218
26
 
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
- ```
27
+ * 詳細な使い方はテストファイルを参照ください。
28
+ * [一般的な使い方](../spec/choron_support/as_props/general_spec.rb)
29
+ * [エッジケース](../spec/choron_support/as_props/edge_spec.rb)
30
+ * [attribute DSLの使い方](../spec/choron_support/as_props/attribute_spec.rb)
31
+ * [inherit DSL の使い方](../spec/choron_support/as_props/inherit_spec.rb)
32
+ * [relation DSLの使い方](../spec/choron_support/as_props/relation_spec.rb)
33
+ * 実装は以下を参照ください
34
+ * [as_props.rb](../lib/choron_support/as_props.rb)
35
+ * [props/](../lib/choron_support/props/)
@@ -9,14 +9,14 @@ module ChoronSupport
9
9
  # @param [Hash] params その他のパラメータ。camel: false を指定すると自動でキャメライズしない。
10
10
  # @return [Hash]
11
11
  def as_props(type_symbol = nil, **params)
12
- serializer = self.__get_props_class(type_symbol, params)
12
+ pass_params = params.except(:camel)
13
13
 
14
+ serializer = self.__get_props_class(type_symbol, pass_params)
14
15
  skip_camel = (params[:camel] == false)
15
- pass_params = params.except(:camel)
16
16
  if serializer.nil?
17
17
  skip_camel ? self.as_json : self.as_json.as_camel
18
18
  else
19
- skip_camel ? serializer.as_props(**pass_params) : serializer.as_props(**pass_params).as_camel
19
+ skip_camel ? serializer.as_props : serializer.as_props.as_camel
20
20
  end
21
21
  end
22
22
 
@@ -31,14 +31,15 @@ module ChoronSupport
31
31
  class_name = type_symbol.to_s.classify
32
32
  # 例: Serialize::Users::FooBar
33
33
  props_class_name = "#{namespace}::#{class_name}"
34
- when nil
35
- namespace = "Props"
36
- # 例: User / Master::Plan
37
- class_name = self.class.to_s
38
- # 例: Props::User
39
- props_class_name = "#{namespace}::#{class_name}"
34
+ when Class
35
+ given_class = type_symbol
36
+ if given_class.ancestors.include?(ChoronSupport::Props::Base)
37
+ props_class_name = given_class.to_s
38
+ else
39
+ raise ArgumentError, "invalid class: #{given_class}"
40
+ end
40
41
  else
41
- raise ArgumentError
42
+ raise ArgumentError, "invalid type_symbol: #{type_symbol}"
42
43
  end
43
44
 
44
45
  begin
@@ -46,11 +47,6 @@ module ChoronSupport
46
47
 
47
48
  props_class.new(self, params)
48
49
  rescue *rescue_errors
49
- # もしmodelを指定しているときはnilを返し、as_jsonを利用させる
50
- if type_symbol == :model
51
- return nil
52
- end
53
-
54
50
  if type_symbol.blank?
55
51
  raise ChoronSupport::AsProps::NameError, "Props class not found: #{props_class_name}. Please create props class."
56
52
  else
@@ -1,381 +1,215 @@
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)
1
+ require_relative "./private/type_builder"
2
+ require_relative "./private/setting"
3
+ module ChoronSupport::Props::Attributes
4
+ unless defined?(ActiveSupport)
5
+ raise "ActiveSupport is not defined. Please require 'active_support/all' in your Gemfile"
6
+ end
227
7
 
228
- _props.merge!(unit_props)
229
- end
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # DSLで設定できる設定値たち ==========================
12
+ # Props作成時に設定されるキーと値を設定します。
13
+ # この値はDSLを経由して内部的に設定されていきます
14
+ class_attribute :settings, instance_writer: false, default: nil
15
+ # Props作成時に自動で付与される元のクラスの文字列を設定しないときはtrueを設定してください
16
+ # @example
17
+ # class Props::Foos::Bar
18
+ # self.skip_meta_mark = true
19
+ # end
20
+ class_attribute :skip_meta_mark, default: false
21
+ # Propsから自動でtypescriptの型を生成しないときはfalseを設定してください
22
+ # @example
23
+ # class Props::Foos::Bar
24
+ # self.skip_typescript = true
25
+ # end
26
+ class_attribute :skip_typescript, default: false
27
+ # 他のPropsクラスの設定を継承するときに設定されます
28
+ class_attribute :inherit_props_class, default: nil
29
+ # ====================================================
30
+
31
+ # Propsとして出力する属性を設定するためのDSLです
32
+ # @param [Symbol] method モデル, もしくは to オプションで設定したオブジェクトに対して実行するメソッドを指定してください
33
+ # @param [Keyword] options その他のオプションを指定してください
34
+ # @option [Symbol] :to 指定したメソッドを実行するオブジェクトを指定できます
35
+ # @option [Symbol | lambda] :if 属性を出力するための条件を指定できます
36
+ # @option [Symbol] :cast 属性を出力する前に指定したメソッドを実行できます。
37
+ # @option [Boolean] :default 属性値がnilのときに代わりに出力する値を指定できます
38
+ # @option [Proc] &block ブロックを渡すとそのブロックの戻り値を属性値として出力します
39
+ def self.attribute(method, **options, &block)
40
+ setting_params = options.merge(method: method, block: block)
41
+ setting = ChoronSupport::Props::Private::Setting.new(setting_params)
42
+
43
+ self.settings ||= []
44
+ self.settings << setting
45
+ end
230
46
 
231
- # ブロックが渡されていれば実行する
232
- if block_given?
233
- _props = yield(_props)
234
- end
47
+ # 他のPropsクラスの設定を継承するためのDSLです
48
+ # @param [ChoronSupport::Props::Base] inherit_props_class 継承するPropsクラスを指定してください
49
+ # @example
50
+ # class Props::Users::General < ChoronSupport::Props::Base
51
+ # inherit Props::Users::Base
52
+ # end
53
+ def self.inherit(props_class)
54
+ # 継承するクラスはProps::Baseを継承している必要があります
55
+ unless props_class.ancestors.include?(ChoronSupport::Props::Base)
56
+ raise "inherit class must be ChoronSupport::Props::Base. got: #{props_class}"
57
+ end
235
58
 
236
- # Classのマークをつける(テスト用)
237
- _props.merge!(__build_props_class_mark__)
238
- # Modelのマークをつける
239
- _props.merge!(__build_props_meta_mark__)
59
+ # 既に継承先が設定されている場合はエラーにします
60
+ if self.inherit_props_class.present?
61
+ raise "inherit props inherit class already set: #{self.inherit_props_class}.(Only one class can be inherited)"
62
+ end
240
63
 
241
- _props
242
- end
64
+ self.inherit_props_class = props_class
65
+ self.settings ||= []
66
+ self.inherit_props_class.settings.to_a.each do |setting|
67
+ self.settings << setting
68
+ end
69
+ end
243
70
 
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 = {}
71
+ # Modelに対して関連付けされた別ModelのPropsを結合するためのDSLです
72
+ # @param [Symbol] method to オプションで指定されたオブジェクトに実行されるメソッドを指定してください
73
+ # @param [ChoronSupport::Props::Base] props モデルをProps化するためのクラスを指定してください
74
+ # @param [Keyword] options その他のオプションを指定してください。詳細は attribute と同じです
75
+ # @example
76
+ # class Props::Users::General < ChoronSupport::Props::Base
77
+ # relation :posts, props: Props::Posts::General
78
+ # #=> { posts: user.posts.as_props(:general) } と同じ結果になる
79
+ # end
80
+ def self.relation(method, props_class, **options)
81
+ self.attribute(method, **options) do |model, params|
82
+ records = model.send(method)
83
+ records&.as_props(props_class, **params)
84
+ end
85
+ end
86
+ end
255
87
 
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
88
+ # @return [Hash] props
89
+ def as_props
90
+ _props = {}
264
91
 
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
92
+ # DSLの設定を設定する
93
+ self.class.settings.to_a.each do |setting|
94
+ _props.merge!(__build_props_attribute__(setting))
95
+ end
271
96
 
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)
97
+ # Classのマークをつける(テスト用)
98
+ _props.merge!(__build_props_class_mark__)
99
+ # Modelのマークをつける
100
+ _props.merge!(__build_props_meta_mark__)
278
101
 
279
- _props
280
- end
102
+ _props
103
+ end
281
104
 
282
- # @return [Hash] props
283
- # @note
284
- #  オーバーライドして使うことを想定しています
285
- def props
286
- {}
287
- end
288
- end
105
+ private
289
106
 
290
- private
107
+ def model
108
+ raise NotImplementedError, "model method is not implemented"
109
+ end
291
110
 
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 = {}
111
+ def params
112
+ raise NotImplementedError, "params method is not implemented"
113
+ end
300
114
 
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
115
+ FORMATS = {
116
+ # HTMLのinput type="date"で使える形式
117
+ date: "%Y-%m-%d",
118
+ datetime: "%Y-%m-%dT%H:%M",
119
+ }.freeze
120
+ # 型のキャスト指定があってもキャストはしないメソッド
121
+ CAST_IGNORE_METHODS = [
122
+ # id は数値のほうが良いため
123
+ :id,
124
+ ].freeze
125
+ # @param [Array<Symbol>] Setting
126
+ def __build_props_attribute__(setting)
127
+ attribute = {}
128
+
129
+ _if = setting.if
130
+ if _if.present?
131
+ result = send(_if)
132
+ return {} unless result
133
+ end
306
134
 
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
135
+ # javascriptは?をキーとして使えないので削除しつつ、isXxx形式に変換する
136
+ key = setting.name
137
+ if key.to_s.end_with?("?")
138
+ key = key.to_s.gsub("?", "").to_sym
139
+ key = "is_#{key}".to_sym unless key.start_with?("is_")
140
+ end
312
141
 
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
142
+ # valはこの後の工程で書き換えの可能性があるため注意
143
+ val = nil
144
+ method = setting.method
145
+ to = setting.to
146
+ if setting.block.present?
147
+ val = setting.block.call(model, params)
148
+ else
149
+ if to == :self
150
+ if method.is_a?(Proc)
151
+ val = method.call(self)
320
152
  else
321
- if method.is_a?(Proc)
322
- val = method.call(send(to))
323
- else
324
- val = send(to)&.send(method)
325
- end
153
+ val = send(method)
326
154
  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])
155
+ else
156
+ if method.is_a?(Proc)
157
+ val = method.call(send(to))
334
158
  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
159
+ val = send(to)&.send(method)
340
160
  end
161
+ end
162
+ end
341
163
 
342
- if val.nil? && default != NO_DEFAULT
343
- val = default
164
+ case val
165
+ when Date
166
+ val = val.strftime(FORMATS[:date])
167
+ when ActiveSupport::TimeWithZone, Time
168
+ # 日付系であればjsで使えるようにhtmlに変換する
169
+ val = val.strftime(FORMATS[:datetime])
170
+ else
171
+ if setting.cast.present? && CAST_IGNORE_METHODS.exclude?(key)
172
+ val = setting.cast.to_s.split(".").inject(val) do |lval, cast_method|
173
+ lval.send(cast_method)
344
174
  end
175
+ end
176
+ end
345
177
 
346
- props[key] = val
178
+ if val.nil? && setting.set_default?
179
+ val = setting.default
180
+ end
347
181
 
348
- props
349
- end
182
+ attribute[key] = val
350
183
 
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
184
+ attribute
185
+ end
360
186
 
361
- mark
187
+ # テストモードのときはどのPropsを実行したかを判定できるように属性をつけたします
188
+ def __build_props_class_mark__
189
+ mark = {}
190
+ if ENV["RAILS_ENV"] == "test"
191
+ mark[:props_class_name] = self.class.name
192
+ if self.class.inherit_props_class.present?
193
+ mark[:inherit_props_class_name] = self.class.inherit_props_class.try(:name) || self.class.inherit_props_class.to_s
362
194
  end
195
+ end
363
196
 
364
- # どのモデルのPropsかを判定できるように属性をつけたします
365
- def __build_props_meta_mark__
366
- return {} if self.class.skip_meta_mark
197
+ mark
198
+ end
367
199
 
368
- type_target = begin
369
- model
370
- rescue StandardError
371
- self
372
- end
200
+ # どのモデルのPropsかを判定できるように属性をつけたします
201
+ def __build_props_meta_mark__
202
+ return {} if self.class.skip_meta_mark
373
203
 
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
204
+ type_target = begin
205
+ model
206
+ rescue StandardError
207
+ self
379
208
  end
209
+
210
+ {
211
+ type: type_target.class.try(:name).to_s,
212
+ model_name: type_target.class.try(:name).try(:demodulize).to_s,
213
+ }
380
214
  end
381
215
  end
@@ -1,24 +1,10 @@
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
+ require_relative "./attributes"
2
+
19
3
  module ChoronSupport
20
4
  module Props
21
5
  class Base
6
+ include ChoronSupport::Props::Attributes
7
+
22
8
  # @param [ActiveRecord::Base] model Props対象のモデルのインスタンス
23
9
  # @param [Hash] params その他のパラメータ
24
10
  def initialize(model, params = {})
@@ -26,15 +12,20 @@ module ChoronSupport
26
12
  @params = params
27
13
  end
28
14
 
29
- # 継承先で実装されることを想定しています
30
- # ChoronSupport::Props::Attributes を読み込んでいるときは、そちらでオーバーライドされています
31
- def as_props
32
- raise NotImplementedError
33
- end
34
-
35
15
  private
36
16
 
37
- attr_reader :model, :params
17
+ # @override
18
+ def model
19
+ @model
20
+ end
21
+
22
+ # @override
23
+ def params
24
+ @params
25
+ end
38
26
  end
39
27
  end
40
28
  end
29
+
30
+ __END__
31
+ abcd
@@ -0,0 +1,42 @@
1
+ class ChoronSupport::Props::Private::Setting
2
+ class Error < StandardError; end
3
+ # デフォルト値を設定しない場合に使う値
4
+ NO_DEFAULT = Object.new.freeze
5
+ private_constant :NO_DEFAULT
6
+ SETTING_ATTRIBUTES = %i[method name to cast default if block].freeze
7
+ private_constant :SETTING_ATTRIBUTES
8
+
9
+ SETTING_ATTRIBUTES.each {|atr_name| attr_reader atr_name }
10
+
11
+ def initialize(params)
12
+ # 不正なオプションがあれば例外を発生させる
13
+ if (params.keys - SETTING_ATTRIBUTES).present?
14
+ raise Error, "invalid params: #{(params.keys - SETTING_ATTRIBUTES).join(", ")}, valid params are #{SETTING_ATTRIBUTES.join(", ")}"
15
+ end
16
+
17
+ @method = params[:method]
18
+ @name = params[:name] || @method
19
+ @to = params[:to] || :model
20
+ @cast = params[:cast]
21
+ @default = params[:default] || NO_DEFAULT
22
+ @if = params[:if] || nil
23
+ @block = params[:block] || nil
24
+
25
+ check_params!
26
+ end
27
+
28
+ def set_default?
29
+ self.default != NO_DEFAULT
30
+ end
31
+
32
+ private
33
+
34
+ def check_params!
35
+ if name.blank?
36
+ raise Error, "name is required"
37
+ end
38
+ if method.blank?
39
+ raise Error, "method is required"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,129 @@
1
+ # @deprecated
2
+ class ChoronSupport::Props::Private::TypeBuilder
3
+ RESULT = Struct.new(:file_path, :body, :type_name, :attributes)
4
+
5
+ # @return [String]
6
+ # @memo
7
+ # 必要に応じてoverrideしてください
8
+ def self.output_dir
9
+ "app/javascript/types/props"
10
+ end
11
+
12
+ # @return [String]
13
+ # @memo
14
+ # 必要に応じてoverrideしてください
15
+ def self.file_path(props_class)
16
+ self.default_build_file_path(props_class)
17
+ end
18
+
19
+ def initialize(props_class)
20
+ @body_buffer = []
21
+ @attributes_buffer = []
22
+ @props_class = props_class
23
+ end
24
+
25
+ # 設定値やクラス名からTypeScriptの型を生成する
26
+ # @return [RESULT]
27
+ # @example
28
+ # class Foo::Bars::Staff < Props::Base
29
+ # attribute :id, type: "number | null"
30
+ # attribute :name, type: "string"
31
+ # attribute :is_super, type: "boolean",
32
+ # attribute :license_names, type: "Array<string>"
33
+ # end
34
+ # builder = ChoronSupport::Props::Private::TypeBuilder.new(Foo::Bars::Staff)
35
+ # builder.build
36
+ # ####====#####
37
+ # type Foo_Bars_StaffProps = {
38
+ # id: number | null,
39
+ # name: string,
40
+ # is_super: boolean,
41
+ # license_names: Array<string>,
42
+ # type: "Foo::Bars::Staff"
43
+ # modelName: "Foo::Bar"
44
+ # }
45
+ # ####====#####
46
+ def build
47
+ set_type_buffer
48
+
49
+ file_path = self.class.file_path(props_class)
50
+ body = body_buffer.join("\n")
51
+ type_name = build_type_name(props_class)
52
+ attributes = attributes_buffer.join("\n")
53
+
54
+ RESULT.new(file_path, body, type_name, attributes)
55
+ end
56
+
57
+ # buildされたTypeScriptの型をファイルに出力する
58
+ # @return [RESULT]
59
+ def generate
60
+ result = self.build
61
+
62
+ # 出力用のディレクトリがなければ作成する
63
+ if !Dir.exist?(self.class.output_dir)
64
+ FileUtils.mkdir_p(self.class.output_dir)
65
+ end
66
+
67
+ # ファイルを作成する
68
+ File.open(result.file_path, "w") do |f|
69
+ f.puts(result.body)
70
+ end
71
+
72
+ result
73
+ end
74
+
75
+ def __body_buffer__
76
+ body_buffer
77
+ end
78
+
79
+ private
80
+
81
+ attr_reader :props_class, :body_buffer, :attributes_buffer
82
+
83
+ def self.default_build_file_path(props_class)
84
+ # 分かりやすいようにそのままtypenameをファイル名にする
85
+ file_name = props_class.name.gsub("::", "_") + ".d.ts"
86
+
87
+ if defined?(Rails) && Rails.root.present?
88
+ Rails.root.join(self.output_dir, file_name).to_s
89
+ else
90
+ File.join(self.output_dir, file_name)
91
+ end
92
+ end
93
+
94
+ def set_type_buffer
95
+ body_buffer << "type #{build_type_name(props_class)} = {"
96
+ attributes_buffer = "{"
97
+
98
+ build_attributes(props_class).each do |attr_val|
99
+ body_buffer << " #{attr_val}"
100
+ attributes_buffer << " #{attr_val}"
101
+ end
102
+
103
+ body_buffer << "}"
104
+ attributes_buffer << "}"
105
+ end
106
+
107
+ def build_attributes(props_class)
108
+ attributes = []
109
+ props_class.settings.each do |setting|
110
+ attributes << build_attribute(setting)
111
+ end
112
+
113
+ attributes
114
+ end
115
+
116
+ def build_attribute(setting)
117
+ name = setting.name
118
+ _if = setting.if
119
+ name_val = "#{name}#{_if ? "?" : ""}"
120
+
121
+ type = setting.type
122
+
123
+ "#{name_val}: #{type}"
124
+ end
125
+
126
+ def build_type_name(props_class)
127
+ props_class.name.gsub("::", "_")
128
+ end
129
+ end
@@ -0,0 +1,62 @@
1
+ # @deprecated
2
+ class ChoronSupport::Props::Private::TypeGenerator
3
+ def run
4
+ start_output
5
+
6
+ results = []
7
+ targer_props.each do |props_class|
8
+ result = builder_class.new(props_class).generate
9
+ results << result
10
+ end
11
+
12
+ output_results(results)
13
+
14
+ results
15
+ end
16
+
17
+ private
18
+
19
+ def target_props_class?(props_class)
20
+ props_class.respond_to?(:skip_typescript) && props_class.respond_to?(:settings)
21
+ end
22
+
23
+ def self.props_base
24
+ Props::Base
25
+ end
26
+
27
+ def targer_props
28
+ props_list = []
29
+ self.class.props_base.descendants.each do |props_class|
30
+ next unless target_props_class?(props_class)
31
+ next if props_class.skip_typescript
32
+
33
+ props_list << props_class
34
+ end
35
+
36
+ props_list
37
+ end
38
+
39
+ def builder_class
40
+ ChoronSupport::Props::Private::TypeBuilder
41
+ end
42
+
43
+ def start_output
44
+ log("Start generating TypeScript Props...: #{targer_props.size}")
45
+ end
46
+
47
+ def output_results(results)
48
+ log("Generated TypeScript Props: #{results.size}")
49
+ log("for...")
50
+ results.each do |result|
51
+ log(" #{result.file_path}")
52
+ end
53
+
54
+ log("Done.")
55
+ end
56
+
57
+ def log(str)
58
+ @logger_method ||= defined?(Rails) ? Rails.logger.method(:info) : method(:puts)
59
+
60
+ @logger_method.call(str)
61
+ end
62
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChoronSupport
4
- VERSION = "0.1.9"
4
+ VERSION = "0.1.10"
5
5
  end
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChoronSupport
4
+ module Domains; end
5
+ module Forms; end
6
+ module Props; end
7
+ module Props::Private; end
8
+ module Queries; end
9
+
4
10
  SUPPORT_FILES = {
5
11
  domains: "choron_support/domain_delegate",
6
12
  queries: "choron_support/scope_query",
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.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - mksava
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-12-05 00:00:00.000000000 Z
11
+ date: 2024-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -212,6 +212,9 @@ files:
212
212
  - lib/choron_support/props/base.rb
213
213
  - lib/choron_support/props/ext/hash.rb
214
214
  - lib/choron_support/props/ext/relation.rb
215
+ - lib/choron_support/props/private/setting.rb
216
+ - lib/choron_support/props/private/type_builder.rb
217
+ - lib/choron_support/props/private/type_generator.rb
215
218
  - lib/choron_support/queries/base.rb
216
219
  - lib/choron_support/scope_query.rb
217
220
  - lib/choron_support/set_mask_for.rb