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,6 @@
1
+ require 'rails-better-filters/rails_better_filters'
2
+ require 'rails-better-filters/weighted_topological_sort'
3
+ require 'rails-better-filters/version'
4
+
5
+ module RailsBetterFilters
6
+ end
@@ -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,3 @@
1
+ module RailsBetterFilters
2
+ VERSION = '0.1.1'
3
+ 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: []