castkit-activerecord 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: caef0ebc2c417ad92c3aaf2f4a1bddfad959ca9b05824068645b20499939d1dd
4
+ data.tar.gz: d90987a60c163bcab570821d8b99ebb9d701023d599ea4c7e8ec12b6cf809c2f
5
+ SHA512:
6
+ metadata.gz: aa44866667adcf3d9ae25b26b2d5facec9d77796420370ad68f372e36e412ca93087ae4fcda76885ef727d6c0946ea2cca64e99298db2a91727d99cf89de90b7
7
+ data.tar.gz: 3d28d5f88764505dbb2238823df1f3172b41b7168dc21c1d767261c02e0707014fa15aaec8b8b91e1f33e60dfb77e226c0d049d0974fa517251f6aca296e979f
@@ -0,0 +1,307 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="AutoImportSettings">
4
+ <option name="autoReloadType" value="SELECTIVE" />
5
+ </component>
6
+ <component name="ChangeListManager">
7
+ <list default="true" id="68f57f34-e50a-475a-a712-14af61f4ccd1" name="Changes" comment="">
8
+ <change afterPath="$PROJECT_DIR$/.github/workflows/main.yml" afterDir="false" />
9
+ <change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
10
+ <change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
11
+ <change afterPath="$PROJECT_DIR$/.rspec" afterDir="false" />
12
+ <change afterPath="$PROJECT_DIR$/.rubocop.yml" afterDir="false" />
13
+ <change afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
14
+ <change afterPath="$PROJECT_DIR$/CODE_OF_CONDUCT.md" afterDir="false" />
15
+ <change afterPath="$PROJECT_DIR$/Gemfile" afterDir="false" />
16
+ <change afterPath="$PROJECT_DIR$/LICENSE.txt" afterDir="false" />
17
+ <change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
18
+ <change afterPath="$PROJECT_DIR$/Rakefile" afterDir="false" />
19
+ <change afterPath="$PROJECT_DIR$/bin/console" afterDir="false" />
20
+ <change afterPath="$PROJECT_DIR$/bin/setup" afterDir="false" />
21
+ <change afterPath="$PROJECT_DIR$/castkit-activerecord.gemspec" afterDir="false" />
22
+ <change afterPath="$PROJECT_DIR$/lib/castkit-activerecord.rb" afterDir="false" />
23
+ <change afterPath="$PROJECT_DIR$/lib/castkit/active_record.rb" afterDir="false" />
24
+ <change afterPath="$PROJECT_DIR$/lib/castkit/active_record/attribute_assigner.rb" afterDir="false" />
25
+ <change afterPath="$PROJECT_DIR$/lib/castkit/active_record/error.rb" afterDir="false" />
26
+ <change afterPath="$PROJECT_DIR$/lib/castkit/active_record/extensions.rb" afterDir="false" />
27
+ <change afterPath="$PROJECT_DIR$/lib/castkit/active_record/serialization.rb" afterDir="false" />
28
+ <change afterPath="$PROJECT_DIR$/lib/castkit/active_record/version.rb" afterDir="false" />
29
+ <change afterPath="$PROJECT_DIR$/sig/castkit/activerecord.rbs" afterDir="false" />
30
+ <change afterPath="$PROJECT_DIR$/spec/castkit/active_record/attribute_assigner_spec.rb" afterDir="false" />
31
+ <change afterPath="$PROJECT_DIR$/spec/castkit/active_record/extensions_spec.rb" afterDir="false" />
32
+ <change afterPath="$PROJECT_DIR$/spec/castkit/activerecord_spec.rb" afterDir="false" />
33
+ <change afterPath="$PROJECT_DIR$/spec/spec_helper.rb" afterDir="false" />
34
+ </list>
35
+ <option name="SHOW_DIALOG" value="false" />
36
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
37
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
38
+ <option name="LAST_RESOLUTION" value="IGNORE" />
39
+ </component>
40
+ <component name="FileTemplateManagerImpl">
41
+ <option name="RECENT_TEMPLATES">
42
+ <list>
43
+ <option value="Ruby Module" />
44
+ <option value="Ruby File" />
45
+ <option value="Ruby Class" />
46
+ </list>
47
+ </option>
48
+ </component>
49
+ <component name="Git.Settings">
50
+ <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
51
+ </component>
52
+ <component name="ProjectColorInfo"><![CDATA[{
53
+ "associatedIndex": 3
54
+ }]]></component>
55
+ <component name="ProjectId" id="2veSb7YQufWlg9O480fu0n2Wigc" />
56
+ <component name="ProjectViewState">
57
+ <option name="autoscrollFromSource" value="true" />
58
+ <option name="hideEmptyMiddlePackages" value="true" />
59
+ <option name="showLibraryContents" value="true" />
60
+ </component>
61
+ <component name="PropertiesComponent"><![CDATA[{
62
+ "keyToString": {
63
+ "RSpec.All specs in spec: castkit-activerecord.executor": "Run",
64
+ "RSpec.Castkit::ActiveRecord.executor": "Run",
65
+ "RSpec.Castkit::ActiveRecord::AttributeAssigner.executor": "Run",
66
+ "RSpec.Castkit::ActiveRecord::Extensions.executor": "Run",
67
+ "RSpec.Castkit::ActiveRecord::UpdateModel.executor": "Run",
68
+ "RunOnceActivity.ShowReadmeOnStart": "true",
69
+ "git-widget-placeholder": "main",
70
+ "node.js.detected.package.eslint": "true",
71
+ "node.js.detected.package.tslint": "true",
72
+ "node.js.selected.package.eslint": "(autodetect)",
73
+ "node.js.selected.package.tslint": "(autodetect)",
74
+ "nodejs_package_manager_path": "npm",
75
+ "ruby.structure.view.model.defaults.configured": "true",
76
+ "vue.rearranger.settings.migration": "true"
77
+ }
78
+ }]]></component>
79
+ <component name="RecentsManager">
80
+ <key name="MoveFile.RECENT_KEYS">
81
+ <recent name="$PROJECT_DIR$/lib/castkit" />
82
+ <recent name="$PROJECT_DIR$/lib" />
83
+ <recent name="$PROJECT_DIR$/lib/castkit/activerecord" />
84
+ </key>
85
+ </component>
86
+ <component name="RunManager" selected="RSpec.All specs in spec: castkit-activerecord">
87
+ <configuration name="All specs in spec: castkit-activerecord" type="RSpecRunConfigurationType" factoryName="RSpec" temporary="true">
88
+ <module name="castkit-activerecord" />
89
+ <predefined_log_file enabled="true" id="RUBY_RSPEC" />
90
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUBY_ARGS" VALUE="" />
91
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="WORK DIR" VALUE="$MODULE_DIR$" />
92
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SHOULD_USE_SDK" VALUE="false" />
93
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ALTERN_SDK_NAME" VALUE="" />
94
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="myPassParentEnvs" VALUE="true" />
95
+ <envs>
96
+ <env name="JRUBY_OPTS" value="-X+O" />
97
+ </envs>
98
+ <EXTENSION ID="BundlerRunConfigurationExtension" BUNDLE_MODE="AUTO" bundleExecEnabled="true" />
99
+ <EXTENSION ID="RubyCoverageRunConfigurationExtension" track_test_folders="true" runner="rcov" ENABLE_BRANCH_COVERAGE="true" ENABLE_FORKED_COVERAGE="true">
100
+ <COVERAGE_PATTERN ENABLED="true">
101
+ <PATTERN REGEXPS="/.rvm/" INCLUDED="false" />
102
+ </COVERAGE_PATTERN>
103
+ </EXTENSION>
104
+ <EXTENSION ID="org.jetbrains.plugins.ruby.rails.run.RailsRunConfigurationExtension" SCRATCH_USE_RAILS_RUNNER="false" />
105
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TESTS_FOLDER_PATH" VALUE="$MODULE_DIR$/spec" />
106
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATH" VALUE="" />
107
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATHS" VALUE="" />
108
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_RUNNER_PATH" VALUE="" />
109
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_FILE_MASK" VALUE="**/*_spec.rb" />
110
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_EXAMPLE_NAME" VALUE="" />
111
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_EXAMPLE_MATCHES" VALUE="false" />
112
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="LINE_NUMBER_EXAMPLE_IDS" VALUE="" />
113
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_TEST_TYPE" VALUE="ALL_IN_FOLDER" />
114
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_ARGS" VALUE="" />
115
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUNNER_VERSION" VALUE="" />
116
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_CUSTOM_SPEC_RUNNER" VALUE="false" />
117
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="DRB" VALUE="false" />
118
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ZEUS" VALUE="false" />
119
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPRING" VALUE="false" />
120
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="FULL_BACKTRACE" VALUE="false" />
121
+ <method v="2" />
122
+ </configuration>
123
+ <configuration name="Castkit::ActiveRecord" type="RSpecRunConfigurationType" factoryName="RSpec" temporary="true" nameIsGenerated="true">
124
+ <module name="castkit-activerecord" />
125
+ <predefined_log_file enabled="true" id="RUBY_RSPEC" />
126
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUBY_ARGS" VALUE="" />
127
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="WORK DIR" VALUE="$MODULE_DIR$" />
128
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SHOULD_USE_SDK" VALUE="false" />
129
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ALTERN_SDK_NAME" VALUE="" />
130
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="myPassParentEnvs" VALUE="true" />
131
+ <envs>
132
+ <env name="JRUBY_OPTS" value="-X+O" />
133
+ </envs>
134
+ <EXTENSION ID="BundlerRunConfigurationExtension" BUNDLE_MODE="AUTO" bundleExecEnabled="true" />
135
+ <EXTENSION ID="RubyCoverageRunConfigurationExtension" track_test_folders="true" runner="rcov" ENABLE_BRANCH_COVERAGE="true" ENABLE_FORKED_COVERAGE="true">
136
+ <COVERAGE_PATTERN ENABLED="true">
137
+ <PATTERN REGEXPS="/.rvm/" INCLUDED="false" />
138
+ </COVERAGE_PATTERN>
139
+ </EXTENSION>
140
+ <EXTENSION ID="org.jetbrains.plugins.ruby.rails.run.RailsRunConfigurationExtension" SCRATCH_USE_RAILS_RUNNER="false" />
141
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TESTS_FOLDER_PATH" VALUE="" />
142
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATH" VALUE="$MODULE_DIR$/spec/castkit/active_record/core_spec.rb" />
143
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATHS" VALUE="" />
144
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_RUNNER_PATH" VALUE="" />
145
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_FILE_MASK" VALUE="**/*_spec.rb" />
146
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_EXAMPLE_NAME" VALUE="Castkit::ActiveRecord" />
147
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_EXAMPLE_MATCHES" VALUE="false" />
148
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="LINE_NUMBER_EXAMPLE_IDS" VALUE="" />
149
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_TEST_TYPE" VALUE="TEST_SCRIPT" />
150
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_ARGS" VALUE="" />
151
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUNNER_VERSION" VALUE="" />
152
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_CUSTOM_SPEC_RUNNER" VALUE="false" />
153
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="DRB" VALUE="false" />
154
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ZEUS" VALUE="false" />
155
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPRING" VALUE="false" />
156
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="FULL_BACKTRACE" VALUE="false" />
157
+ <method v="2" />
158
+ </configuration>
159
+ <configuration name="Castkit::ActiveRecord::AttributeAssigner" type="RSpecRunConfigurationType" factoryName="RSpec" temporary="true" nameIsGenerated="true">
160
+ <module name="castkit-activerecord" />
161
+ <predefined_log_file enabled="true" id="RUBY_RSPEC" />
162
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUBY_ARGS" VALUE="" />
163
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="WORK DIR" VALUE="$MODULE_DIR$" />
164
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SHOULD_USE_SDK" VALUE="false" />
165
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ALTERN_SDK_NAME" VALUE="" />
166
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="myPassParentEnvs" VALUE="true" />
167
+ <envs>
168
+ <env name="JRUBY_OPTS" value="-X+O" />
169
+ </envs>
170
+ <EXTENSION ID="BundlerRunConfigurationExtension" BUNDLE_MODE="AUTO" bundleExecEnabled="true" />
171
+ <EXTENSION ID="RubyCoverageRunConfigurationExtension" track_test_folders="true" runner="rcov" ENABLE_BRANCH_COVERAGE="true" ENABLE_FORKED_COVERAGE="true">
172
+ <COVERAGE_PATTERN ENABLED="true">
173
+ <PATTERN REGEXPS="/.rvm/" INCLUDED="false" />
174
+ </COVERAGE_PATTERN>
175
+ </EXTENSION>
176
+ <EXTENSION ID="org.jetbrains.plugins.ruby.rails.run.RailsRunConfigurationExtension" SCRATCH_USE_RAILS_RUNNER="false" />
177
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TESTS_FOLDER_PATH" VALUE="" />
178
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATH" VALUE="$MODULE_DIR$/spec/castkit/active_record/attribute_assigner_spec.rb" />
179
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATHS" VALUE="" />
180
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_RUNNER_PATH" VALUE="" />
181
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_FILE_MASK" VALUE="**/*_spec.rb" />
182
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_EXAMPLE_NAME" VALUE="Castkit::ActiveRecord::AttributeAssigner" />
183
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_EXAMPLE_MATCHES" VALUE="false" />
184
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="LINE_NUMBER_EXAMPLE_IDS" VALUE="" />
185
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_TEST_TYPE" VALUE="TEST_SCRIPT" />
186
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_ARGS" VALUE="" />
187
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUNNER_VERSION" VALUE="" />
188
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_CUSTOM_SPEC_RUNNER" VALUE="false" />
189
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="DRB" VALUE="false" />
190
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ZEUS" VALUE="false" />
191
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPRING" VALUE="false" />
192
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="FULL_BACKTRACE" VALUE="false" />
193
+ <method v="2" />
194
+ </configuration>
195
+ <configuration name="Castkit::ActiveRecord::Extensions" type="RSpecRunConfigurationType" factoryName="RSpec" temporary="true" nameIsGenerated="true">
196
+ <module name="castkit-activerecord" />
197
+ <predefined_log_file enabled="true" id="RUBY_RSPEC" />
198
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUBY_ARGS" VALUE="" />
199
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="WORK DIR" VALUE="$MODULE_DIR$" />
200
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SHOULD_USE_SDK" VALUE="false" />
201
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ALTERN_SDK_NAME" VALUE="" />
202
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="myPassParentEnvs" VALUE="true" />
203
+ <envs>
204
+ <env name="JRUBY_OPTS" value="-X+O" />
205
+ </envs>
206
+ <EXTENSION ID="BundlerRunConfigurationExtension" BUNDLE_MODE="AUTO" bundleExecEnabled="true" />
207
+ <EXTENSION ID="RubyCoverageRunConfigurationExtension" track_test_folders="true" runner="rcov" ENABLE_BRANCH_COVERAGE="true" ENABLE_FORKED_COVERAGE="true">
208
+ <COVERAGE_PATTERN ENABLED="true">
209
+ <PATTERN REGEXPS="/.rvm/" INCLUDED="false" />
210
+ </COVERAGE_PATTERN>
211
+ </EXTENSION>
212
+ <EXTENSION ID="org.jetbrains.plugins.ruby.rails.run.RailsRunConfigurationExtension" SCRATCH_USE_RAILS_RUNNER="false" />
213
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TESTS_FOLDER_PATH" VALUE="" />
214
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATH" VALUE="$MODULE_DIR$/spec/castkit/active_record/extensions_spec.rb" />
215
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATHS" VALUE="" />
216
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_RUNNER_PATH" VALUE="" />
217
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_FILE_MASK" VALUE="**/*_spec.rb" />
218
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_EXAMPLE_NAME" VALUE="Castkit::ActiveRecord::Extensions" />
219
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_EXAMPLE_MATCHES" VALUE="false" />
220
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="LINE_NUMBER_EXAMPLE_IDS" VALUE="" />
221
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_TEST_TYPE" VALUE="TEST_SCRIPT" />
222
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_ARGS" VALUE="" />
223
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUNNER_VERSION" VALUE="" />
224
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_CUSTOM_SPEC_RUNNER" VALUE="false" />
225
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="DRB" VALUE="false" />
226
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ZEUS" VALUE="false" />
227
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPRING" VALUE="false" />
228
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="FULL_BACKTRACE" VALUE="false" />
229
+ <method v="2" />
230
+ </configuration>
231
+ <configuration name="Castkit::ActiveRecord::UpdateModel" type="RSpecRunConfigurationType" factoryName="RSpec" temporary="true" nameIsGenerated="true">
232
+ <module name="castkit-activerecord" />
233
+ <predefined_log_file enabled="true" id="RUBY_RSPEC" />
234
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUBY_ARGS" VALUE="" />
235
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="WORK DIR" VALUE="$MODULE_DIR$" />
236
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SHOULD_USE_SDK" VALUE="false" />
237
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ALTERN_SDK_NAME" VALUE="" />
238
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="myPassParentEnvs" VALUE="true" />
239
+ <envs>
240
+ <env name="JRUBY_OPTS" value="-X+O" />
241
+ </envs>
242
+ <EXTENSION ID="BundlerRunConfigurationExtension" BUNDLE_MODE="AUTO" bundleExecEnabled="true" />
243
+ <EXTENSION ID="RubyCoverageRunConfigurationExtension" track_test_folders="true" runner="rcov" ENABLE_BRANCH_COVERAGE="true" ENABLE_FORKED_COVERAGE="true">
244
+ <COVERAGE_PATTERN ENABLED="true">
245
+ <PATTERN REGEXPS="/.rvm/" INCLUDED="false" />
246
+ </COVERAGE_PATTERN>
247
+ </EXTENSION>
248
+ <EXTENSION ID="org.jetbrains.plugins.ruby.rails.run.RailsRunConfigurationExtension" SCRATCH_USE_RAILS_RUNNER="false" />
249
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TESTS_FOLDER_PATH" VALUE="" />
250
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATH" VALUE="$MODULE_DIR$/spec/castkit/active_record/update_model_spec.rb" />
251
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATHS" VALUE="" />
252
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_RUNNER_PATH" VALUE="" />
253
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_FILE_MASK" VALUE="**/*_spec.rb" />
254
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_EXAMPLE_NAME" VALUE="Castkit::ActiveRecord::UpdateModel" />
255
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_EXAMPLE_MATCHES" VALUE="false" />
256
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="LINE_NUMBER_EXAMPLE_IDS" VALUE="" />
257
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_TEST_TYPE" VALUE="TEST_SCRIPT" />
258
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_ARGS" VALUE="" />
259
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUNNER_VERSION" VALUE="" />
260
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_CUSTOM_SPEC_RUNNER" VALUE="false" />
261
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="DRB" VALUE="false" />
262
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ZEUS" VALUE="false" />
263
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPRING" VALUE="false" />
264
+ <RSPEC_RUN_CONFIG_SETTINGS_ID NAME="FULL_BACKTRACE" VALUE="false" />
265
+ <method v="2" />
266
+ </configuration>
267
+ <recent_temporary>
268
+ <list>
269
+ <item itemvalue="RSpec.All specs in spec: castkit-activerecord" />
270
+ <item itemvalue="RSpec.Castkit::ActiveRecord::AttributeAssigner" />
271
+ <item itemvalue="RSpec.Castkit::ActiveRecord" />
272
+ <item itemvalue="RSpec.Castkit::ActiveRecord::Extensions" />
273
+ <item itemvalue="RSpec.Castkit::ActiveRecord::UpdateModel" />
274
+ </list>
275
+ </recent_temporary>
276
+ </component>
277
+ <component name="SharedIndexes">
278
+ <attachedChunks>
279
+ <set>
280
+ <option value="bundled-js-predefined-d6986cc7102b-1632447f56bf-JavaScript-RM-243.26053.19" />
281
+ </set>
282
+ </attachedChunks>
283
+ </component>
284
+ <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
285
+ <component name="SpringUtil" SPRING_PRE_LOADER_OPTION="true" RAKE_SPRING_PRE_LOADER_OPTION="true" RAILS_SPRING_PRE_LOADER_OPTION="true" />
286
+ <component name="TaskManager">
287
+ <task active="true" id="Default" summary="Default task">
288
+ <changelist id="68f57f34-e50a-475a-a712-14af61f4ccd1" name="Changes" comment="" />
289
+ <created>1744504114865</created>
290
+ <option name="number" value="Default" />
291
+ <option name="presentableId" value="Default" />
292
+ <updated>1744504114865</updated>
293
+ <workItem from="1744504116069" duration="29224000" />
294
+ </task>
295
+ <servers />
296
+ </component>
297
+ <component name="TypeScriptGeneratedFilesManager">
298
+ <option name="version" value="3" />
299
+ </component>
300
+ <component name="com.intellij.coverage.CoverageDataManagerImpl">
301
+ <SUITE FILE_PATH="coverage/castkit_activerecord@Castkit__ActiveRecord__AttributeAssigner.rcov" NAME="Castkit::ActiveRecord::AttributeAssigner Coverage Results" MODIFIED="1744546387147" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="rcov" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" MODULE_NAME="castkit-activerecord" />
302
+ <SUITE FILE_PATH="coverage/castkit_activerecord@All_specs_in_spec__castkit_activerecord.rcov" NAME="All specs in spec: castkit-activerecord Coverage Results" MODIFIED="1744547818646" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="rcov" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" MODULE_NAME="castkit-activerecord" />
303
+ <SUITE FILE_PATH="coverage/castkit_activerecord@Castkit__ActiveRecord__Extensions.rcov" NAME="Castkit::ActiveRecord::Extensions Coverage Results" MODIFIED="1744513004728" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="rcov" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" MODULE_NAME="castkit-activerecord" />
304
+ <SUITE FILE_PATH="coverage/castkit_activerecord@Castkit__ActiveRecord.rcov" NAME="Castkit::ActiveRecord Coverage Results" MODIFIED="1744533156469" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="rcov" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" MODULE_NAME="castkit-activerecord" />
305
+ <SUITE FILE_PATH="coverage/castkit_activerecord@Castkit__ActiveRecord__UpdateModel.rcov" NAME="Castkit::ActiveRecord::UpdateModel Coverage Results" MODIFIED="1744512870206" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="rcov" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" MODULE_NAME="castkit-activerecord" />
306
+ </component>
307
+ </project>
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rspec_status ADDED
@@ -0,0 +1,19 @@
1
+ example_id | status | run_time |
2
+ -------------------------------------------------------------- | ------ | --------------- |
3
+ ./spec/castkit/active_record/attribute_assigner_spec.rb[1:1:1] | passed | 0.00053 seconds |
4
+ ./spec/castkit/active_record/attribute_assigner_spec.rb[1:1:2] | passed | 0.00012 seconds |
5
+ ./spec/castkit/active_record/attribute_assigner_spec.rb[1:2:1] | passed | 0.0006 seconds |
6
+ ./spec/castkit/active_record/attribute_assigner_spec.rb[1:2:2] | passed | 0.00008 seconds |
7
+ ./spec/castkit/active_record/attribute_assigner_spec.rb[1:3:1] | passed | 0.00042 seconds |
8
+ ./spec/castkit/active_record/attribute_assigner_spec.rb[1:3:2] | passed | 0.0001 seconds |
9
+ ./spec/castkit/active_record/extensions_spec.rb[1:1:1] | passed | 0.00061 seconds |
10
+ ./spec/castkit/active_record/extensions_spec.rb[1:1:2] | passed | 0.00005 seconds |
11
+ ./spec/castkit/active_record/extensions_spec.rb[1:1:3] | passed | 0.00112 seconds |
12
+ ./spec/castkit/active_record/extensions_spec.rb[1:2:1] | passed | 0.08415 seconds |
13
+ ./spec/castkit/active_record/extensions_spec.rb[1:3:1] | passed | 0.00019 seconds |
14
+ ./spec/castkit/activerecord_spec.rb[1:1:1] | passed | 0.00013 seconds |
15
+ ./spec/castkit/activerecord_spec.rb[1:1:2] | passed | 0.00012 seconds |
16
+ ./spec/castkit/activerecord_spec.rb[1:1:3] | passed | 0.00006 seconds |
17
+ ./spec/castkit/activerecord_spec.rb[1:2:1] | passed | 0.00012 seconds |
18
+ ./spec/castkit/activerecord_spec.rb[1:3:1] | passed | 0.00014 seconds |
19
+ ./spec/castkit/activerecord_spec.rb[1:3:2] | passed | 0.00008 seconds |
data/.rubocop.yml ADDED
@@ -0,0 +1,41 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7.0
3
+ NewCops: enable
4
+ SuggestExtensions: true
5
+
6
+ Style/Documentation:
7
+ Exclude:
8
+ - spec/castkit/**/*.rb
9
+
10
+ Style/StringLiterals:
11
+ Enabled: true
12
+ EnforcedStyle: double_quotes
13
+
14
+ Style/StringLiteralsInInterpolation:
15
+ Enabled: true
16
+ EnforcedStyle: double_quotes
17
+
18
+ Gemspec/DevelopmentDependencies:
19
+ EnforcedStyle: gemspec
20
+
21
+ Layout/LineLength:
22
+ Max: 120
23
+
24
+ Metrics/ClassLength:
25
+ Max: 200
26
+
27
+ Metrics/BlockLength:
28
+ Exclude:
29
+ - castkit-activerecord.gemspec
30
+ - spec/**/*.rb
31
+
32
+ Metrics/MethodLength:
33
+ Max: 20
34
+
35
+ Naming/FileName:
36
+ Exclude:
37
+ - lib/castkit-activerecord.rb
38
+
39
+ Naming/MemoizedInstanceVariableName:
40
+ Exclude:
41
+ - lib/castkit/active_record/extensions.rb
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-04-12
4
+
5
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at bnlucas@outlook.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Nathan Lucas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # Castkit ActiveRecord
2
+
3
+ **Castkit::ActiveRecord** is a companion gem to [Castkit](https://rubygems.org/gems/castkit) that bridges your `Castkit::DataObject` classes with ActiveRecord models. It enables conversion to/from models, assignment, and safe update flows for nested objects.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bundle add castkit-activerecord
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Setup
14
+
15
+ Include the module in your `Castkit::DataObject` class and associate it with a model:
16
+
17
+ ```ruby
18
+ class UserDto < Castkit::DataObject
19
+ include Castkit::ActiveRecord
20
+ model User
21
+
22
+ string :name
23
+ has_many :posts, of: PostDto
24
+ end
25
+ ```
26
+
27
+ ### Converting a Model to a DTO
28
+
29
+ ```ruby
30
+ user_dto = UserDto.from_model(User.find(1))
31
+ ```
32
+
33
+ ### Converting a DTO to a Model
34
+
35
+ ```ruby
36
+ user_model = user_dto.to_model
37
+ ```
38
+
39
+ ### Updating a Model from a DTO
40
+
41
+ ```ruby
42
+ user.update_from_dataobject(user_dto, mode: :merge)
43
+ ```
44
+
45
+ Use `update_from_dataobject!` if you want to raise on failure. Pass `ignore: [:field]` to skip certain fields.
46
+
47
+ ## Nested Support
48
+
49
+ Nested `dataobject` and `has_many`/`has_one` relationships are supported:
50
+
51
+ ```ruby
52
+ class PostDto < Castkit::DataObject
53
+ include Castkit::ActiveRecord
54
+ model Post
55
+
56
+ string :title
57
+ dataobject :author, UserDto
58
+ end
59
+ ```
60
+
61
+ ## Opting Out of Attribute Updates
62
+
63
+ You can skip certain fields from being assigned during updates:
64
+
65
+ ```ruby
66
+ # This is automatically included
67
+ class User < ApplicationRecord
68
+
69
+ castkit_ignored_on_update :id, :status
70
+ end
71
+ ```
72
+
73
+ ## Update Modes
74
+
75
+ - `:replace` — full object replacement (default)
76
+ - `:merge` — performs recursive merging via `update_from_dataobject!` on nested models
77
+
78
+ ## Methods
79
+
80
+ ### On `Castkit::DataObject`
81
+
82
+ - `to_model` → Instantiates a model from the DTO
83
+
84
+ ### On `ActiveRecord::Base`
85
+
86
+ - `to_dataobject(UserDto)`
87
+ - `update_from_dataobject(dto, mode: :replace, ignore: [])`
88
+ - `update_from_dataobject!(dto, mode: :replace, ignore: [])`
89
+
90
+ ## 📃 License
91
+
92
+ MIT. See [LICENSE](LICENSE.txt).
93
+
94
+ ---
95
+
96
+ ## 🙏 Credits
97
+
98
+ Created with ❤️ by [Nathan Lucas](https://github.com/bnlucas)
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/castkit/active_record/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "castkit-activerecord"
7
+ spec.version = Castkit::ActiveRecord::VERSION
8
+ spec.authors = ["Nathan Lucas"]
9
+ spec.email = ["bnlucas@outlook.com"]
10
+
11
+ spec.summary = "ActiveRecord integration for Castkit."
12
+ spec.description = "Provides conversion helpers to serialize and deserialize between ActiveRecord " \
13
+ "models and Castkit DataObjects. Supports nested DataObjects, eager loading, and " \
14
+ "ActiveRecord-safe hydration."
15
+ spec.homepage = "https://github.com/bnlucas/castkit-activerecord"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 2.7.0"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/bnlucas/castkit-activerecord"
21
+ spec.metadata["changelog_uri"] = "https://github.com/bnlucas/castkit-activerecord/blob/main/CHANGELOG.md"
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ spec.files = Dir.chdir(__dir__) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ (File.expand_path(f) == __FILE__) ||
27
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
28
+ end
29
+ end
30
+
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_dependency "activerecord", ">= 6.1"
36
+ spec.add_dependency "castkit", "~> 0.1", ">=0.1.1"
37
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module ActiveRecord
5
+ # Converts a Castkit::DataObject into a hash of assignable attributes
6
+ # for ActiveRecord model instances.
7
+ #
8
+ # This handles nested `dataobject` and `dataobject_collection` types,
9
+ # and supports recursive conversion using `.to_model` or `.update_from_dataobject!`.
10
+ class AttributeAssigner
11
+ # Supported update modes
12
+ UPDATE_MODES = %i[replace merge].freeze
13
+
14
+ class << self
15
+ # Builds assignable attributes for a DataObject, suitable for ActiveRecord models.
16
+ #
17
+ # @param dataobject [Castkit::DataObject] the DTO instance
18
+ # @param model [ActiveRecord::Base, nil] optional target model
19
+ # @param mode [Symbol] the update mode (:replace or :merge)
20
+ # @param ignore [Array<Symbol>] additional attribute keys to exclude from assignment
21
+ # @return [Hash<Symbol, Object>] model-assignable attributes
22
+ def call(dataobject, model = nil, mode: :replace, ignore: [])
23
+ new(dataobject).attributes(model, mode: mode, ignore: ignore)
24
+ end
25
+ end
26
+
27
+ # @param dataobject [Castkit::DataObject] the source data object to assign from
28
+ def initialize(dataobject)
29
+ @dataobject = dataobject
30
+ @attributes = dataobject.class.attributes
31
+ end
32
+
33
+ # Returns a filtered and transformed hash of attributes for assignment
34
+ #
35
+ # @param model [ActiveRecord::Base, nil] optional model for filtering
36
+ # @param mode [Symbol] either :replace or :merge
37
+ # @param ignore [Array<Symbol>] optional attribute keys to exclude
38
+ # @return [Hash<Symbol, Object>] assignable attributes
39
+ # @raise [ArgumentError] for invalid mode or assignment failure
40
+ def attributes(model = nil, mode: :replace, ignore: [])
41
+ validate_mode!(mode)
42
+
43
+ assignable_attributes(model, ignore).each_with_object({}) do |(field, attribute), hash|
44
+ value = @dataobject.public_send(field)
45
+ (hash[field] = nil) and next unless value
46
+
47
+ target = model.public_send(field) if model
48
+ hash[field] = assign(attribute, value, target, mode)
49
+ rescue Castkit::Error
50
+ raise
51
+ rescue StandardError => e
52
+ raise ArgumentError, "Unable to assign attribute #{field}, #{e}"
53
+ end.freeze
54
+ end
55
+
56
+ private
57
+
58
+ # Validates the provided mode against the list of supported modes, UPDATE_MODES.
59
+ #
60
+ # @param mode [Symbol] either :replace or :merge
61
+ # @raise [ArgumentError] for invalid mode or assignment failure
62
+ def validate_mode!(mode)
63
+ mode = mode.to_sym
64
+ raise ArgumentError, "Unsupported update mode: #{mode.inspect}" unless UPDATE_MODES.include?(mode)
65
+ end
66
+
67
+ # Filters attributes based on readonly/ignored rules
68
+ #
69
+ # @param model [ActiveRecord::Base, nil]
70
+ # @param ignore [Array<Symbol>] explicit keys to skip
71
+ # @return [Hash<Symbol, Castkit::Attribute>] remaining attributes
72
+ def assignable_attributes(model = nil, ignore = [])
73
+ return @attributes unless model
74
+
75
+ unassignable = model.class.try(:castkit_ignored_on_update) || model.class.readonly_attributes.to_a
76
+ all_ignored = (unassignable + ignore).map(&:to_sym)
77
+
78
+ @attributes.reject { |key, _| all_ignored.include?(key) }
79
+ end
80
+
81
+ # Applies transformation logic for an individual attribute.
82
+ #
83
+ # @param attribute [Castkit::Attribute]
84
+ # @param value [Object] the DTO value
85
+ # @param target [Object] the current model value (if merging)
86
+ # @param mode [Symbol]
87
+ # @return [Object] the value to assign
88
+ def assign(attribute, value, target, mode)
89
+ if attribute.dataobject? && value.respond_to?(:to_model)
90
+ assign_dataobject(value, target, mode)
91
+ elsif attribute.dataobject_collection? && value.all? { |v| v.respond_to?(:to_model) }
92
+ assign_dataobject_collection(value, target, mode)
93
+ else
94
+ value
95
+ end
96
+ end
97
+
98
+ # Assigns a nested dataobject to the target model
99
+ #
100
+ # @param value [Castkit::DataObject]
101
+ # @param target [Object]
102
+ # @param mode [Symbol]
103
+ # @return [ActiveRecord::Base]
104
+ def assign_dataobject(value, target, mode)
105
+ return value.to_model if mode == :replace
106
+ return value.to_model unless target.respond_to?(:update_from_dataobject!)
107
+
108
+ target.update_from_dataobject!(value, mode: :merge)
109
+ target
110
+ rescue StandardError
111
+ warn "Unable to assign attribute #{target.inspect} to #{mode.inspect}"
112
+ raise
113
+ end
114
+
115
+ # Assigns a collection of nested dataobjects to the target model collection
116
+ #
117
+ # @param value [Array<Castkit::DataObject>]
118
+ # @param target [Array<ActiveRecord::Base>]
119
+ # @param mode [Symbol]
120
+ # @return [Array<ActiveRecord::Base>]
121
+ def assign_dataobject_collection(value, target, mode)
122
+ return value.map(&:to_model) if mode == :replace
123
+
124
+ raise ArgumentError, "expected target to be Enumerable" unless target.is_a?(Enumerable)
125
+ raise ArgumentError, "size mismatch; expected #{target.size}, got #{value.size}" if value.size != target.size
126
+
127
+ value.zip(target).map do |dataobject, model|
128
+ model.update_from_dataobject!(dataobject, mode: :merge)
129
+ model
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module ActiveRecord
5
+ # Base error class for all Castkit ActiveRecord-related exceptions.
6
+ class Error < Castkit::Error; end
7
+
8
+ # Raised when a Castkit::DataObject does not have an associated model defined.
9
+ #
10
+ # This typically occurs when calling `from_model`, `to_model`, or `update_model!`
11
+ # without first declaring a model via `.model SomeModel`.
12
+ #
13
+ # @example
14
+ # class UserDto < Castkit::DataObject
15
+ # include Castkit::ActiveRecord
16
+ # # no model defined here
17
+ # end
18
+ #
19
+ # UserDto.new.to_model
20
+ # # => raises Castkit::ActiveRecord::ModelNotDefined
21
+ class ModelNotDefined < Error
22
+ # @param klass [Class] the dataobject class missing a model definition
23
+ def initialize(klass)
24
+ super("No model defined for #{klass.name}. Did you forget to call model SomeModel?")
25
+ end
26
+ end
27
+
28
+ # Raised when the object passed to `update_model!` is not of the expected model type.
29
+ #
30
+ # This ensures that the DataObject only attempts to update the specific model it was
31
+ # designed to work with.
32
+ #
33
+ # @example
34
+ # class UserDto < Castkit::DataObject
35
+ # include Castkit::ActiveRecord
36
+ # model User
37
+ # end
38
+ #
39
+ # UserDto.new.from_model(comment_record)
40
+ # # => raises Castkit::ActiveRecord::InvalidModel
41
+ class InvalidModel < Error
42
+ # @param model [Class] the expected model class
43
+ # @param obj [Object] the object that was passed in
44
+ def initialize(model, obj)
45
+ super("Expected instance of #{model.class}, got #{obj.class}")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attribute_assigner"
4
+
5
+ module Castkit
6
+ module ActiveRecord
7
+ # Adds support for customizing update behavior and ergonomic model → DataObject conversion.
8
+ #
9
+ # When included into an ActiveRecord model, this module allows:
10
+ # - Opting out of assigning certain fields during updates via `castkit_ignored_on_update`
11
+ # - Converting a model instance to a Castkit::DataObject via `to_dataobject`
12
+ # - Updating a model instance from a Castkit::DataObject via `update_from_dataobject!`
13
+ module Extensions
14
+ def self.included(base)
15
+ base.extend(ClassMethods)
16
+ end
17
+
18
+ # Class-level methods to extend ActiveRecord models.
19
+ module ClassMethods
20
+ # Defines or returns the list of attributes that should not be assigned
21
+ # when calling `update_from_dataobject!` on an ActiveRecord model.
22
+ #
23
+ # This method merges any custom-defined attributes with the model's
24
+ # `readonly_attributes`, unless explicitly overridden.
25
+ #
26
+ # @example Mark `:id` and `:status` as unassignable
27
+ # class User < ApplicationRecord
28
+ # castkit_ignored_on_update :id, :status
29
+ # end
30
+ #
31
+ # @param attributes [Array<Symbol>] optional attributes to ignore
32
+ # @return [Array<Symbol>] the full list of ignored attributes
33
+ # @raise [NotImplementedError] if the model doesn't support `.readonly_attributes`
34
+ def castkit_ignored_on_update(*attributes)
35
+ unless respond_to?(:readonly_attributes)
36
+ raise NotImplementedError, "Expected class to respond to `readonly_attributes`"
37
+ end
38
+
39
+ @__castkit_ignored_attributes ||= []
40
+
41
+ unless attributes.empty?
42
+ @__castkit_ignored_attributes = attributes.map(&:to_sym)
43
+ @__castkit_cached_ignored_attributes = nil
44
+ end
45
+
46
+ unassignable_attributes = (@__castkit_ignored_attributes + readonly_attributes.to_a)
47
+ @__castkit_cached_ignored_attributes ||= unassignable_attributes.map(&:to_sym).uniq.freeze
48
+ end
49
+ end
50
+
51
+ # Converts this model instance into a Castkit::DataObject.
52
+ #
53
+ # @param klass [Class<Castkit::DataObject>] the target dto class
54
+ # @return [Castkit::DataObject]
55
+ # @raise [ArgumentError] if the class is not a valid Castkit::DataObject
56
+ def to_dataobject(klass)
57
+ unless Castkit.dataobject?(klass) && klass.respond_to?(:from_model)
58
+ raise ArgumentError, "#{klass} must include Castkit::ActiveRecord"
59
+ end
60
+
61
+ klass.from_model(self)
62
+ end
63
+
64
+ # Updates this model instance from a Castkit::DataObject.
65
+ #
66
+ # This is the safe variant of `update_from_dataobject!`, returning false
67
+ # if the update or save fails due to validation errors.
68
+ #
69
+ # @param dto [Castkit::DataObject] the source dto
70
+ # @param mode [Symbol] the update mode (:replace or :merge)
71
+ # @param ignore [Array<Symbol>] additional attributes to skip
72
+ # @return [Boolean] whether the update was successful
73
+ def update_from_dataobject(dto, mode: :replace, ignore: [])
74
+ update_from_dataobject!(dto, mode: mode, ignore: ignore)
75
+ true
76
+ rescue ::ActiveRecord::RecordInvalid, ::ActiveModel::ValidationError
77
+ false
78
+ end
79
+
80
+ # Updates this model instance from a Castkit::DataObject.
81
+ #
82
+ # This performs a recursive update, including nested DataObjects,
83
+ # and saves the model after assignment.
84
+ #
85
+ # @param dto [Castkit::DataObject] the source dto
86
+ # @param mode [Symbol] the update mode (:replace or :merge)
87
+ # @param ignore [Array<Symbol>] additional attribute keys to skip
88
+ # @return [ActiveRecord::Base] the updated model
89
+ # @raise [ActiveRecord::RecordInvalid, ActiveModel::ValidationError] if the model fails to save
90
+ def update_from_dataobject!(dto, mode: :replace, ignore: [])
91
+ attributes = Castkit::ActiveRecord::AttributeAssigner.call(
92
+ dto,
93
+ self,
94
+ mode: mode,
95
+ ignore: ignore
96
+ )
97
+
98
+ assign_attributes(attributes)
99
+ save!
100
+
101
+ self
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module ActiveRecord
5
+ # Provides methods for serializing and deserializing between
6
+ # ActiveRecord model instances and Castkit::DataObject instances.
7
+ #
8
+ # This module supports nested associations and optional eager loading
9
+ # for relations.
10
+ module Serialization
11
+ # Builds a Castkit::DataObject from an ActiveRecord model instance.
12
+ #
13
+ # @param obj [ActiveRecord::Base] the model instance to convert
14
+ # @return [Castkit::DataObject] the hydrated DataObject
15
+ # @raise [Castkit::ActiveRecord::ModelNotDefined] if no model is defined
16
+ # @raise [Castkit::ActiveRecord::InvalidModel] if the object is not an instance of the expected model
17
+ def from_model(obj)
18
+ ensure_model_type!(obj)
19
+ from_hash(attributes_from_model(obj))
20
+ end
21
+
22
+ # Builds a collection of DataObjects from an ActiveRecord::Relation or array of model instances.
23
+ #
24
+ # Optionally eager loads nested associations.
25
+ #
26
+ # @param relation [Enumerable] the model collection
27
+ # @param eager_load [Boolean] whether to eager load nested associations
28
+ # @param as [Class<Castkit::DataObject>, nil] an optional DataObject class to use
29
+ # @return [Array<Castkit::DataObject>] an array of hydrated DataObjects
30
+ # @raise [ArgumentError] if the relation is not enumerable
31
+ def from_relation(relation, eager_load: false, as: nil)
32
+ dataobject = as || self
33
+ raise ArgumentError, "Expected an Enumerable (e.g. ActiveRecord::Relation)" unless relation.respond_to?(:map)
34
+
35
+ relation = relation.includes(*dataobject.nested_associations) if eager_load && relation.respond_to?(:includes)
36
+ relation.map { |record| from_model(record) }
37
+ end
38
+
39
+ # Returns a list of nested association fields defined on the DataObject.
40
+ #
41
+ # This is used to support eager loading.
42
+ #
43
+ # @return [Array<Symbol>] an array of nested field names
44
+ def nested_associations
45
+ attributes.values.select(&:dataobject?).map(&:field)
46
+ end
47
+
48
+ private
49
+
50
+ # Extracts a hash of attribute values from a model instance.
51
+ #
52
+ # For nested attributes, the method recursively invokes `.from_model`.
53
+ #
54
+ # @param obj [ActiveRecord::Base] the model instance
55
+ # @return [Hash] a hash of normalized attribute values for the DataObject
56
+ def attributes_from_model(obj)
57
+ model_attributes = obj.attributes.transform_keys(&:to_sym)
58
+
59
+ attributes.each_with_object({}) do |(field, attribute), hash|
60
+ hash[field] =
61
+ if attribute.dataobject?
62
+ field_association(obj, attribute)
63
+ else
64
+ model_attributes[field]
65
+ end
66
+ end
67
+ end
68
+
69
+ # Resolves a nested field value from a model association.
70
+ #
71
+ # @param obj [ActiveRecord::Base] the model
72
+ # @param attribute [Castkit::Attribute] the Castkit attribute
73
+ # @return [Object] the resolved value (DataObject or raw value)
74
+ def field_association(obj, attribute)
75
+ association = obj.public_send(attribute.field)
76
+
77
+ return if association.nil?
78
+ return attribute.type.from_model(association) if attribute.dataobject?
79
+ return association.map { attribute.options[:of].from_model(_1) } if attribute.dataobject_collection?
80
+
81
+ association
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module ActiveRecord
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "castkit"
4
+ require_relative "active_record/version"
5
+ require_relative "active_record/error"
6
+ require_relative "active_record/extensions"
7
+ require_relative "active_record/serialization"
8
+ require_relative "active_record/attribute_assigner"
9
+
10
+ module Castkit
11
+ # Adds ActiveRecord integration to a Castkit::DataObject.
12
+ #
13
+ # Includes support for defining a corresponding model class, converting
14
+ # models to/from DataObjects, and assigning model attributes.
15
+ #
16
+ # @example Defining a DataObject for an ActiveRecord model
17
+ # class UserDto < Castkit::DataObject
18
+ # include Castkit::ActiveRecord
19
+ # model User
20
+ #
21
+ # string :name
22
+ # has_many :posts, of: PostDto
23
+ # end
24
+ module ActiveRecord
25
+ # Called when the module is included in a class.
26
+ #
27
+ # Extends the class with ActiveRecord-aware relationship DSLs.
28
+ #
29
+ # @param base [Class] the including class
30
+ def self.included(base)
31
+ base.extend(ClassMethods)
32
+
33
+ class << base
34
+ alias_method :belongs_to, :dataobject
35
+ alias_method :has_one, :dataobject
36
+ alias_method :has_many, :array
37
+ end
38
+ end
39
+
40
+ # Defines class-level behavior for ActiveRecord integration.
41
+ module ClassMethods
42
+ include Castkit::ActiveRecord::Serialization
43
+
44
+ # Defines or returns the ActiveRecord model class associated with the DataObject.
45
+ #
46
+ # @param model [Class, nil] the model to assign
47
+ # @return [Class] the current model class
48
+ def model(model = nil)
49
+ model ? (@model_class = model) : @model_class
50
+ end
51
+
52
+ # Ensures that a model has been defined for the given DataObject class.
53
+ #
54
+ # @param klass [Class] the DataObject class (defaults to self)
55
+ # @raise [Castkit::ActiveRecord::ModelNotDefined] if no model is assigned
56
+ def ensure_model_defined!(klass = self)
57
+ raise Castkit::ActiveRecord::ModelNotDefined, klass unless klass.model
58
+ end
59
+
60
+ # Ensures that the given object is an instance of the associated model class.
61
+ #
62
+ # @param obj [Object] the object to validate
63
+ # @param klass [Class] the DataObject class (defaults to self)
64
+ # @raise [Castkit::ActiveRecord::ModelNotDefined] if no model is assigned
65
+ # @raise [Castkit::ActiveRecord::InvalidModel] if obj is not an instance of the model
66
+ def ensure_model_type!(obj, klass = self)
67
+ ensure_model_defined!(klass)
68
+ raise Castkit::ActiveRecord::InvalidModel.new(model, obj) unless obj.is_a?(model)
69
+ end
70
+ end
71
+
72
+ # Builds a new instance of the associated ActiveRecord model
73
+ # from the DataObject’s current values.
74
+ #
75
+ # Calls `to_model` on any nested DataObjects if available.
76
+ #
77
+ # @return [ActiveRecord::Base] a new model with attributes assigned
78
+ # @raise [Castkit::ActiveRecord::ModelNotDefined] if no model is defined
79
+ def to_model
80
+ self.class.ensure_model_defined!
81
+ attributes = Castkit::ActiveRecord::AttributeAssigner.call(self)
82
+
83
+ model = self.class.model.new
84
+ model.assign_attributes(attributes)
85
+ model
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "castkit"
5
+
6
+ require_relative "castkit/active_record"
7
+
8
+ ActiveSupport.on_load(:active_record) do
9
+ include Castkit::ActiveRecord::Extensions
10
+ end
@@ -0,0 +1,6 @@
1
+ module Castkit
2
+ module Activerecord
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: castkit-activerecord
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Lucas
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-04-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: castkit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 0.1.1
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '0.1'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.1.1
47
+ description: Provides conversion helpers to serialize and deserialize between ActiveRecord
48
+ models and Castkit DataObjects. Supports nested DataObjects, eager loading, and
49
+ ActiveRecord-safe hydration.
50
+ email:
51
+ - bnlucas@outlook.com
52
+ executables: []
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - ".idea/workspace.xml"
57
+ - ".rspec"
58
+ - ".rspec_status"
59
+ - ".rubocop.yml"
60
+ - CHANGELOG.md
61
+ - CODE_OF_CONDUCT.md
62
+ - LICENSE.txt
63
+ - README.md
64
+ - Rakefile
65
+ - castkit-activerecord.gemspec
66
+ - lib/castkit-activerecord.rb
67
+ - lib/castkit/active_record.rb
68
+ - lib/castkit/active_record/attribute_assigner.rb
69
+ - lib/castkit/active_record/error.rb
70
+ - lib/castkit/active_record/extensions.rb
71
+ - lib/castkit/active_record/serialization.rb
72
+ - lib/castkit/active_record/version.rb
73
+ - sig/castkit/activerecord.rbs
74
+ homepage: https://github.com/bnlucas/castkit-activerecord
75
+ licenses:
76
+ - MIT
77
+ metadata:
78
+ homepage_uri: https://github.com/bnlucas/castkit-activerecord
79
+ source_code_uri: https://github.com/bnlucas/castkit-activerecord
80
+ changelog_uri: https://github.com/bnlucas/castkit-activerecord/blob/main/CHANGELOG.md
81
+ rubygems_mfa_required: 'true'
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 2.7.0
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.5.4
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: ActiveRecord integration for Castkit.
101
+ test_files: []