miscellany 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +3 -0
- data/config/initializers/01_custom_preloaders.rb +2 -0
- data/config/initializers/arbitrary_prefetch.rb +1 -0
- data/config/initializers/cancancan.rb +34 -0
- data/lib/miscellany.rb +6 -0
- data/lib/miscellany/active_record/arbitrary_prefetch.rb +163 -0
- data/lib/miscellany/active_record/batch_matcher.rb +199 -0
- data/lib/miscellany/active_record/batched_destruction.rb +96 -0
- data/lib/miscellany/active_record/custom_preloaders.rb +59 -0
- data/lib/miscellany/batch_processor.rb +41 -0
- data/lib/miscellany/batching_csv_processor.rb +86 -0
- data/lib/miscellany/controller/http_error_handling.rb +62 -0
- data/lib/miscellany/controller/json_uploads.rb +41 -0
- data/lib/miscellany/controller/sliced_response.rb +271 -0
- data/lib/miscellany/param_validator.rb +442 -0
- data/lib/miscellany/version.rb +3 -0
- data/miscellany.gemspec +43 -0
- metadata +297 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 296f82b755fbbff37b7b939e31d30d53c1e1ed3b528df93992460a5c5093e1bb
|
4
|
+
data.tar.gz: f42d95c04a728a4b16e9b0286e900d243b18b1385649150fed2b1a0501e7098a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 513b776e716e713413ee7f1c45cb2300c6142ffad6fcd3b550bfa06f5f496b630dea82fb99274853e2f825d47387648d015364d619de3b7572e0565b83160219
|
7
|
+
data.tar.gz: 85540e4cca127328d52c1b68a51be2b2366e5bcf5dfef95464703ece61539ba68ed3e0689fd55e22d8c258bbb625efa486b2d5f0b32075bffcf43da866d4e110
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Miscellany::ArbitraryPrefetch.install
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# This patch fixes an issue where CanCanCan wasn't properly handling
|
2
|
+
# existence checks for AR Scopes - instead of do a DB Query, it loaded the entire relation
|
3
|
+
module CanCan
|
4
|
+
module ConditionsMatcher
|
5
|
+
def condition_match?(attribute, value)
|
6
|
+
case value
|
7
|
+
when Hash
|
8
|
+
hash_condition_match?(attribute, value)
|
9
|
+
when Range
|
10
|
+
value.cover?(attribute)
|
11
|
+
when ActiveRecord::Relation
|
12
|
+
ar_condition_match?(attribute, value)
|
13
|
+
when Enumerable
|
14
|
+
value.include?(attribute)
|
15
|
+
else
|
16
|
+
attribute == value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def ar_condition_match?(attribute, value)
|
21
|
+
return false if attribute.nil?
|
22
|
+
|
23
|
+
if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation) && attribute.loaded?)
|
24
|
+
value.where(id: attribute.pluck(:id)).exists?
|
25
|
+
elsif (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
|
26
|
+
value.where(id: attribute.select(:id)).exists?
|
27
|
+
elsif (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Base))
|
28
|
+
value.where(id: attribute.id).exists?
|
29
|
+
else
|
30
|
+
value.where(id: attribute).exists?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/miscellany.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
# Add support for creating arbitrary associations when using ActiveRecord
|
2
|
+
# Adds a `prefetch` method to ActiveRecord Queries.
|
3
|
+
# This method accepts a Hash. The keys of the Hash represent how the Association will be made available.
|
4
|
+
# The values of the Hash may be an array of [Symbol, Relation] or another (filtered) Relation.
|
5
|
+
# Objects are queried from an existing Association on the model. This Association is detemrined
|
6
|
+
# by either the Symbol when an array is passed, or by finding an Assoication for the passed Relation's model
|
7
|
+
#
|
8
|
+
# NOTICE: This implementation is NOT COMPLETE by itself - it depends on Goldiloader
|
9
|
+
# to detect the use of the virtual associations and prevent N+1s. We were already using
|
10
|
+
# Goldiloader, so this made sense. If this module is ever needed stand-alone,
|
11
|
+
# the following options have been identified:
|
12
|
+
# 1. Extend ActiveRecordRelationPatch#exec_queries to execute an ActiveRecord::Associations::Preloader
|
13
|
+
# that will load the related objects
|
14
|
+
# 2. Duplicates the relevant snippets from Goldiloader into this module. See Goldiloader::AutoIncludeContext
|
15
|
+
# The current Goldiloader implementation uses Option 1 internally, but also makes the relations lazy - even
|
16
|
+
# if you define a prefetch, it won't actually be loaded until you attempt to access it on one of the models.
|
17
|
+
module Miscellany
|
18
|
+
module ArbitraryPrefetch
|
19
|
+
class PrefetcherContext
|
20
|
+
attr_accessor :model, :target_attribute
|
21
|
+
attr_reader :options
|
22
|
+
|
23
|
+
def initialize(model, opts)
|
24
|
+
@options = opts
|
25
|
+
@model = model
|
26
|
+
@source_key = opts[:relation]
|
27
|
+
@target_attribute = opts[:attribute]
|
28
|
+
@queryset = opts[:queryset]
|
29
|
+
@models = []
|
30
|
+
end
|
31
|
+
|
32
|
+
def link_models(models)
|
33
|
+
Array(models).each do |m|
|
34
|
+
@models << m
|
35
|
+
|
36
|
+
# assoc = PrefetchAssociation.new(m, self, reflection)
|
37
|
+
assoc = reflection.association_class.new(m, reflection)
|
38
|
+
m.send(:association_instance_set, target_attribute, assoc)
|
39
|
+
|
40
|
+
m.instance_eval <<-CODE, __FILE__, __LINE__ + 1
|
41
|
+
def #{target_attribute}
|
42
|
+
association(:#{target_attribute}).reader
|
43
|
+
end
|
44
|
+
CODE
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def reflection
|
49
|
+
@reflection ||= begin
|
50
|
+
queryset = @queryset
|
51
|
+
source_refl = model.reflections[@source_key.to_s]
|
52
|
+
scope = lambda { |*_args|
|
53
|
+
qs = queryset
|
54
|
+
qs = qs.merge(source_refl.scope_for(model.unscoped)) if source_refl.scope
|
55
|
+
qs
|
56
|
+
}
|
57
|
+
ActiveRecord::Reflection.create(
|
58
|
+
options[:type],
|
59
|
+
@target_attribute,
|
60
|
+
scope,
|
61
|
+
source_refl.options.merge(
|
62
|
+
class_name: source_refl.class_name,
|
63
|
+
inverse_of: nil
|
64
|
+
),
|
65
|
+
model
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module ActiveRecordBasePatch
|
72
|
+
extend ActiveSupport::Concern
|
73
|
+
|
74
|
+
included do
|
75
|
+
class << self
|
76
|
+
delegate :prefetch, to: :all
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module ActiveRecordRelationPatch
|
82
|
+
def exec_queries
|
83
|
+
return super if loaded?
|
84
|
+
|
85
|
+
records = super
|
86
|
+
preloader = nil
|
87
|
+
(@values[:prefetches] || {}).each do |_key, opts|
|
88
|
+
pfc = PrefetcherContext.new(model, opts)
|
89
|
+
pfc.link_models(records)
|
90
|
+
|
91
|
+
unless defined?(Goldiloader)
|
92
|
+
preloader ||= build_preloader
|
93
|
+
preloader.preload(records, opts[:attribute])
|
94
|
+
end
|
95
|
+
end
|
96
|
+
records
|
97
|
+
end
|
98
|
+
|
99
|
+
def prefetch(**kwargs)
|
100
|
+
spawn.add_prefetches!(kwargs)
|
101
|
+
end
|
102
|
+
|
103
|
+
def add_prefetches!(kwargs)
|
104
|
+
return unless kwargs.present?
|
105
|
+
|
106
|
+
assert_mutability!
|
107
|
+
@values[:prefetches] ||= {}
|
108
|
+
kwargs.each do |attr, opts|
|
109
|
+
@values[:prefetches][attr] = normalize_options(attr, opts)
|
110
|
+
end
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
def normalize_options(attr, opts)
|
115
|
+
norm = if opts.is_a?(Array)
|
116
|
+
{ relation: opts[0], queryset: opts[1] }
|
117
|
+
elsif opts.is_a?(ActiveRecord::Relation)
|
118
|
+
rel_name = opts.model.name.underscore
|
119
|
+
rel = (model.reflections[rel_name] || model.reflections[rel_name.pluralize])&.name
|
120
|
+
{ relation: rel, queryset: opts }
|
121
|
+
else
|
122
|
+
opts
|
123
|
+
end
|
124
|
+
|
125
|
+
norm[:attribute] = attr
|
126
|
+
norm[:type] ||= (attr.to_s.pluralize == attr.to_s) ? :has_many : :has_one
|
127
|
+
|
128
|
+
norm
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
module ActiveRecordMergerPatch
|
133
|
+
def merge
|
134
|
+
super.tap do
|
135
|
+
merge_prefetches
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def merge_prefetches
|
142
|
+
relation.add_prefetches!(other.values[:prefetches])
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.install
|
147
|
+
::ActiveRecord::Base.include(ActiveRecordBasePatch)
|
148
|
+
::ActiveRecord::Relation.prepend(ActiveRecordRelationPatch)
|
149
|
+
::ActiveRecord::Relation::Merger.prepend(ActiveRecordMergerPatch)
|
150
|
+
|
151
|
+
return unless defined? ::Goldiloader
|
152
|
+
|
153
|
+
::Goldiloader::AssociationLoader.module_eval do
|
154
|
+
def self.has_association?(model, association_name) # rubocop:disable Naming/PredicateName
|
155
|
+
model.association(association_name)
|
156
|
+
true
|
157
|
+
rescue ::ActiveRecord::AssociationNotFoundError => _err
|
158
|
+
false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module Miscellany
|
2
|
+
# Matches a set of rows (eg from a CSV) against Rows in the Database.
|
3
|
+
#
|
4
|
+
# Examples:
|
5
|
+
# rule_matcher = BatchMatcher.new(
|
6
|
+
# Rule, rows,
|
7
|
+
# validate_all: false,
|
8
|
+
# columns: [[:rule_id, :id, 'ID'], [:rule_import_id, :import_id, 'Import ID']]
|
9
|
+
# )
|
10
|
+
# context_matcher = BatchMatcher.new(
|
11
|
+
# [Account, Course], rows,
|
12
|
+
# polymorphic_on: :rule_context,
|
13
|
+
# columns: [[:canvas_context_id, :canvas_id, 'Canvas ID'], [:sis_context_id, :sis_id, 'SIS ID']]
|
14
|
+
# )
|
15
|
+
# role_matcher = BatchMatcher.new(
|
16
|
+
# Role, rows,
|
17
|
+
# columns: [[:canvas_role_id, :canvas_id, 'Canvas Role ID'], [:role_label, :label, 'Role Label']]
|
18
|
+
# )
|
19
|
+
#
|
20
|
+
# Params:
|
21
|
+
# @param clazz - Model or ActiveRecord::Realtion
|
22
|
+
# @param rows - Data to search the DB for matches
|
23
|
+
# @param columns - [[CSV Header Key, Model Key, (Human Name)], ...]
|
24
|
+
class BatchMatcher
|
25
|
+
attr_reader :rows, :maps, :columns, :primary_column
|
26
|
+
|
27
|
+
def initialize(clazz, rows, columns:, polymorphic_on: false, validate_all: true)
|
28
|
+
# columns: [csv_key, db_key, human_name]
|
29
|
+
@options = {
|
30
|
+
mode: :eager,
|
31
|
+
polymorphic_on: polymorphic_on,
|
32
|
+
validate_all: validate_all
|
33
|
+
}
|
34
|
+
@clazz = clazz
|
35
|
+
@columns = columns
|
36
|
+
@primary_column = columns[0]
|
37
|
+
@rows = rows
|
38
|
+
@maps = {}
|
39
|
+
@loaded_columns = {}
|
40
|
+
@mode = :eager
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_for_row!(row)
|
44
|
+
resolve_row_value(row, :get_by_column)
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_for_row(row)
|
48
|
+
get_for_row!(row)
|
49
|
+
rescue ActiveRecord::RecordNotFound
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_primary_for_row!(row)
|
54
|
+
resolve_row_value(row, :column_to_primary_key)
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_primary_for_row(row)
|
58
|
+
get_primary_for_row!(row)
|
59
|
+
rescue ActiveRecord::RecordNotFound
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns True if the row has a value for any of this Matcher's Columns
|
64
|
+
def should_match?(row)
|
65
|
+
@columns.any? { |col| row[col[0]].present? }
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
def get_by_column(column, row)
|
71
|
+
if column == @primary_column
|
72
|
+
column_value(column, row)
|
73
|
+
else
|
74
|
+
mapped_id = column_value(column, row)
|
75
|
+
get_column_map(primary_column, row, via_column: column)[transform_key(mapped_id)]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def column_value(column, row)
|
80
|
+
get_column_map(column, row)[transform_key(row[column[0]])]
|
81
|
+
end
|
82
|
+
|
83
|
+
def column_to_primary_key(column, row)
|
84
|
+
if column == @primary_column
|
85
|
+
transform_key(row[column[0]])
|
86
|
+
else
|
87
|
+
transform_key(column_value(column, row))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
92
|
+
def resolve_row_value(row, resolver)
|
93
|
+
found_values = []
|
94
|
+
@columns.each do |c|
|
95
|
+
next unless row[c[0]].present?
|
96
|
+
|
97
|
+
inst = send(resolver, c, row)
|
98
|
+
if inst.nil? && (!found_values.present? || @options[:validate_all])
|
99
|
+
clazz = as_class(get_base_query(row))
|
100
|
+
raise ActiveRecord::RecordNotFound, "could not find #{clazz.name} with #{c[2] || c[0]} #{row[c[0]]}"
|
101
|
+
end
|
102
|
+
found_values << inst
|
103
|
+
end
|
104
|
+
|
105
|
+
if @options[:validate_all] && found_values.uniq.count > 1
|
106
|
+
raise ActiveRecord::RecordNotFound, "multiple of [#{@columns.pluck(2).join(', ')}] were supplied, but resolved to different objects" # rubocop:disable Metrics/LineLength
|
107
|
+
end
|
108
|
+
|
109
|
+
found_values[0]
|
110
|
+
end
|
111
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
112
|
+
|
113
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
114
|
+
def get_column_map(column, row, via_column: nil)
|
115
|
+
base_query = get_base_query(row)
|
116
|
+
clazz = as_class(base_query)
|
117
|
+
raise ActiveRecord::RecordNotFound, "invalid #{@options[:polymorphic_on]}: #{row[@options[:polymorphic_on]]}" if clazz.nil? # rubocop:disable Metrics/LineLength
|
118
|
+
|
119
|
+
load_column(clazz, column, via_column) do
|
120
|
+
relevant_rows = rows
|
121
|
+
relevant_rows = relevant_rows.select { |r| r[@options[:polymorphic_on]] == clazz.name } if @options[:polymorphic_on]
|
122
|
+
row_keys = Set.new(relevant_rows.pluck(column[0]).compact)
|
123
|
+
|
124
|
+
# In :eager mode, requesting the primary column triggers loading of all columns
|
125
|
+
row_keys |= eager_load_columns(base_query, relevant_rows) if @options[:mode] == :eager && column == @primary_column
|
126
|
+
|
127
|
+
# Load data for the requested column
|
128
|
+
loaded_hash = load_column_data(column, base_query, row_keys)
|
129
|
+
|
130
|
+
# In :lazy mode, the corresponding primary_column data is loaded with each column
|
131
|
+
load_column_data(primary_column, base_query, loaded_hash.values) if @options[:mode] == :lazy && column != @primary_column # rubocop:disable Metrics/LineLength
|
132
|
+
end
|
133
|
+
end
|
134
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
135
|
+
|
136
|
+
def load_column_data(column, base_query, keys)
|
137
|
+
data = if column == @primary_column
|
138
|
+
base_query.where(column[1] => keys).index_by do |row|
|
139
|
+
transform_key(row[column[1]])
|
140
|
+
end
|
141
|
+
else
|
142
|
+
base_query.where(column[1] => keys).pluck(column[1], @primary_column[1]).map do |k, v|
|
143
|
+
[transform_key(k), v]
|
144
|
+
end.to_h
|
145
|
+
end
|
146
|
+
add_to_column(column, as_class(base_query), data)
|
147
|
+
data
|
148
|
+
end
|
149
|
+
|
150
|
+
def add_to_column(column, clazz, entries)
|
151
|
+
@maps[column] ||= {}
|
152
|
+
cache = @maps[column][clazz] ||= {}
|
153
|
+
cache.merge! entries
|
154
|
+
cache
|
155
|
+
end
|
156
|
+
|
157
|
+
def as_class(clazz)
|
158
|
+
clazz.is_a?(ActiveRecord::Relation) ? clazz.model : clazz
|
159
|
+
end
|
160
|
+
|
161
|
+
def get_base_query(row)
|
162
|
+
return row if row.is_a?(Class) || row.is_a?(ActiveRecord::Relation)
|
163
|
+
return @clazz unless @options[:polymorphic_on]
|
164
|
+
|
165
|
+
@clazz.find { |cls| cls.name == row[@options[:polymorphic_on]] }
|
166
|
+
end
|
167
|
+
|
168
|
+
def transform_key(key)
|
169
|
+
key.nil? ? key : key.to_s
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def load_column(clazz, column, request_column)
|
175
|
+
lkey = loaded_key(clazz, column, request_column)
|
176
|
+
unless @loaded_columns[lkey]
|
177
|
+
yield
|
178
|
+
@loaded_columns[lkey] = true
|
179
|
+
end
|
180
|
+
@maps[column][clazz]
|
181
|
+
end
|
182
|
+
|
183
|
+
def loaded_key(clazz, column, request_column)
|
184
|
+
key_column = @options[:mode] == :eager ? column : request_column || column
|
185
|
+
[*key_column, clazz]
|
186
|
+
end
|
187
|
+
|
188
|
+
def eager_load_columns(base_query, relevant_rows)
|
189
|
+
additional_keys = Set.new
|
190
|
+
@columns.each do |column|
|
191
|
+
next if column == primary_column
|
192
|
+
|
193
|
+
loaded_col = load_column_data(column, base_query, relevant_rows.pluck(column[0]).compact.uniq)
|
194
|
+
additional_keys |= loaded_col.values
|
195
|
+
end
|
196
|
+
additional_keys
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Miscellany
|
2
|
+
module BatchedDestruction
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
define_model_callbacks :bulk_destroy
|
7
|
+
define_model_callbacks :destroy_batch
|
8
|
+
|
9
|
+
before_destroy_batch do
|
10
|
+
# TODO Delete Dependant Relations
|
11
|
+
model_class.reflections.each do |name, reflection|
|
12
|
+
options = reflection.options
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class_methods do
|
18
|
+
def bulk_destroy(**kwargs)
|
19
|
+
return to_sql
|
20
|
+
bulk_destroy_internal(self, **kwargs)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Hook for performing the actual deletion of items, may be used to facilitate soft-deletion.
|
24
|
+
# Must not call destroy().
|
25
|
+
# Default implementation is to delete the batch using delete_all(id: batch_ids).
|
26
|
+
def destroy_bulk_batch(batch, options)
|
27
|
+
delete_ids = batch.map(&:id)
|
28
|
+
where(id: delete_ids).delete_all()
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def bulk_destroy_internal(items, **kwargs)
|
34
|
+
options = {}
|
35
|
+
options.merge!(kwargs)
|
36
|
+
ClassCallbackExector.run_callbacks(model_class, :bulk_destroy, options: options) do
|
37
|
+
if items.respond_to?(:find_in_batches)
|
38
|
+
items.find_in_batches do |batch|
|
39
|
+
_destroy_batch(batch, options)
|
40
|
+
end
|
41
|
+
else
|
42
|
+
_destroy_batch(items, options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def _destroy_batch(batch, options)
|
48
|
+
ClassCallbackExector.run_callbacks(model_class, :destroy_batch, {
|
49
|
+
model_class: model_class,
|
50
|
+
batch: batch,
|
51
|
+
}) do
|
52
|
+
model_class.destroy_bulk_batch(batch, options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def model_class
|
59
|
+
try(:model) || self
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def destroy(*args, legacy: false, **kwargs)
|
64
|
+
if legacy
|
65
|
+
super(*args)
|
66
|
+
else
|
67
|
+
self.class.send(:bulk_destroy_internal, [self], **kwargs)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# These classes are some Hackery to allow us to use callbacks against the Model classes instead of Model instances
|
74
|
+
class ClassCallbackExector
|
75
|
+
include ActiveSupport::Callbacks
|
76
|
+
|
77
|
+
attr_reader :callback_class
|
78
|
+
delegate :__callbacks, to: :callback_class
|
79
|
+
delegate_missing_to :callback_class
|
80
|
+
|
81
|
+
def initialize(cls, env)
|
82
|
+
@callback_class = cls
|
83
|
+
env.keys.each do |k|
|
84
|
+
define_singleton_method(k) do
|
85
|
+
env[k]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
@options = options
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.run_callbacks(cls, callback, env={}, &blk)
|
92
|
+
new(cls, env).run_callbacks(callback, &blk)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|