rails-better-filters 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2d6e779e2c0d0c2cc4162d0e6c063b116addfbe8
|
4
|
+
data.tar.gz: 0a5051a9492ca917e064bd6ac626902fb39c6c06
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: eeb98545e2069a23485aee869f2bc923eb03cc90942070fa37eb995b520528c06bf13977aced9d462c6c2f5f8900e85f740e62ba866ba25f8a05b56148db032b
|
7
|
+
data.tar.gz: 1b96ab0ea02fe005b1496f269ec8ee3dbe3ee3f729a411709a44aab2ee53ed0534d8a94a5734cba4ec208a61bf97647bef4b7aaca0f93031ded53b55da6c869a
|
@@ -0,0 +1,247 @@
|
|
1
|
+
module RailsBetterFilters
|
2
|
+
def self.included(base)
|
3
|
+
base.extend(ClassMethods)
|
4
|
+
end
|
5
|
+
|
6
|
+
def dispatch_better_filters(action = nil)
|
7
|
+
if !action
|
8
|
+
if defined? params && params[:action]
|
9
|
+
action = params[:action]
|
10
|
+
else
|
11
|
+
raise ArgumentError, 'no action given'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
self.class.better_filter_chain_each do |name, callback, only|
|
16
|
+
if only.empty? || only.include?(action.to_sym)
|
17
|
+
if callback.is_a?(Symbol) && self.respond_to?(callback, true)
|
18
|
+
return if false == self.send(callback)
|
19
|
+
elsif callback.is_a?(Proc)
|
20
|
+
return if false == self.instance_eval(&callback)
|
21
|
+
elsif callback.respond_to?(:call)
|
22
|
+
return if false == callback.call
|
23
|
+
else
|
24
|
+
raise ArgumentError, "don't know how to call better_filter #{name} (Callback: #{callback.inspect})"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
# Register a new bfilter, or overwrite (!) an
|
32
|
+
# already register one. Set a callback function
|
33
|
+
# (i.e. a symbol naming an instance method of the
|
34
|
+
# resp. controller), callable or block.
|
35
|
+
# If neither callback nor block are given, the name
|
36
|
+
# of the filter is used as the name of the instance
|
37
|
+
# method.
|
38
|
+
def better_filter(name, callback = nil, &block)
|
39
|
+
@bfilters ||= {}
|
40
|
+
|
41
|
+
if callback
|
42
|
+
@bfilters[name.to_sym] = callback
|
43
|
+
elsif block_given?
|
44
|
+
@bfilters[name.to_sym] = block
|
45
|
+
else
|
46
|
+
@bfilters[name.to_sym] = name
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set some options for a bfilter. Option hashes get
|
51
|
+
# merged in the end, conflicting options will be resolved
|
52
|
+
# based on the value of the :importance entry (the higher
|
53
|
+
# the better).
|
54
|
+
def better_filter_opts(name, opts = {})
|
55
|
+
@bfilter_opts ||= {}
|
56
|
+
@bfilter_opts[name.to_sym] ||= []
|
57
|
+
@bfilter_opts[name.to_sym] << sanitize_opts(opts)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get the chain of bfilters for this class. Uses caching for
|
61
|
+
# performance of subsequent calls.
|
62
|
+
# Returns a list of hashes of :name => [:only_actions], e.g.
|
63
|
+
# [{:bfilter1 => [:action1, :action2], :bfilter2 => [], ...}]
|
64
|
+
def better_filter_chain
|
65
|
+
if !@final_bfilter_chain
|
66
|
+
# Make sure everything is in place.
|
67
|
+
@bfilters ||= {}
|
68
|
+
@bfilter_opts ||= {}
|
69
|
+
|
70
|
+
# Get advice from our forefathers.
|
71
|
+
igo = inherited_bfilter_opts
|
72
|
+
ig = inherited_bfilters
|
73
|
+
|
74
|
+
# Do the math.
|
75
|
+
@final_bfilter_opts = finalize_bfilter_opts(ig, igo)
|
76
|
+
@final_bfilter_chain = finalize_bfilters(ig, @final_bfilter_opts)
|
77
|
+
end
|
78
|
+
|
79
|
+
@final_bfilter_chain
|
80
|
+
end
|
81
|
+
|
82
|
+
def better_filter_chain_each
|
83
|
+
better_filter_chain.each do |bfilter|
|
84
|
+
name = bfilter.keys.first
|
85
|
+
data = bfilter.values.first
|
86
|
+
yield name, data[:callback], data[:only]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Finds all the bfilters referenced in all base classes of the
|
93
|
+
# current class. For conflicting bfilters, the most recent one
|
94
|
+
# (i.e. youngest in inheritance hierarchy) is chosen.
|
95
|
+
def inherited_bfilters
|
96
|
+
ancestors.reverse.inject({}) do |ig, ancestor|
|
97
|
+
if ancestor.instance_variable_defined?(:@bfilters)
|
98
|
+
ig.merge(ancestor.instance_variable_get(:@bfilters))
|
99
|
+
else
|
100
|
+
ig
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Finds all bfilter_opts hashes in ancestor classes and the
|
106
|
+
# current class, storing them in an array.
|
107
|
+
def inherited_bfilter_opts
|
108
|
+
ancestors.inject({}) do |igo, ancestor|
|
109
|
+
if ancestor.instance_variable_defined?(:@bfilter_opts)
|
110
|
+
ancestor.instance_variable_get(:@bfilter_opts).each do |name, opts|
|
111
|
+
igo[name] ||= []
|
112
|
+
igo[name].concat(opts)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
igo
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Consolidates an array of bfilter_opts hashes into
|
120
|
+
# a single hash, ensures bfilter_opts hashes for every
|
121
|
+
# known bfilter and discards hashes for unknown bfilters.
|
122
|
+
def finalize_bfilter_opts(_bfilters, _bfilter_opts)
|
123
|
+
result = {}
|
124
|
+
_bfilters.keys.each do |name|
|
125
|
+
if !_bfilter_opts[name] || _bfilter_opts[name].empty?
|
126
|
+
# Default options if no option hash given.
|
127
|
+
result[name] = sanitize_opts({})
|
128
|
+
elsif _bfilter_opts[name].size == 1
|
129
|
+
# Use the first option hash if only one was given.
|
130
|
+
result[name] = _bfilter_opts[name].first
|
131
|
+
else
|
132
|
+
# Flatten multiple option hashs weighted by their :importance.
|
133
|
+
result[name] = flatten_opts_list(_bfilter_opts[name])
|
134
|
+
end
|
135
|
+
# Drop the now useless :importance field.
|
136
|
+
result[name].delete(:importance)
|
137
|
+
end
|
138
|
+
result
|
139
|
+
end
|
140
|
+
|
141
|
+
# Finds a topological weighted ordering for the bfilters
|
142
|
+
# known in this class and given the various :before,
|
143
|
+
# :after, :blocks, and :priority constraints.
|
144
|
+
# Returns a list of hashes of bfilter names pointing to their
|
145
|
+
# callbacks and :only constraints.
|
146
|
+
def finalize_bfilters(_bfilters, _bfilter_opts)
|
147
|
+
# Step 1: Drop unknown :before, :after, :block constraints
|
148
|
+
# and convert all :after constraints into :before.
|
149
|
+
_bfilter_opts.each do |name, opts|
|
150
|
+
[:before, :after, :blocks].each do |s|
|
151
|
+
opts[s].select! { |g| _bfilters.keys.include?(g) }
|
152
|
+
end
|
153
|
+
|
154
|
+
while (g = opts[:after].shift)
|
155
|
+
if !_bfilter_opts[g][:before].include?(name)
|
156
|
+
_bfilter_opts[g][:before] << name
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Step 1: Make sure they're all unique now.
|
162
|
+
_bfilter_opts.each do |_, opts|
|
163
|
+
[:before, :blocks].each do |s|
|
164
|
+
opts[s].uniq!
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Step 2: Blocking! This is actually pretty tricky functionality.
|
169
|
+
# For transitive :block constraints (e.g., bfilter :a
|
170
|
+
# blocks bfilter :b blocks bfilter :c), we want to block the
|
171
|
+
# bfilters one after the other, i.e. in this case first block
|
172
|
+
# bfilter :b (as required by :a), but then *keep* bfilter :c, as
|
173
|
+
# it is now not blocked by :b anymore. The reasoning behind this
|
174
|
+
# is that :a most likely came last and knows about both :b and :c,
|
175
|
+
# so if :a wanted to block them both it would be sufficient to
|
176
|
+
# specify them both in the :block field. Need to find a real-world
|
177
|
+
# example for this.
|
178
|
+
# Anyway, the algorithm right now goes like this:
|
179
|
+
# Topologically sort the bfilters based on the :blocks DAG and their
|
180
|
+
# priority, execute all the blocks of the very first bfilter (that
|
181
|
+
# specifies blocks), repeat until no :blocks remain.
|
182
|
+
|
183
|
+
# To speed things up a little, we first remove all nodes that do
|
184
|
+
# not have any outgoing or incoming :blocks constraints.
|
185
|
+
candidates = _bfilters.keys.select do |name|
|
186
|
+
!_bfilter_opts[name][:blocks].empty? ||
|
187
|
+
_bfilter_opts.any? { |_, opts| opts[:blocks].include?(name) }
|
188
|
+
end
|
189
|
+
candidates_opts = _bfilter_opts.select { |name, _| candidates.include?(name) }
|
190
|
+
|
191
|
+
victims = []
|
192
|
+
loop do
|
193
|
+
nodes = Hash[candidates.map { |name| [name, candidates_opts[:priority]] }]
|
194
|
+
edges = Hash[candidates_opts.map { |name, opts| [name, opts[:blocks]] }]
|
195
|
+
break if edges.empty?
|
196
|
+
|
197
|
+
wts = WeightedTopologicalSort.sort(nodes, edges)
|
198
|
+
executor = wts.shift
|
199
|
+
|
200
|
+
# Do the blocks!
|
201
|
+
victims.concat(candidates_opts[executor][:blocks])
|
202
|
+
|
203
|
+
# Filter the candidates.
|
204
|
+
candidates.delete(executor)
|
205
|
+
candidates.reject! { |name| victims.include?(name) }
|
206
|
+
candidates_opts.delete(executor)
|
207
|
+
candidates_opts.reject! { |name, _| victims.include?(name) }
|
208
|
+
end
|
209
|
+
|
210
|
+
# Now the final shoot-the-bfilter-in-the-head.
|
211
|
+
_bfilters.reject! { |name, _| victims.include?(name) }
|
212
|
+
_bfilter_opts.reject! { |name, _| victims.include?(name) }
|
213
|
+
|
214
|
+
# Step 3: Sorting! Compared to blocking, this is rather easy.
|
215
|
+
# Topological sort based on the :before values and the :priority.
|
216
|
+
nodes = Hash[_bfilter_opts.map { |name, opts| [name, opts[:priority]] }]
|
217
|
+
edges = Hash[_bfilter_opts.map { |name, opts| [name, opts[:before]] }]
|
218
|
+
edges.select! { |_, to| !to.empty? }
|
219
|
+
wts = WeightedTopologicalSort.sort(nodes, edges)
|
220
|
+
|
221
|
+
# Step 4: Data shuffle.
|
222
|
+
wts.map { |name| { name => {:callback => _bfilters[name], :only => _bfilter_opts[name][:only]} } }
|
223
|
+
end
|
224
|
+
|
225
|
+
# Default values for option hashes.
|
226
|
+
def sanitize_opts(opts)
|
227
|
+
opts = opts.dup
|
228
|
+
opts[:importance] ||= 0
|
229
|
+
opts[:priority] ||= 0
|
230
|
+
[:before, :after, :blocks, :only].each do |k|
|
231
|
+
if opts[k]
|
232
|
+
opts[k] = [opts[k]] if !opts[k].is_a?(Array)
|
233
|
+
opts[k].map! { |s| s.to_sym }
|
234
|
+
else
|
235
|
+
opts[k] = []
|
236
|
+
end
|
237
|
+
end
|
238
|
+
opts
|
239
|
+
end
|
240
|
+
|
241
|
+
# Merges a list of option hashes ordered by their :importance, so that for conflicting
|
242
|
+
# keys the value of the most important hash is chosen.
|
243
|
+
def flatten_opts_list(opts_list)
|
244
|
+
opts_list.sort_by { |opts| opts[:importance] }.inject({}) { |res, opts| res.merge(opts) }
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module RailsBetterFilters
|
2
|
+
private
|
3
|
+
module WeightedTopologicalSort
|
4
|
+
# Input:
|
5
|
+
# nodes = Hash of node name => priority.
|
6
|
+
# edges = Hash of node name => list of children.
|
7
|
+
# Output:
|
8
|
+
# List of node names.
|
9
|
+
def self.sort(nodes, edges)
|
10
|
+
# We use the algorithm of Kahn (1962) from
|
11
|
+
# http://en.wikipedia.org/w/index.php?title=Topological_sorting&oldid=586746444
|
12
|
+
# and enhance it by sorting the set of nodes with no
|
13
|
+
# incoming edges by their priority.
|
14
|
+
|
15
|
+
# First convert to our own data structures.
|
16
|
+
nodes = make_nodes(nodes)
|
17
|
+
edges = make_edges(edges)
|
18
|
+
|
19
|
+
# The list.
|
20
|
+
l = []
|
21
|
+
|
22
|
+
# Find all nodes with no incoming edges.
|
23
|
+
s = nodes.select { |n| edges.none? { |e| e.to?(n) } }
|
24
|
+
while !s.empty?
|
25
|
+
# Here's the priority trick.
|
26
|
+
s.sort!.reverse!
|
27
|
+
|
28
|
+
# Get the one with the highest priority.
|
29
|
+
current = s.shift
|
30
|
+
l << current
|
31
|
+
|
32
|
+
# Get all outgoing edges from current node and remove them from the graph.
|
33
|
+
outgoing, edges = edges.partition { |e| e.from?(current) }
|
34
|
+
|
35
|
+
# Find current's children.
|
36
|
+
children_names = outgoing.map { |e| e.to }
|
37
|
+
children = nodes.select { |n| children_names.include?(n.name) }
|
38
|
+
|
39
|
+
# If child has no other incoming edges, we can add it to s.
|
40
|
+
children.each do |n|
|
41
|
+
if edges.none? { |e| e.to?(n) }
|
42
|
+
s << n
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# If there are still edges in the graph, there is at least one cycle.
|
48
|
+
raise 'graph is not acyclic!' if !edges.empty?
|
49
|
+
|
50
|
+
# Convert to names again.
|
51
|
+
l.map { |n| n.name }
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def self.make_nodes(nodes)
|
57
|
+
nodes.map { |name, priority| Node.new(name, priority) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.make_edges(edges)
|
61
|
+
edges.map { |from, to_list| to_list.map { |to| [from, to] }}.flatten(1).map { |a| Edge.new(a[0], a[1]) }
|
62
|
+
end
|
63
|
+
|
64
|
+
class Node
|
65
|
+
include Comparable
|
66
|
+
|
67
|
+
attr_reader :name, :priority
|
68
|
+
|
69
|
+
def initialize(n, p)
|
70
|
+
@name = n
|
71
|
+
@priority = p
|
72
|
+
end
|
73
|
+
|
74
|
+
def <=>(other)
|
75
|
+
priority <=> other.priority
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Edge
|
80
|
+
attr_reader :from, :to
|
81
|
+
|
82
|
+
def initialize(from, to)
|
83
|
+
@from = from
|
84
|
+
@to = to
|
85
|
+
end
|
86
|
+
|
87
|
+
def to?(node)
|
88
|
+
@to == node.name
|
89
|
+
end
|
90
|
+
|
91
|
+
def from?(node)
|
92
|
+
@from == node.name
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails-better-filters
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- FlavourSys Technology GmbH
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.14'
|
27
|
+
description: RailsBetterFilters provides more flexible filters for Rails controllers.
|
28
|
+
Filters may specify order constraints (:before, :after), blocking constraints, and
|
29
|
+
priorities.
|
30
|
+
email: technology@flavoursys.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- lib/rails-better-filters.rb
|
36
|
+
- lib/rails-better-filters/rails_better_filters.rb
|
37
|
+
- lib/rails-better-filters/version.rb
|
38
|
+
- lib/rails-better-filters/weighted_topological_sort.rb
|
39
|
+
homepage: http://gitlab.flavoursys.lan/internal/rails-better-filters
|
40
|
+
licenses:
|
41
|
+
- Proprietary
|
42
|
+
metadata: {}
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 2.2.2
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: Better filters for your Rails controllers
|
63
|
+
test_files: []
|