rails_dictionary 0.3.pre.rc1 → 0.4.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.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +33 -0
  3. data/.gitignore +5 -3
  4. data/CHANGELOG +19 -3
  5. data/Gemfile +10 -3
  6. data/Gemfile.lock +350 -0
  7. data/{README.v2.0.rdoc → README.rdoc} +33 -15
  8. data/Rakefile +10 -4
  9. data/docs/load-order.md +118 -0
  10. data/lib/rails_dictionary/active_record_extension.rb +48 -0
  11. data/lib/rails_dictionary/acts_as_dict_slave.rb +85 -0
  12. data/lib/rails_dictionary/acts_as_dict_type.rb +53 -0
  13. data/lib/rails_dictionary/acts_as_dictionary.rb +118 -0
  14. data/lib/rails_dictionary/array_core_ext.rb +10 -0
  15. data/lib/rails_dictionary/railtie.rb +11 -0
  16. data/lib/rails_dictionary/version.rb +1 -1
  17. data/lib/rails_dictionary.rb +64 -20
  18. data/lib/tasks/dicts.rake +9 -11
  19. data/pkg/rails_dictionary-0.2.2.gem +0 -0
  20. data/pkg/rails_dictionary-0.2.3.gem +0 -0
  21. data/rails_dictionary.gemspec +4 -4
  22. data/spec/fake_app.rb +29 -0
  23. data/spec/init_models.rb +12 -0
  24. data/spec/load_order/empty_boot_recovers.rb +19 -0
  25. data/spec/load_order/lazy_slave_model_boot.rb +25 -0
  26. data/spec/load_order/preseeded_boot.rb +22 -0
  27. data/spec/load_order/support.rb +41 -0
  28. data/spec/rails_dictionary_spec.rb +172 -0
  29. data/spec/spec_helper.rb +22 -0
  30. data/v1.0-roadmap.md +31 -0
  31. metadata +37 -38
  32. data/Readme.md +0 -10
  33. data/lib/rails_dictionary/models/active_record_extension.rb +0 -38
  34. data/lib/rails_dictionary/models/acts_as_dict_consumer.rb +0 -77
  35. data/lib/rails_dictionary/models/acts_as_dictionary.rb +0 -27
  36. data/test/acts_as_consumer_test.rb +0 -44
  37. data/test/fake_app.rb +0 -40
  38. data/test/lookup_test.rb +0 -39
  39. data/test/rails_dictionary_test.rb +0 -81
  40. data/test/test_helper.rb +0 -36
@@ -0,0 +1,118 @@
1
+ # Load & Seed Order
2
+
3
+ This document explains how the per-type lookup methods
4
+ (`Dictionary.address_city`, `Dictionary.student_school`, ...) come to exist,
5
+ and why earlier versions broke depending on **when** models were loaded and
6
+ **when** `DictType` rows were written. If you hit a `NoMethodError: undefined
7
+ method 'address_city' for class Dictionary`, this is the page to read.
8
+
9
+ ## Background: how lookups are generated
10
+
11
+ Up to 0.2.x, `Dictionary.address_city` was resolved lazily at call time through
12
+ `method_missing`. Every call re-checked `DictType.all_types`, so it never
13
+ mattered when the data was seeded — the first call always worked.
14
+
15
+ From 0.3.0 onward the lookups are **real singleton methods**, generated eagerly
16
+ by `Dictionary.reload_dict_methods`, which walks `DictType.all_types` and calls
17
+ `define_singleton_method` for each category. This is faster and introspectable
18
+ (`Dictionary.respond_to?(:address_city)`), but it means the methods only exist
19
+ once generation has actually run for a given model.
20
+
21
+ Generation is triggered from three places:
22
+
23
+ 1. **Boot** — the Railtie's `config.to_prepare` calls
24
+ `RailsDictionary.load_dict_methods` after the app initializes.
25
+ 2. **Runtime writes** — `DictType` has `after_save`/`after_destroy
26
+ :delete_all_caches`, which resets the cached type list and calls
27
+ `RailsDictionary.reload_dict_methods`.
28
+ 3. **Model load** (added in 0.4.0) — when a model runs `acts_as_dictionary`,
29
+ its `included` hook generates its own lookups from current data.
30
+
31
+ `RailsDictionary.reload_dict_methods` only regenerates methods on models that
32
+ have **registered themselves**. A model registers (appends to
33
+ `RailsDictionary.dictionary_model_names`) the moment its class body runs
34
+ `acts_as_dictionary`. With `config.eager_load = false` (development, test,
35
+ console, runner) that does not happen until the constant is first referenced.
36
+
37
+ ## The two moving parts
38
+
39
+ Method generation needs **both** of these to be true at the moment it runs:
40
+
41
+ - the dictionary model (`Dictionary`) is **loaded/registered**, and
42
+ - the `dict_types` table **exists and contains the rows** you expect.
43
+
44
+ Every historical bug is some ordering where one of those was not yet true when
45
+ the only generation trigger fired, and nothing fired again afterward.
46
+
47
+ ## Scenarios
48
+
49
+ Each scenario below has an isolated, single-process reproduction under
50
+ `spec/load_order/` (the shared rspec suite can't exercise boot ordering because
51
+ its tables and models already exist). Run them with `bundle exec rake
52
+ load_order`.
53
+
54
+ ### 1. Pre-seeded boot (normal production) — `preseeded_boot.rb`
55
+
56
+ `dict_types` is migrated and populated **before** the app boots. Seeding via
57
+ `insert`/SQL skips callbacks, so trigger #2 never fires. The methods must come
58
+ from trigger #1 (boot). Works as long as the model is reachable at
59
+ `to_prepare`, which it is under eager loading in production.
60
+
61
+ ### 2. Empty boot, data arrives later — `empty_boot_recovers.rb`
62
+
63
+ The app boots while `dict_types` does **not exist yet** (e.g. before the first
64
+ migration, or during `db:create`). Generation at boot must not raise — it is
65
+ guarded by `RailsDictionary.dict_table_ready?`. No methods exist yet. Once the
66
+ table is created and a `DictType` is **created** (not bulk-inserted), trigger #2
67
+ regenerates and the lookup appears.
68
+
69
+ ### 3. Slave/lookup model loaded after seeding — `lazy_slave_model_boot.rb`
70
+
71
+ This is the bug that motivated 0.4.0, and the one a host app's
72
+ seed/fixture file hits. With lazy loading:
73
+
74
+ - `DictType` gets referenced and seeded first (`DictType.create!(...)`).
75
+ - At that point `Dictionary` has **never been referenced**, so it is not
76
+ registered. Trigger #2 fires but finds an empty model registry and defines
77
+ nothing.
78
+ - The host then references `Dictionary` for the first time. Before 0.4.0 the
79
+ `included` hook only registered the model; it did **not** generate methods,
80
+ so `Dictionary.address_city` raised. A *second* call could succeed only if
81
+ some later reload happened to run after registration (e.g. the dev reloader
82
+ re-running `to_prepare`), which is why the failure looked intermittent.
83
+
84
+ 0.4.0 fixes this with trigger #3: the model generates its own lookups in its
85
+ `included` hook, so load order between `DictType`, `Dictionary`, and seeding no
86
+ longer matters.
87
+
88
+ ### 4. Reverse load order (`Dictionary` before `DictType`)
89
+
90
+ If the dictionary model loads before the `DictType` constant is defined, trigger
91
+ #3 is **skipped** (guarded by `Object.const_defined?(:DictType)`) rather than
92
+ raising. Generation then happens at the next boot hook or the next `DictType`
93
+ write. The gem's own `spec/init_models.rb` defines `Dictionary` first and
94
+ relies on this.
95
+
96
+ ## Guarantees after 0.4.0
97
+
98
+ - Boot with a missing/unmigrated DB never raises (`dict_table_ready?`).
99
+ - A model loaded after its data was seeded gets its lookups immediately on load.
100
+ - A model loaded before `DictType` exists defers cleanly and recovers at the
101
+ next boot hook or `DictType` write.
102
+ - Runtime `DictType.create!`/`destroy` keeps categories in sync via callbacks.
103
+
104
+ ## If you still see a missing lookup
105
+
106
+ Force a regeneration and inspect state:
107
+
108
+ ```ruby
109
+ RailsDictionary.dictionary_model_names # is your model registered?
110
+ RailsDictionary.dict_table_ready? # can the table be queried?
111
+ DictType.all_types # are the categories present?
112
+ RailsDictionary.reload_dict_methods # regenerate now
113
+ Dictionary.respond_to?(:address_city) # => true
114
+ ```
115
+
116
+ Bulk seeding (`insert_all`, fixtures, raw SQL) bypasses the `after_save`
117
+ callback. If you seed that way after boot, call
118
+ `RailsDictionary.reload_dict_methods` yourself afterward.
@@ -0,0 +1,48 @@
1
+ module RailsDictionary
2
+ module ActiveRecordExtension
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ # TODO: move macro define in each module file
9
+ # See Usage in readme.doc.
10
+ def acts_as_dict_type
11
+
12
+ has_many :dictionaries
13
+ validates_uniqueness_of :name
14
+ after_save :delete_all_caches
15
+ after_destroy :delete_all_caches
16
+
17
+ include RailsDictionary::ActsAsDictType
18
+ end
19
+
20
+ def acts_as_dictionary
21
+
22
+ belongs_to :dict_type
23
+ after_save :delete_dicts_cache
24
+ after_destroy :delete_dicts_cache
25
+ scope :dict_type_name_eq, ->(name) { joins(:dict_type).where({ "dict_types.name" => name }) }
26
+
27
+ include RailsDictionary::ActsAsDictionary
28
+ end
29
+
30
+ # Ex: acts_as_dict_slave :add => :category
31
+ # :except - remove dict mapping column
32
+ # :add - add dict mapping column
33
+ # :locale - add and initialize class attribute default_dict_locale
34
+ def acts_as_dict_slave(ops={})
35
+ include RailsDictionary::ActsAsDictSlave
36
+ class_attribute :default_dict_locale, :instance_writer => false
37
+ cattr_accessor :dict_mapping_columns, :instance_writer => false
38
+ self.default_dict_locale = ops[:locale] if ops[:locale]
39
+ self.dict_mapping_columns = dict_columns(ops)
40
+ unless dict_mapping_columns.nil?
41
+ add_dynamic_column_method
42
+ end
43
+ end
44
+
45
+ alias_method :acts_as_dict_consumer, :acts_as_dict_slave
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,85 @@
1
+ module RailsDictionary
2
+ module ActsAsDictSlave
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ # return columns that exist in DictType#tab_and_column
7
+ def columns_in_dict_type
8
+ if ActiveRecord::VERSION::STRING < '3.1'
9
+ DictType.tab_and_column[self.name.underscore.to_sym]
10
+ elsif ::DictType.table_exists?
11
+ DictType.tab_and_column[self.name.underscore.to_sym]
12
+ else
13
+ []
14
+ end
15
+ end
16
+
17
+ # columns which map to dictionary
18
+ def dict_columns(ops={})
19
+ conf = { except: nil, add: nil}
20
+ conf.update(ops)
21
+ cidt = columns_in_dict_type || []
22
+ cidt.delete(conf[:except])
23
+ case conf[:add]
24
+ when String
25
+ cidt.push(conf[:add])
26
+ when Array
27
+ cidt.push(*conf[:add])
28
+ else nil
29
+ end
30
+ cidt.uniq! || cidt
31
+ end
32
+
33
+ # add a belongs_to(Dictionary) association and a named_{column} method
34
+ def add_dynamic_column_method
35
+ dict_mapping_columns.each { |e| belongs_to "#{e}_dict".to_sym, class_name: "Dictionary", foreign_key: e.to_sym }
36
+ dict_mapping_columns.each { |ele| named_dict_value ele.to_sym }
37
+ dict_mapping_columns.each { |ele| dict_name_equal ele.to_sym }
38
+ end
39
+
40
+ # Generate dynamic instance method named_column to consumer model
41
+ # def named_city(locale=nil)
42
+ # locale = locale.presence || default_dict_locale.presence || :en
43
+ # locale = "name_#{locale}"
44
+ # self.send(city_dict).try(:send,locale)
45
+ # end
46
+ # alias_method :city_name, :named_city
47
+ def named_dict_value(method_name)
48
+ belongs_to_name="#{method_name}_dict".to_sym
49
+ origin_method_name = method_name
50
+ method_name="named_#{method_name}"
51
+ define_method(method_name) do | locale=nil |
52
+ locale = locale.presence || default_dict_locale.presence || :en
53
+ locale = "name_#{locale}"
54
+ self.send(belongs_to_name).try(:send,locale)
55
+ end
56
+ alias_method "#{origin_method_name}_name".to_sym, method_name.to_sym
57
+ end
58
+
59
+ # Build dynamic method column_name= to the consumer model
60
+ #
61
+ # def city_name=(value, options = {})
62
+ #
63
+ #
64
+ # end
65
+ def dict_name_equal(colname)
66
+ method_name = "#{colname}_name="
67
+ belongs_to_name="#{colname}_dict".to_sym
68
+ define_method(method_name) do |value, options={}|
69
+ options.merge!(name_en: value)
70
+ dict_type_id = DictType.revert("#{self.class.table_name.singularize}_#{colname}")
71
+ options.merge!(dict_type_id: dict_type_id)
72
+ exist_dictionary = Dictionary.where(options)
73
+ if exist_dictionary.present?
74
+ exist_id = exist_dictionary.first.id
75
+ else
76
+ exist_id = send("create_#{belongs_to_name}!", options).id
77
+ end
78
+ send "#{colname}=", exist_id
79
+ end
80
+ end
81
+
82
+ end # END class_methods
83
+
84
+ end
85
+ end
@@ -0,0 +1,53 @@
1
+ module RailsDictionary
2
+ module ActsAsDictType
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ base.mattr_accessor :whole_types
6
+ end
7
+
8
+ module ClassMethods
9
+
10
+ def all_types
11
+ whole_types.presence ||
12
+ self.whole_types = all.map(&:name).map(&:to_sym)
13
+ end
14
+
15
+ # short method to transfer id to name or name to id
16
+ # TODO: cache it
17
+ def revert(arg)
18
+ if arg.is_a?(String)
19
+ DictType.where(name: arg).first.try(:id)
20
+ elsif arg.is_a?(Integer)
21
+ DictType.where(id: arg).first.try(:name)
22
+ end
23
+ end
24
+
25
+ #
26
+ # Parse the name value to get which column and model(or table) are listed in DictType
27
+ #
28
+ # Programmer DOC:
29
+ # There are two chooses to get subclass,one is subclasses the other is descendants,
30
+ # I don't know which is better,but descendants contains subclass of subclass,it contains more.
31
+ #
32
+ # Class like +Ckeditor::Asset+ transfer to "ckeditor/asset",but we can not naming method like that,
33
+ # So it still not support, the solution may be simple,just make another convention to escape "/"
34
+ #
35
+ # Seems this method did not need to be cached in production.
36
+ # Because everyclass was cached before application was run.So after application was run, it never be run again.
37
+ # TODO:
38
+ # To cache this method output need more skills on how to caculate ActiveRecord::Base.descendants
39
+ # Temply remove the cache
40
+ # And add test for this situation
41
+ def tab_and_column
42
+ all_model_class=ActiveRecord::Base.descendants.map(&:name).map(&:underscore)
43
+ RailsDictionary.extract_to_hash(all_types.map(&:to_s), all_model_class)
44
+ end
45
+ end
46
+
47
+ def delete_all_caches
48
+ self.whole_types = nil
49
+ RailsDictionary.reload_dict_methods
50
+ return true
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,118 @@
1
+ require "active_support/concern"
2
+
3
+ module RailsDictionary
4
+ module ActsAsDictionary
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ RailsDictionary.register_dictionary_model(self)
9
+ # A model loaded after the Railtie's boot-time hook (the common case
10
+ # under lazy autoloading) would otherwise have no lookup methods until
11
+ # the next DictType write or reload. Generate them now from current data
12
+ # so load order between this model and DictType never matters. Skipped
13
+ # when DictType isn't defined yet (reverse load order) -- the boot hook
14
+ # or the next DictType write will generate them then.
15
+ reload_dict_methods if Object.const_defined?(:DictType) && RailsDictionary.dict_table_ready?
16
+ end
17
+
18
+ class_methods do
19
+
20
+ # Define one singleton method per dict type, replacing the old
21
+ # method_missing dispatch. Called at boot (Railtie) and whenever a
22
+ # DictType row is written, so runtime-added categories stay available.
23
+ #
24
+ # Dictionary.student_city # => [<Dictionary>, ...]
25
+ # Dictionary.student_city(locale: :zh) # => [["北京", 2], ...] for select
26
+ def reload_dict_methods
27
+ ::DictType.all_types.each { |name| define_dict_method(name) }
28
+ end
29
+
30
+ # List all distinct categories. Useful for admin UIs.
31
+ def categories
32
+ ::DictType.all_types
33
+ end
34
+
35
+ # Add an entry to a category. Returns the created record.
36
+ # Dictionary.add(:city, "New York")
37
+ def add(category, name, attrs = {})
38
+ dict_type_id = ::DictType.revert(category.to_s)
39
+ create!(attrs.merge(name_en: name, dict_type_id: dict_type_id))
40
+ end
41
+
42
+ # Remove every entry in a category matching the given name.
43
+ # Dictionary.remove(:city, "New York")
44
+ def remove(category, name)
45
+ dict_type_id = ::DictType.revert(category.to_s)
46
+ where(name_en: name, dict_type_id: dict_type_id).destroy_all
47
+ end
48
+
49
+ # Form-ready output for options_for_select: [[label, id], ...].
50
+ # Dictionary.options_for(:city, locale: :en)
51
+ def options_for(category, locale: :en)
52
+ public_send(category.to_s.downcase)
53
+ .map { |d| [d.send("name_#{locale}"), d.id] }
54
+ .reject { |pair| pair.first.nil? }
55
+ end
56
+
57
+ # Override this method to get customed sort block
58
+ def sort_dicts(options)
59
+ if options.keys.include? :locale or options.keys.include? "locale"
60
+ locale="name_#{ options[:locale] }"
61
+ if options[:locale].to_sym == :zh
62
+ Proc.new { |a,b| a.send(locale).encode('GBK') <=> b.send(locale).encode('GBK') }
63
+ else
64
+ Proc.new { |a,b| a.send(locale).downcase <=> b.send(locale).downcase }
65
+ end
66
+ else
67
+ false
68
+ end
69
+ end
70
+
71
+ def dict_cache_key(name)
72
+ "Dictionary.#{connection_db_config.database}.#{name}"
73
+ end
74
+
75
+ private
76
+
77
+ def define_dict_method(method_id)
78
+ method_name = method_id.to_s.downcase
79
+ build_scope_method(method_id)
80
+ define_singleton_method(method_name) do |options = {}|
81
+ key = dict_cache_key(method_name)
82
+ Rails.cache.delete(key) if options[:query]
83
+ Rails.cache.fetch(key) { dict_type_name_eq(method_name).to_a }
84
+ listed_attr = Rails.cache.read(key).dup
85
+ if options.keys.include? :locale or options.keys.include? "locale"
86
+ RailsDictionary.deprecator.warn(
87
+ "Passing :locale to Dictionary.#{method_name} is deprecated; " \
88
+ "use Dictionary.options_for(:#{method_name}, locale: ...) instead."
89
+ )
90
+ locale = "name_#{ options[:locale] }"
91
+ sort_block = sort_dicts(options)
92
+ listed_attr.sort!(&sort_block) if sort_block
93
+ listed_attr.map! { |a| [a.send(locale), a.id] }.reject! { |ele| ele.first.nil? }
94
+ end
95
+ listed_attr
96
+ end
97
+ end
98
+
99
+ def build_scope_method(name)
100
+ scope_method_name = "scoped_#{name}".to_sym
101
+ unless respond_to? scope_method_name
102
+ define_singleton_method scope_method_name do
103
+ # see http://stackoverflow.com/questions/18198963/with-rails-4-model-scoped-is-deprecated-but-model-all-cant-replace-it
104
+ # for usage of where(nil)
105
+ dict_type_name_eq(name).where(nil)
106
+ end
107
+ end
108
+ end
109
+
110
+ end # End class_method
111
+
112
+ def delete_dicts_cache
113
+ method_name = ::DictType.revert(self.dict_type_id)
114
+ Rails.cache.delete(self.class.dict_cache_key(method_name))
115
+ return true
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,10 @@
1
+ class Array
2
+ # Deprecated: the Array monkey-patch will be removed in a future major
3
+ # release. The real logic now lives in RailsDictionary.extract_to_hash.
4
+ def extract_to_hash(keys_array)
5
+ RailsDictionary.deprecator.warn(
6
+ "Array#extract_to_hash is deprecated; use RailsDictionary.extract_to_hash instead."
7
+ )
8
+ RailsDictionary.extract_to_hash(self, keys_array)
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module RailsDictionary
2
+ class Railtie < Rails::Railtie
3
+ # Generate the dynamic lookup methods after the app (and its autoloaded
4
+ # models) are ready. The actual call is guarded in
5
+ # RailsDictionary.load_dict_methods so a missing/unmigrated database
6
+ # during boot, asset builds, or db:create never raises.
7
+ config.to_prepare do
8
+ RailsDictionary.load_dict_methods
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsDictionary
2
- VERSION = "0.3-rc1"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -1,33 +1,77 @@
1
- require File.join(File.dirname(__FILE__), "rails_dictionary/models/active_record_extension")
2
- require File.join(File.dirname(__FILE__), "rails_dictionary/models/acts_as_dictionary")
3
- require File.join(File.dirname(__FILE__), "rails_dictionary/models/acts_as_dict_consumer")
4
-
5
1
  # rake tasks not autoload in Rails4
6
- # todo: may add migration(from 0.2 to 0.3) task
7
- # Dir[File.expand_path('../tasks/**/*.rake',__FILE__)].each { |ext| load ext } if defined?(Rake)
2
+ Dir[File.expand_path('../tasks/**/*.rake',__FILE__)].each { |ext| load ext } if defined?(Rake)
8
3
 
9
4
  module RailsDictionary
10
-
11
- def self.config
12
- Config.instance
5
+ # Models that called +acts_as_dictionary+. Stored by name so the registry
6
+ # survives Zeitwerk reloads in development.
7
+ def self.dictionary_model_names
8
+ @dictionary_model_names ||= []
13
9
  end
14
10
 
15
- class Config < Struct.new(:dictionary_klass, :defined_sti_klass)
16
- include Singleton
11
+ def self.register_dictionary_model(klass)
12
+ return if klass.name.nil?
13
+ dictionary_model_names << klass.name unless dictionary_model_names.include?(klass.name)
17
14
  end
18
15
 
19
- config.dictionary_klass = :Dictionary
20
- config.defined_sti_klass = []
16
+ # Gem-specific deprecator so warnings can be silenced/raised independently
17
+ # of the host app. Silenced by default: the removal version is not decided
18
+ # yet, so we don't nag apps. Re-enable with
19
+ # `RailsDictionary.deprecator.silenced = false`.
20
+ def self.deprecator
21
+ @deprecator ||= ActiveSupport::Deprecation.new("a future major release", "rails_dictionary").tap do |deprecator|
22
+ deprecator.silenced = true
23
+ end
24
+ end
21
25
 
22
- def self.dclass
23
- @dclass ||= config.dictionary_klass.to_s.constantize
26
+ # Internal implementation of the (deprecated) Array#extract_to_hash, kept
27
+ # here so the gem's own callers don't trip the deprecation warning.
28
+ def self.extract_to_hash(array, keys_array)
29
+ ret_hash = {}
30
+ keys_array.each { |ky| ret_hash[ky.to_sym] = [] }
31
+ array.each do |sf|
32
+ keys_array.each do |ky|
33
+ ret_hash[ky.to_sym] << sf.sub("#{ky}_", "") if sf =~ Regexp.new("^#{ky}_")
34
+ end
35
+ end
36
+ ret_hash.reject { |_k, v| v.blank? }
24
37
  end
25
38
 
26
- def self.init_dict_sti_class(klass)
27
- unless config.defined_sti_klass.include?(klass) || Module.const_defined?(klass)
28
- subklass = klass.sub "#{config.dictionary_klass}::", ''
29
- dclass.const_set subklass, Class.new(dclass)
30
- config.defined_sti_klass.push(klass)
39
+ # Regenerate the per-type lookup methods on every registered dictionary
40
+ # model. Called whenever a DictType row changes.
41
+ def self.reload_dict_methods
42
+ dictionary_model_names.each do |name|
43
+ klass = name.safe_constantize
44
+ klass&.reload_dict_methods
31
45
  end
32
46
  end
47
+
48
+ # Boot-time entry point (used by the Railtie). Guarded so a missing or
49
+ # unmigrated database during boot, asset builds, or db:create never raises.
50
+ def self.load_dict_methods
51
+ reload_dict_methods if dict_table_ready?
52
+ end
53
+
54
+ # True only when the dict_types table can actually be queried. Used to guard
55
+ # method generation so a missing/unmigrated database during boot, asset
56
+ # builds, or db:create never raises.
57
+ def self.dict_table_ready?
58
+ ActiveRecord::Base.connection.table_exists?("dict_types")
59
+ rescue ActiveRecord::NoDatabaseError,
60
+ ActiveRecord::StatementInvalid,
61
+ ActiveRecord::ConnectionNotEstablished
62
+ false
63
+ end
33
64
  end
65
+
66
+ require File.join(File.dirname(__FILE__), "rails_dictionary/array_core_ext")
67
+
68
+ ActiveSupport.on_load :active_record do
69
+ require File.join(File.dirname(__FILE__), "rails_dictionary/active_record_extension")
70
+ require File.join(File.dirname(__FILE__), "rails_dictionary/acts_as_dict_type")
71
+ require File.join(File.dirname(__FILE__), "rails_dictionary/acts_as_dictionary")
72
+ require File.join(File.dirname(__FILE__), "rails_dictionary/acts_as_dict_slave")
73
+
74
+ ::ActiveRecord::Base.send :include, RailsDictionary::ActiveRecordExtension
75
+ end
76
+
77
+ require File.join(File.dirname(__FILE__), "rails_dictionary/railtie") if defined?(Rails::Railtie)
data/lib/tasks/dicts.rake CHANGED
@@ -2,22 +2,20 @@
2
2
  namespace :dicts do
3
3
  desc "Generate dictionary and dict_type model"
4
4
  task :generate do
5
- system "rails g model dictionary name_en name_zh name_fr type"
5
+ system "rails g model dictionary name_en:string name_zh:string name_fr:string dict_type_id:integer"
6
+ system "rails g model dict_type name:string"
6
7
  end
7
-
8
8
  desc "Generate student model"
9
- task :sample_consumer do
9
+ task :sample_slave do
10
10
  system "rails g model student email:string city:integer school:integer"
11
11
  end
12
-
13
12
  desc "Generate sample data for rails_dictionary gem"
14
13
  task :sample_data => [:environment] do
15
- # @dt_stu_city=DictType.create! name: "student_city"
16
- # @dt_stu_school=DictType.create! name: "student_school"
17
- @dy_shanghai=Dictionary.create! name_en: "shanghai", name_zh: "上海", name_fr: "shanghai", dict_type_id: @dt_stu_city.id
18
- @dy_beijing=Dictionary.create! name_en: "beijing", name_zh: "北京", name_fr: "Pékin", dict_type_id: @dt_stu_city.id
19
- @stu_beijing=Student.create! email: "beijing@dict.com", city: @dy_beijing.id
20
- @stu_shanghai=Student.create! email: "shanghai@dict.com", city: @dy_shanghai.id
14
+ @dt_stu_city=DictType.create! :name => "student_city"
15
+ @dt_stu_school=DictType.create! :name => "student_school"
16
+ @dy_shanghai=Dictionary.create! name_en: "shanghai",name_zh: "上海",name_fr: "shanghai",dict_type_id: @dt_stu_city.id
17
+ @dy_beijing=Dictionary.create! name_en: "beijing",name_zh: "北京",name_fr: "Pékin",dict_type_id: @dt_stu_city.id
18
+ @stu_beijing=Student.create! email: "beijing@dict.com",city: @dy_beijing.id
19
+ @stu_shanghai=Student.create! email: "shanghai@dict.com",city: @dy_shanghai.id
21
20
  end
22
-
23
21
  end
Binary file
Binary file
@@ -7,17 +7,17 @@ Gem::Specification.new do |s|
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ["Raykin Lee"]
9
9
  s.email = ["raykincoldxiao@campus.com"]
10
+ s.licenses = ['MIT']
10
11
  s.homepage = "https://github.com/raykin/rails_dictionary"
11
12
  s.summary = %q{dictionary data for web application}
12
13
  s.description = %q{Rails plugin for mapping static data of web application to Dictionary class}
13
14
 
14
- s.rubyforge_project = "rails_dictionary"
15
15
 
16
- s.add_runtime_dependency 'rails', '> 4.0'
17
- s.add_runtime_dependency 'database_cleaner'
16
+ s.add_runtime_dependency 'rails', '>= 7.1', '< 9'
18
17
 
19
18
  s.files = `git ls-files`.split("\n")
20
- s.test_files = `git ls-files -- {test}/*`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
20
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
21
  s.require_paths = ["lib"]
22
+ s.required_ruby_version = '>= 3.3.0'
23
23
  end
data/spec/fake_app.rb ADDED
@@ -0,0 +1,29 @@
1
+ # database
2
+ ActiveRecord::Base.configurations = {'test' => {:adapter => 'sqlite3', :database => ':memory:'}}
3
+
4
+ if ActiveRecord::VERSION::MAJOR >= 5
5
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
6
+ else
7
+ ActiveRecord::Base.establish_connection(:test)
8
+ end
9
+
10
+ # config
11
+ app = Class.new(Rails::Application)
12
+ app.config.active_support.deprecation = :log
13
+ app.config.eager_load = false
14
+ app.initialize!
15
+
16
+ #migrations
17
+ class CreateAllTables < ActiveRecord::VERSION::MAJOR >= 5 ? ActiveRecord::Migration[5.0] : ActiveRecord::Migration
18
+ def self.up
19
+ create_table(:dict_types) {|t| t.string :name}
20
+ create_table(:dictionaries) {|t| t.string :name_en; t.string :name_zh ; t.string :name_fr ; t.integer :dict_type_id}
21
+ create_table(:students) {|t| t.string :email; t.integer :city; t.integer :school}
22
+ end
23
+
24
+ def self.down
25
+ drop_table :dict_types
26
+ drop_table :dictionaries
27
+ drop_table :students
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ class Dictionary < ActiveRecord::Base
2
+ acts_as_dictionary
3
+ end
4
+
5
+ class DictType < ActiveRecord::Base
6
+ acts_as_dict_type
7
+ end
8
+
9
+ # TODO: Here is a serious code loading issue. Student can't be load before DictType and Dictionary
10
+ class Student < ActiveRecord::Base
11
+ acts_as_dict_slave
12
+ end