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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +33 -0
- data/.gitignore +5 -3
- data/CHANGELOG +19 -3
- data/Gemfile +10 -3
- data/Gemfile.lock +350 -0
- data/{README.v2.0.rdoc → README.rdoc} +33 -15
- data/Rakefile +10 -4
- data/docs/load-order.md +118 -0
- data/lib/rails_dictionary/active_record_extension.rb +48 -0
- data/lib/rails_dictionary/acts_as_dict_slave.rb +85 -0
- data/lib/rails_dictionary/acts_as_dict_type.rb +53 -0
- data/lib/rails_dictionary/acts_as_dictionary.rb +118 -0
- data/lib/rails_dictionary/array_core_ext.rb +10 -0
- data/lib/rails_dictionary/railtie.rb +11 -0
- data/lib/rails_dictionary/version.rb +1 -1
- data/lib/rails_dictionary.rb +64 -20
- data/lib/tasks/dicts.rake +9 -11
- data/pkg/rails_dictionary-0.2.2.gem +0 -0
- data/pkg/rails_dictionary-0.2.3.gem +0 -0
- data/rails_dictionary.gemspec +4 -4
- data/spec/fake_app.rb +29 -0
- data/spec/init_models.rb +12 -0
- data/spec/load_order/empty_boot_recovers.rb +19 -0
- data/spec/load_order/lazy_slave_model_boot.rb +25 -0
- data/spec/load_order/preseeded_boot.rb +22 -0
- data/spec/load_order/support.rb +41 -0
- data/spec/rails_dictionary_spec.rb +172 -0
- data/spec/spec_helper.rb +22 -0
- data/v1.0-roadmap.md +31 -0
- metadata +37 -38
- data/Readme.md +0 -10
- data/lib/rails_dictionary/models/active_record_extension.rb +0 -38
- data/lib/rails_dictionary/models/acts_as_dict_consumer.rb +0 -77
- data/lib/rails_dictionary/models/acts_as_dictionary.rb +0 -27
- data/test/acts_as_consumer_test.rb +0 -44
- data/test/fake_app.rb +0 -40
- data/test/lookup_test.rb +0 -39
- data/test/rails_dictionary_test.rb +0 -81
- data/test/test_helper.rb +0 -36
data/docs/load-order.md
ADDED
|
@@ -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
|
data/lib/rails_dictionary.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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 :
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
@dy_shanghai=Dictionary.create! name_en: "shanghai",
|
|
18
|
-
@dy_beijing=Dictionary.create! name_en: "beijing",
|
|
19
|
-
@stu_beijing=Student.create! email: "beijing@dict.com",
|
|
20
|
-
@stu_shanghai=Student.create! email: "shanghai@dict.com",
|
|
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
|
data/rails_dictionary.gemspec
CHANGED
|
@@ -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', '
|
|
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
|
data/spec/init_models.rb
ADDED
|
@@ -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
|