support_table_data 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 93d64b82d63c76b8a0d51232bc39dacb319d5c5304aea0f97899d843fb32b21a
4
+ data.tar.gz: 8d07ea94b74ea7daf8dd5a863527cdd57c55a0bd301e347dfeb25ec5915208c9
5
+ SHA512:
6
+ metadata.gz: 2032fa102c26ffd779b01a75d7cd7c1079b51b33e6afb3677a0b34f879864a60c6895fa6e4795c74f300b4cdd4786e2bb195a43f03c75749f88dec74fdf8b245
7
+ data.tar.gz: d3877f8a0d8abe36a293ad726471db5336ac0f555b3aa13f41ef1ffb06f457692674f726c4056aae300d97b236b8715bb5a79a19a5c55a601eb7088e4fb1d6be
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 1.0.0
8
+
9
+ ### Added
10
+ - Add SupportTableData concern to enable automatic syncing of data on support tables.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Brian Durand
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # Support Table Data
2
+
3
+ [![Continuous Integration](https://github.com/bdurand/support_table_data/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/support_table_data/actions/workflows/continuous_integration.yml)
4
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+
6
+ This gem provides a mixin for ActiveRecord support table models that allows you to load data from YAML, JSON, or CSV files and reference specific records more easily. It is intended to solve issues with support tables (also known as lookup tables) that contain a small set of canonical data that must exist for your application to work.
7
+
8
+ These kinds of models blur the line between data and code. You'll often end up with constants and application logic based on specific values that need to exist in the table. By using this gem, you can easily define methods for loading and comparing specific instances. This can give you cleaner code that reads far more naturally. You can also avoid defining dozens of constants or referencing magic values (i.e. no more hard-coded strings or ids in the code to look up specific records).
9
+
10
+ ## Usage
11
+
12
+ In the examples below, suppose we have a simple `Status` model in which each row has an id and a name, and the name can only have a handful of statuses: "Pending", "In Progress", and "Completed".
13
+
14
+ Now, we may have code that needs to reference the status and make decisions based on it. This will require that the table have the exact same values in it in every environment. This gem lets you define these values in a YAML file:
15
+
16
+ ```yaml
17
+ - id: 1
18
+ name: Pending
19
+
20
+ - id: 2
21
+ name: In Progress
22
+
23
+ - id: 3
24
+ name: Completed
25
+ ```
26
+
27
+ You can then use this mixin to match that data with your model:
28
+
29
+ ```ruby
30
+ class Status < ApplicationRecord
31
+ include SupportTableData
32
+
33
+ # Set the default location for data files. (This is the default value in a Rails application.)
34
+ self.support_table_data_directory = Rails.root + "db" + "support_tables"
35
+
36
+ # Add the data file to the model; you can also specify an absolute path and add multiple data files.
37
+ add_support_table_data "statuses.yml"
38
+ end
39
+ ```
40
+
41
+ ### Specifying Data Files
42
+
43
+ You use the `add_support_table_data` class method to add a data file path. This file must be a YAML, JSON, or CSV file that defines a list of attributes. YAML and JSON files should contain an array where each element is a hash of the attributes for each record. YAML and JSON file can also be defined as a hash when using named instances (see below). CSV files must use comma delimiters, double quotes for the quote character, and have a header row containing the attribute names.
44
+
45
+ One of the attributes in your data files will be the key attribute. This attribute must uniquely identify each element. By default, the key attribute will be the table's primary key. You can change this by setting the `support_table_key_attribute` class attribute on the model.
46
+
47
+ ```ruby
48
+ class Status < ApplicationRecord
49
+ include SupportTableData
50
+
51
+ self.support_table_key_attribute = :name
52
+ end
53
+ ```
54
+
55
+ You cannot update the value of the key attribute in a record in the data file. If you do, a new record will be created and the existing record will be left unchanged.
56
+
57
+ You can specify data files as relative paths. This can be done by setting the `SupportTableData.data_directory` value. You can override this value for a model by setting the `support_table_data_directory` attribute on its class. In a Rails application, `SupportTableData.data_directory` will be automatically set to `db/support_tables/`. Otherwise, relative file paths will be resolved from the current working directory. You must define the directory to load relative files from before loading your model classes.
58
+
59
+ ### Named Instances
60
+
61
+ You can also automatically define helper methods to load instances and determine if they match specific values. This allows you to add more natural ways of referencing specific records.
62
+
63
+ Named instances are defined if you supply a hash instead of an array in the data files. The hash keys must be valid Ruby method names. Keys that begin with an underscore will not be used to generate named instances. If you only want to create named instances on a few rows in a table, you can add them to an array under an underscored key.
64
+
65
+ Here is an example data file using named instances:
66
+
67
+ ```yaml
68
+ pending:
69
+ id: 1
70
+ name: Pending
71
+ icon: :clock:
72
+
73
+ in_progress:
74
+ id: 2
75
+ name: In Progress
76
+ icon: :construction:
77
+
78
+ completed:
79
+ id: 3
80
+ name: Completed
81
+ icon: :heavy_check_mark:
82
+
83
+ _others:
84
+ - id: 4
85
+ name: Draft
86
+
87
+ - id: 5
88
+ name: Deleted
89
+ ```
90
+
91
+ The hash keys will be used to define helper methods to load and test for specific instances. In this example, our model defines these methods that make it substantially more natural to reference specific instances.
92
+
93
+ ```ruby
94
+ # These methods can be used to load specific instances.
95
+ Status.pending # Status.find_by!(id: 1)
96
+ Status.in_progress # Status.find_by!(id: 2)
97
+ Status.completed # Status.find_by!(id: 3)
98
+
99
+ # These methods can be used to test for specific instances.
100
+ status.pending? # status.id == 1
101
+ status.in_progress? # status.id == 2
102
+ status.completed? # status.id == 3
103
+ ```
104
+
105
+ Helper methods will not override already defined methods on a model class. If a method is already defined, an `ArgumentError` will be raised.
106
+
107
+ ### Caching
108
+
109
+ You can use the companion [support_table_cache gem](https://github.com/bdurand/support_table_cache) to add caching support to your models. That way your application won't need to constantly query the database for records that will never change.
110
+
111
+ ```
112
+ class Status < ApplicationRecord
113
+ include SupportTableData
114
+ include SupportTableCache
115
+
116
+ add_support_table_data "statuses.yml"
117
+
118
+ # Cache lookups when finding by name or by id.
119
+ cache_by :name
120
+ cache_by :id
121
+
122
+ # Cache records in local memory instead of a shared cache for best performance.
123
+ self.support_table_cache = :memory
124
+ end
125
+
126
+ class Thing < ApplicationRecord
127
+ belongs_to :status
128
+
129
+ # Use caching to load the association rather than hitting the database every time.
130
+ cache_belongs_to :status
131
+ end
132
+ ```
133
+
134
+ ### Loading Data
135
+
136
+ Calling `sync_table_data!` on your model class will synchronize the data in the database table with the values from the data files.
137
+
138
+ ```ruby
139
+ Status.sync_table_data!
140
+ ```
141
+
142
+ This will add any missing records to the table and update existing records so that the attributes in the table match the values in the data files. Records that do not appear in the data files will not be touched. Any attributes not specified in the data files will not be changed.
143
+
144
+ The number of records contained in data files should be fairly small (ideally fewer than 100). It is possible to load just a subset of rows in a large table because only the rows listed in the data files will be synced. You can use this feature if your table allows user-entered data, but has a few rows that must exist for the code to work.
145
+
146
+ Loading data is done inside a database transaction. No changes will be persisted to the database unless all rows for a model can be synced.
147
+
148
+ You can synchronize the data in all models by calling `SupportTableData.sync_all!`. This method will discover all ActiveRecord models that include `SupportTableData` and synchronize each of them. (Note that there can be issues discovering all support table models in a Rails application if eager loading is turned off.) The discovery mechanism will try to detect unloaded classes by looking at the file names in the support table data directory so it's best to stick to standard Rails naming conventions for your data files.
149
+
150
+ You need to call `SupportTableData.sync_all!` when deploying your application. This gem includes a rake task `support_table_data:sync` that is suitable for hooking into deploy scripts. An easy way to hook it into a Rails application is by enhancing the `db:migrate` task so that the sync task runs immediately after database migrations are run. You can do this by adding code to a Rakefile in your application's `lib/tasks` directory:
151
+
152
+ ```ruby
153
+ if Rake::Task.task_defined?("db:migrate")
154
+ Rake::Task["db:migrate"].enhance do
155
+ Rake::Task["support_table_data:sync"].invoke
156
+ end
157
+ end
158
+ ```
159
+
160
+ Enhancing the `db:migrate` task also ensures that local development environments will stay up to date.
161
+
162
+ ### Testing
163
+
164
+ You must also call `SupportTableData.sync_all!` before running your test suite. This method should be called in the test suite setup code after any data in the test database has been purged and before any tests are run.
165
+
166
+ ## Installation
167
+
168
+ Add this line to your application's Gemfile:
169
+
170
+ ```ruby
171
+ gem "support_table_data"
172
+ ```
173
+
174
+ Then execute:
175
+ ```bash
176
+ $ bundle
177
+ ```
178
+
179
+ Or install it yourself as:
180
+ ```bash
181
+ $ gem install support_table_data
182
+ ```
183
+
184
+ ## Contributing
185
+
186
+ Open a pull request on [GitHub](https://github.com/bdurand/support_table_data).
187
+
188
+ Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
189
+
190
+ ## License
191
+
192
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SupportTableData
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load File.expand_path("../tasks/support_table_data.rake", __dir__)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This concern can be mixed into models that represent static support tables. These are small tables
4
+ # that have a limited number of rows, and have values that are often tied to the logic in the code.
5
+ #
6
+ # The values that should be in support tables can be defined in YAML, JSON, or CSV files. These
7
+ # values can then be synced to the database and helper methods can be generated from them.
8
+ module SupportTableData
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+ # Synchronize the rows in the table with the values defined in the data files added with
13
+ # `add_support_table_data`. Note that rows will not be deleted if they are no longer in
14
+ # the data files.
15
+ #
16
+ # @return [Array<Hash>] List of saved changes for each record that was created or modified.
17
+ def sync_table_data!
18
+ return unless table_exists?
19
+
20
+ key_attribute = (support_table_key_attribute || primary_key).to_s
21
+ canonical_data = support_table_data.each_with_object({}) { |attributes, hash| hash[attributes[key_attribute].to_s] = attributes }
22
+ records = where(key_attribute => canonical_data.keys)
23
+ changes = []
24
+
25
+ ActiveSupport::Notifications.instrument("support_table_data.sync", class: self) do
26
+ transaction do
27
+ records.each do |record|
28
+ key = record[key_attribute].to_s
29
+ attributes = canonical_data.delete(key)
30
+ attributes&.each do |name, value|
31
+ record.send("#{name}=", value) if record.respond_to?("#{name}=", true)
32
+ end
33
+ if record.changed?
34
+ changes << record.changes
35
+ record.save!
36
+ end
37
+ end
38
+
39
+ canonical_data.each_value do |attributes|
40
+ record = new
41
+ attributes.each do |name, value|
42
+ record.send("#{name}=", value) if record.respond_to?("#{name}=", true)
43
+ end
44
+ changes << record.changes
45
+ record.save!
46
+ end
47
+ end
48
+ end
49
+
50
+ changes
51
+ end
52
+
53
+ # Add a data file that contains the support table data. This method can be called multiple times to
54
+ # load data from multiple files.
55
+ #
56
+ # @param data_file_path [String, Pathname] The path to a YAML, JSON, or CSV file containing data for this model. If
57
+ # the path is a relative path, then it will be resolved from the either the default directory set for
58
+ # this model or the global directory set with SupportTableData.data_directory.
59
+ # @return [void]
60
+ def add_support_table_data(data_file_path)
61
+ @support_table_data_files ||= []
62
+ root_dir = (support_table_data_directory || SupportTableData.data_directory || Dir.pwd)
63
+ @support_table_data_files << File.expand_path(data_file_path, root_dir)
64
+ define_support_table_named_instances
65
+ end
66
+
67
+ # Get the data for the support table from the data files.
68
+ #
69
+ # @return [Array<Hash>] List of attributes for all records in the data files.
70
+ def support_table_data
71
+ @support_table_data_files ||= []
72
+ data = {}
73
+ key_attribute = (support_table_key_attribute || primary_key).to_s
74
+
75
+ @support_table_data_files.each do |data_file_path|
76
+ file_data = support_table_parse_data_file(data_file_path)
77
+ file_data = file_data.values if file_data.is_a?(Hash)
78
+ file_data = Array(file_data).flatten
79
+ file_data.each do |attributes|
80
+ key_value = attributes[key_attribute].to_s
81
+ existing = data[key_value]
82
+ if existing
83
+ existing.merge!(attributes)
84
+ else
85
+ data[key_value] = attributes
86
+ end
87
+ end
88
+ end
89
+
90
+ data.values
91
+ end
92
+
93
+ # Get the names of all named instances.
94
+ #
95
+ # @return [Array<String>] List of all instance names.
96
+ def instance_names
97
+ @support_table_instance_names ||= Set.new
98
+ @support_table_instance_names.to_a
99
+ end
100
+
101
+ # Get the key values for all instances loaded from the data files.
102
+ #
103
+ # @return [Array] List of all the key attribute values.
104
+ def instance_keys
105
+ unless defined?(@support_table_instance_keys)
106
+ key_attribute = (support_table_key_attribute || primary_key).to_s
107
+ values = []
108
+ support_table_data.each do |attributes|
109
+ key_value = attributes[key_attribute]
110
+ instance = new
111
+ instance.send("#{key_attribute}=", key_value)
112
+ values << instance.send(key_attribute)
113
+ end
114
+ @support_table_instance_keys = values.uniq
115
+ end
116
+ @support_table_instance_keys
117
+ end
118
+
119
+ # Return true if the instance has data being managed from a data file.
120
+ #
121
+ # @return [Boolean]
122
+ def protected_instance?(instance)
123
+ key_attribute = (support_table_key_attribute || primary_key).to_s
124
+
125
+ unless defined?(@protected_keys)
126
+ keys = support_table_data.collect { |attributes| attributes[key_attribute].to_s }
127
+ @protected_keys = keys
128
+ end
129
+
130
+ @protected_keys.include?(instance[key_attribute].to_s)
131
+ end
132
+
133
+ private
134
+
135
+ def define_support_table_named_instances
136
+ @support_table_data_files ||= []
137
+ @support_table_instance_names ||= Set.new
138
+ key_attribute = (support_table_key_attribute || primary_key).to_s
139
+
140
+ @support_table_data_files.each do |file_path|
141
+ data = support_table_parse_data_file(file_path)
142
+ if data.is_a?(Hash)
143
+ data.each do |key, attributes|
144
+ method_name = key.to_s.freeze
145
+ next if method_name.start_with?("_")
146
+
147
+ unless attributes.is_a?(Hash)
148
+ raise ArgumentError.new("Cannot define named instance #{method_name} on #{name}; value must be a Hash")
149
+ end
150
+
151
+ unless method_name.match?(/\A[a-z][a-z0-9_]+\z/)
152
+ raise ArgumentError.new("Cannot define named instance #{method_name} on #{name}; name contains illegal characters")
153
+ end
154
+
155
+ unless @support_table_instance_names.include?(method_name)
156
+ @support_table_instance_names << method_name
157
+ key_value = attributes[key_attribute]
158
+ define_support_table_instance_helper(method_name, key_attribute, key_value)
159
+ define_support_table_predicates_helper("#{method_name}?", key_attribute, key_value)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ def define_support_table_instance_helper(method_name, attribute_name, attribute_value)
167
+ if respond_to?(method_name, true)
168
+ raise ArgumentError.new("Could not define support table helper method #{name}.#{method_name} because it is already a defined method")
169
+ end
170
+
171
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
172
+ def self.#{method_name}
173
+ find_by!(#{attribute_name}: #{attribute_value.inspect})
174
+ end
175
+ RUBY
176
+ end
177
+
178
+ def define_support_table_predicates_helper(method_name, attribute_name, attribute_value)
179
+ if method_defined?(method_name) || private_method_defined?(method_name)
180
+ raise ArgumentError.new("Could not define support table helper method #{name}##{method_name} because it is already a defined method")
181
+ end
182
+
183
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
184
+ def #{method_name}
185
+ #{attribute_name} == #{attribute_value.inspect}
186
+ end
187
+ RUBY
188
+ end
189
+
190
+ def support_table_parse_data_file(file_path)
191
+ file_data = File.read(file_path)
192
+
193
+ extension = file_path.split(".").last&.downcase
194
+ data = []
195
+
196
+ case extension
197
+ when "json"
198
+ require "json" unless defined?(JSON)
199
+ data = JSON.parse(file_data)
200
+ when "csv"
201
+ require "csv" unless defined?(CSV)
202
+ CSV.new(file_data, headers: true).each do |row|
203
+ data << row.to_h
204
+ end
205
+ else
206
+ require "yaml" unless defined?(YAML)
207
+ data = YAML.safe_load(file_data)
208
+ end
209
+
210
+ data
211
+ end
212
+ end
213
+
214
+ included do
215
+ # Define the attribute used as the key of the hash in the data files.
216
+ # This should be a value that never changes. By default the key attribute will be the id.
217
+ class_attribute :support_table_key_attribute, instance_accessor: false
218
+
219
+ # Define the directory where data files should be loaded from. This value will override the global
220
+ # value set by SupportTableData.data_directory. This is only used if relative paths are passed
221
+ # in to add_support_table_data.
222
+ class_attribute :support_table_data_directory, instance_accessor: false
223
+ end
224
+
225
+ class << self
226
+ # Specify the default directory for data files.
227
+ attr_writer :data_directory
228
+
229
+ # The directory where data files live by default. If you are running in a Rails environment,
230
+ # then this will be `db/support_tables`. Otherwise, the current working directory will be used.
231
+ #
232
+ # @return [String]
233
+ def data_directory
234
+ if defined?(@data_directory)
235
+ @data_directory
236
+ elsif defined?(Rails.root)
237
+ Rails.root.join("db", "support_tables").to_s
238
+ end
239
+ end
240
+
241
+ # Sync all support table classes. Classes must already be loaded in order to be synced.
242
+ #
243
+ # You can pass in a list of classes that you want to ensure are synced. This feature
244
+ # can be used to force load classes that are only loaded at runtime. For instance, if
245
+ # eager loading is turned off for the test environment in a Rails application (which is
246
+ # the default), then there is a good chance that support table models won't be loaded
247
+ # when the test suite is initializing.
248
+ #
249
+ # @param extra_classes [Class] List of classes to force into the detected list of classes to sync.
250
+ # @return [Hash<Class, Array<Hash>] Hash of classes synced with a list of saved changes.
251
+ def sync_all!(*extra_classes)
252
+ changes = {}
253
+ support_table_classes(*extra_classes).each do |klass|
254
+ changes[klass] = klass.sync_table_data!
255
+ end
256
+ changes
257
+ end
258
+
259
+ # Return the list of all support table classes in the order they should be loaded.
260
+ # Note that this method relies on the classes already having been loaded by the application.
261
+ # It can return indeterminate results if eager loading is turned off (i.e. development
262
+ # or test mode in a Rails application).
263
+ #
264
+ # If any data files exist in the default data directory, any class name that matches
265
+ # the file name will attempt to be loaded (i.e. "task/statuses.yml" will attempt to
266
+ # load the `Task::Status` class if it exists).
267
+ #
268
+ # You can also pass in a list of classes that you explicitly want to include in the returned list.
269
+ #
270
+ # @param extra_classes [Class] List of extra classes to include in the return list.
271
+ # @return [Array<Class>] List of classes in the order they should be loaded.
272
+ # @api private
273
+ def support_table_classes(*extra_classes)
274
+ classes = []
275
+ extra_classes.flatten.each do |klass|
276
+ unless klass.is_a?(Class) && klass.include?(SupportTableData)
277
+ raise ArgumentError.new("#{klass} does not include SupportTableData")
278
+ end
279
+ classes << klass
280
+ end
281
+
282
+ # Eager load any classes defined in the default data directory by guessing class names
283
+ # from the file names.
284
+ if SupportTableData.data_directory && File.exist?(SupportTableData.data_directory) && File.directory?(SupportTableData.data_directory)
285
+ Dir.chdir(SupportTableData.data_directory) { Dir.glob(File.join("**", "*")) }.each do |file_name|
286
+ class_name = file_name.sub(/\.[^.]*/, "").singularize.camelize
287
+ class_name.safe_constantize
288
+ end
289
+ end
290
+
291
+ ActiveRecord::Base.descendants.sort_by(&:name).each do |klass|
292
+ next unless klass.include?(SupportTableData)
293
+ next if klass.abstract_class?
294
+ next if classes.include?(klass)
295
+ classes << klass
296
+ end
297
+
298
+ levels = [classes]
299
+ checked = Set.new
300
+ loop do
301
+ checked << classes
302
+ dependencies = classes.collect { |klass| support_table_dependencies(klass) }.flatten.uniq.sort_by(&:name)
303
+ break if dependencies.empty? || checked.include?(dependencies)
304
+ levels.unshift(dependencies)
305
+ classes = dependencies
306
+ end
307
+
308
+ levels.flatten.uniq
309
+ end
310
+
311
+ private
312
+
313
+ # Extract support table dependencies from the belongs to associations on a class.
314
+ #
315
+ # @return [Array<Class>]
316
+ def support_table_dependencies(klass)
317
+ dependencies = []
318
+ klass.reflections.values.select(&:belongs_to?).each do |reflection|
319
+ if reflection.klass.include?(SupportTableData) && !(reflection.klass <= klass)
320
+ dependencies << reflection.klass
321
+ end
322
+ end
323
+ dependencies
324
+ end
325
+ end
326
+
327
+ # Return true if this instance has data being managed from a data file. You can add validation
328
+ # logic using this information if you want to prevent the application from updating protected instances.
329
+ #
330
+ # @return [Boolean]
331
+ def protected_instance?
332
+ self.class.protected_instance?(self)
333
+ end
334
+ end
335
+
336
+ if defined?(Rails::Railtie)
337
+ require_relative "support_table_data/railtie"
338
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :support_table_data do
4
+ desc "Syncronize data for all models that include SupportTableData."
5
+ task sync: :environment do
6
+ # Eager load models if we are in a Rails enviroment with eager loading turned off.
7
+ if defined?(Rails.application)
8
+ unless Rails.application.config.eager_load
9
+ if defined?(Rails.application.eager_load!)
10
+ Rails.application.eager_load!
11
+ elsif defined?(Rails.autoloaders.zeitwerk_enabled?) && Rails.autoloaders.zeitwerk_enabled?
12
+ Rails.autoloaders.each(&:eager_load)
13
+ else
14
+ warn "Could not eager load models; some support table data may not load"
15
+ end
16
+ end
17
+ end
18
+
19
+ logger_callback = lambda do |name, started, finished, unique_id, payload|
20
+ klass = payload[:class]
21
+ elapsed_time = finished - started
22
+ message = "Synchronized support table model #{klass.name} in #{(elapsed_time * 1000).round}ms"
23
+ if klass.logger
24
+ klass.logger.info(message)
25
+ else
26
+ puts message
27
+ end
28
+ end
29
+
30
+ ActiveSupport::Notifications.subscribed(logger_callback, "support_table_data.sync") do
31
+ SupportTableData.sync_all!
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "support_table_data"
3
+ spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
4
+ spec.authors = ["Brian Durand"]
5
+ spec.email = ["bbdurand@gmail.com"]
6
+
7
+ spec.summary = "Extension for ActiveRecord models to manage synchronizing data in support/lookup tables across environments. Also provides the ability to directly reference and test for specific rows in these tables."
8
+
9
+ spec.homepage = "https://github.com/bdurand/support_table_data"
10
+ spec.license = "MIT"
11
+
12
+ # Specify which files should be added to the gem when it is released.
13
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
14
+ ignore_files = %w[
15
+ .
16
+ Appraisals
17
+ Gemfile
18
+ Gemfile.lock
19
+ Rakefile
20
+ bin/
21
+ gemfiles/
22
+ spec/
23
+ ]
24
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
26
+ end
27
+
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "activerecord"
31
+
32
+ spec.add_development_dependency "bundler"
33
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: support_table_data
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Durand
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-11-02 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: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - bbdurand@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - VERSION
52
+ - lib/support_table_data.rb
53
+ - lib/support_table_data/railtie.rb
54
+ - lib/tasks/support_table_data.rake
55
+ - support_table_data.gemspec
56
+ homepage: https://github.com/bdurand/support_table_data
57
+ licenses:
58
+ - MIT
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.0.3
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Extension for ActiveRecord models to manage synchronizing data in support/lookup
79
+ tables across environments. Also provides the ability to directly reference and
80
+ test for specific rows in these tables.
81
+ test_files: []