miscellany 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|