periscope 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.md +132 -0
- data/lib/periscope.rb +41 -4
- data/periscope.gemspec +17 -22
- metadata +56 -117
- data/.gitignore +0 -4
- data/Gemfile +0 -3
- data/README.rdoc +0 -58
- data/Rakefile +0 -2
- data/init.rb +0 -1
- data/lib/periscope/adapters/abstract.rb +0 -67
- data/lib/periscope/adapters/active_record.rb +0 -18
- data/lib/periscope/permission_set.rb +0 -34
- data/lib/periscope/sanitizer.rb +0 -27
- data/lib/periscope/version.rb +0 -3
- data/spec/periscope/adapters/active_record_spec.rb +0 -129
- data/spec/periscope/black_list_spec.rb +0 -19
- data/spec/periscope/version_spec.rb +0 -10
- data/spec/periscope/white_list_spec.rb +0 -19
- data/spec/spec_helper.rb +0 -14
- data/spec/support/abstract_examples.rb +0 -89
- data/spec/support/connections/active_record.rb +0 -17
- data/spec/support/permission_set_examples.rb +0 -35
- data/spec/support/sanitizer_examples.rb +0 -66
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2011 Steve Richert
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# Periscope [![Build Status](https://secure.travis-ci.org/laserlemon/periscope.png)](http://travis-ci.org/laserlemon/periscope) [![Dependency Status](https://gemnasium.com/laserlemon/periscope.png)](https://gemnasium.com/laserlemon/periscope)
|
2
|
+
|
3
|
+
Periscope provides a simple way to chain scopes on your models and to open those scopes up to your users.
|
4
|
+
|
5
|
+
## The Problem
|
6
|
+
|
7
|
+
More often than not, the index action in a RESTful Rails controller is expected to do a lot more than simply return all the records for a given model. We ask it to do all sorts of stuff like filtering, sorting and paginating results. Of course, this is typically done using _scopes_.
|
8
|
+
|
9
|
+
But it can get ugly building long, complicated chains of scopes in the controller, especially when you try to give your users control over the scoping. Picture this:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
def index
|
13
|
+
@articles = Article.scoped
|
14
|
+
@articles = @articles.published_after(params[:published_after]) if params.key?(:published_after)
|
15
|
+
@articles = @articles.published_before(params[:published_before]) if params.key?(:published_before)
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
You can imagine how bad this would get if more than two scopes were involved.
|
20
|
+
|
21
|
+
## The Solution
|
22
|
+
|
23
|
+
With Periscope, you can have this instead:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
def index
|
27
|
+
@articles = Article.periscope(request.query_parameters)
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
The `periscope` method will find keys in your params matching your scope names and chain your scopes for you.
|
32
|
+
|
33
|
+
**Note:** We're using `request.query_parameters` so that we can exclude our controller and action params. `request.query_parameters` will just return the params that appear in the query string.
|
34
|
+
|
35
|
+
## But Wait!
|
36
|
+
|
37
|
+
"What if I don't want to make all my scopes publicly accessible?"
|
38
|
+
|
39
|
+
Within your model you can use the `scope_accessible` method to specify which scopes you want Periscope to honor.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class User < ActiveRecord::Base
|
43
|
+
scope :gender, lambda{|g| where(gender: g) }
|
44
|
+
scope :makes, lambda{|s| where('salary >= ?', s) }
|
45
|
+
|
46
|
+
scope_accessible :gender
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
And in your controller:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class UsersController < ApplicationController
|
54
|
+
def index
|
55
|
+
@users = User.periscope(request.query_parameters)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
Requests to `/users?gender=male` will filter results to only male users. But a request to `/users?makes=1000000` will return all users, silently ignoring the protected scope.
|
61
|
+
|
62
|
+
By default, all scopes are protected.
|
63
|
+
|
64
|
+
## There's More!
|
65
|
+
|
66
|
+
### Custom Parameter Parsing
|
67
|
+
|
68
|
+
Sometimes the values you get from the query parameters aren't quite good enough. They may need to be massaged in order to work with your scopes and class methods. In those cases, you can provide a `:parser` option to your `scope_accessible` method.
|
69
|
+
|
70
|
+
Parsers must respond to the `call` method, receiving the raw query parameter and returning an array of arguments to pass to the scope or class method.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class User < ActiveRecord::Base
|
74
|
+
scope :gender, lambda{|g| where(gender: g) }
|
75
|
+
|
76
|
+
scope_accessible :gender, parser: lambda{|g| [g.downcase] }
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
### On/Off Scopes
|
81
|
+
|
82
|
+
But not all scopes accept arguments. For scopes that you want to toggle on or off, you can set a `:boolean => true` option. Whenever the received parameter is truthy, the scope will be applied. Otherwise, it will be skipped.
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
class User < ActiveRecord::Base
|
86
|
+
scope :male, where(gender: 'male')
|
87
|
+
scope :female, where(gender: 'female')
|
88
|
+
|
89
|
+
scope_accessible :male, :female, boolean: true
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
### Custom Method Names
|
94
|
+
|
95
|
+
Sometimes the query parameters you want to open up to your users may collide with existing method names or reserved Ruby words. In order to avoid collision, you can set a `:method` option to specify what method to use for a query parameter.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
class Project < ActiveRecord::Base
|
99
|
+
scope_accessible :begin, method: :begins_after
|
100
|
+
scope_accessible :end, method: :ends_before
|
101
|
+
|
102
|
+
def self.begins_after(date)
|
103
|
+
where('begins_at >= ?', date)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.ends_before(date)
|
107
|
+
where('ends_at <= ?', date)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
Alternatively, you can set `:prefix` and/or `:suffix` options, which will be applied to the query parameter name to determine the corresponding method name.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class Project < ActiveRecord::Base
|
116
|
+
scope_accessible :begin, :end, suffix: '_date'
|
117
|
+
|
118
|
+
def self.begin_date(date)
|
119
|
+
where('begins_at >= ?', date)
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.end_date(date)
|
123
|
+
where('ends_at <= ?', date)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
## This sucks. How can I make it better?
|
129
|
+
|
130
|
+
1. Fork it.
|
131
|
+
2. Make it better.
|
132
|
+
3. Send me a pull request.
|
data/lib/periscope.rb
CHANGED
@@ -1,6 +1,43 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Periscope
|
2
|
+
def scope_accessible(*scopes)
|
3
|
+
options = scopes.last.is_a?(Hash) ? scopes.pop : {}
|
4
|
+
scopes.each{|s| periscope_options[s.to_s] = options }
|
5
|
+
end
|
3
6
|
|
4
|
-
|
7
|
+
def periscope(params = {})
|
8
|
+
params.inject(periscope_default_scope) do |chain, (scope, param)|
|
9
|
+
periscope_call(chain, scope.to_s, param)
|
10
|
+
end
|
11
|
+
end
|
5
12
|
|
6
|
-
|
13
|
+
private
|
14
|
+
|
15
|
+
def periscope_options
|
16
|
+
@periscope_options ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def periscope_default_scope
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def periscope_call(chain, scope, param)
|
24
|
+
return chain unless options = periscope_options[scope]
|
25
|
+
|
26
|
+
method = periscope_method(scope, options)
|
27
|
+
values = periscope_values(param, options)
|
28
|
+
|
29
|
+
if options[:boolean]
|
30
|
+
values.first ? chain.send(method) : chain
|
31
|
+
else
|
32
|
+
chain.send(method, *values)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def periscope_method(scope, options)
|
37
|
+
options[:method] || [options[:prefix], scope, options[:suffix]].compact.join
|
38
|
+
end
|
39
|
+
|
40
|
+
def periscope_values(param, options)
|
41
|
+
options[:parser] ? options[:parser].call(param) : [param]
|
42
|
+
end
|
43
|
+
end
|
data/periscope.gemspec
CHANGED
@@ -1,27 +1,22 @@
|
|
1
|
-
#
|
2
|
-
$:.push File.expand_path('../lib', __FILE__)
|
3
|
-
require 'periscope/version'
|
1
|
+
# encoding: utf-8
|
4
2
|
|
5
|
-
Gem::Specification.new do |
|
6
|
-
|
7
|
-
|
8
|
-
s.platform = Gem::Platform::RUBY
|
9
|
-
s.authors = ['Steve Richert']
|
10
|
-
s.email = ['steve.richert@gmail.com']
|
11
|
-
s.homepage = 'https://github.com/laserlemon/periscope'
|
12
|
-
s.summary = %(Bring your models' scopes up above the surface.)
|
13
|
-
s.description = %(Periscope acts like attr_accessible or attr_protected, but for your models' scopes.)
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = 'periscope'
|
5
|
+
gem.version = '1.0.0'
|
14
6
|
|
15
|
-
|
7
|
+
gem.authors = ['Steve Richert']
|
8
|
+
gem.email = ['steve.richert@gmail.com']
|
9
|
+
gem.description = %(Push your models' scopes up to the surface)
|
10
|
+
gem.summary = gem.description
|
11
|
+
gem.homepage = 'https://github.com/laserlemon/periscope'
|
16
12
|
|
17
|
-
|
18
|
-
|
19
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f) }
|
20
|
-
s.require_paths = ['lib']
|
13
|
+
gem.add_development_dependency 'rake', '~> 0.9'
|
14
|
+
gem.add_development_dependency 'rspec', '~> 2.0'
|
21
15
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
16
|
+
gem.files = %w(
|
17
|
+
LICENSE
|
18
|
+
lib/periscope.rb
|
19
|
+
periscope.gemspec
|
20
|
+
README.md
|
21
|
+
)
|
27
22
|
end
|
metadata
CHANGED
@@ -1,148 +1,87 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: periscope
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 1
|
8
|
-
- 0
|
9
|
-
version: 0.1.0
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
10
6
|
platform: ruby
|
11
|
-
authors:
|
7
|
+
authors:
|
12
8
|
- Steve Richert
|
13
9
|
autorequire:
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
name: activesupport
|
22
|
-
prerelease: false
|
23
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
-
none: false
|
25
|
-
requirements:
|
26
|
-
- - ">="
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
segments:
|
29
|
-
- 3
|
30
|
-
- 0
|
31
|
-
- 0
|
32
|
-
version: 3.0.0
|
33
|
-
type: :runtime
|
34
|
-
version_requirements: *id001
|
35
|
-
- !ruby/object:Gem::Dependency
|
36
|
-
name: rspec
|
37
|
-
prerelease: false
|
38
|
-
requirement: &id002 !ruby/object:Gem::Requirement
|
12
|
+
date: 2012-06-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
39
17
|
none: false
|
40
|
-
requirements:
|
41
|
-
- -
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
|
44
|
-
- 0
|
45
|
-
version: "0"
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0.9'
|
46
22
|
type: :development
|
47
|
-
version_requirements: *id002
|
48
|
-
- !ruby/object:Gem::Dependency
|
49
|
-
name: sqlite3
|
50
23
|
prerelease: false
|
51
|
-
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.9'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
52
33
|
none: false
|
53
|
-
requirements:
|
54
|
-
- -
|
55
|
-
- !ruby/object:Gem::Version
|
56
|
-
|
57
|
-
- 0
|
58
|
-
version: "0"
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '2.0'
|
59
38
|
type: :development
|
60
|
-
version_requirements: *id003
|
61
|
-
- !ruby/object:Gem::Dependency
|
62
|
-
name: activerecord
|
63
39
|
prerelease: false
|
64
|
-
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
41
|
none: false
|
66
|
-
requirements:
|
67
|
-
- -
|
68
|
-
- !ruby/object:Gem::Version
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
- 0
|
73
|
-
version: 3.0.0
|
74
|
-
type: :development
|
75
|
-
version_requirements: *id004
|
76
|
-
description: Periscope acts like attr_accessible or attr_protected, but for your models' scopes.
|
77
|
-
email:
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.0'
|
46
|
+
description: Push your models' scopes up to the surface
|
47
|
+
email:
|
78
48
|
- steve.richert@gmail.com
|
79
49
|
executables: []
|
80
|
-
|
81
50
|
extensions: []
|
82
|
-
|
83
51
|
extra_rdoc_files: []
|
84
|
-
|
85
|
-
|
86
|
-
- .gitignore
|
87
|
-
- Gemfile
|
88
|
-
- README.rdoc
|
89
|
-
- Rakefile
|
90
|
-
- init.rb
|
52
|
+
files:
|
53
|
+
- LICENSE
|
91
54
|
- lib/periscope.rb
|
92
|
-
- lib/periscope/adapters/abstract.rb
|
93
|
-
- lib/periscope/adapters/active_record.rb
|
94
|
-
- lib/periscope/permission_set.rb
|
95
|
-
- lib/periscope/sanitizer.rb
|
96
|
-
- lib/periscope/version.rb
|
97
55
|
- periscope.gemspec
|
98
|
-
-
|
99
|
-
- spec/periscope/black_list_spec.rb
|
100
|
-
- spec/periscope/version_spec.rb
|
101
|
-
- spec/periscope/white_list_spec.rb
|
102
|
-
- spec/spec_helper.rb
|
103
|
-
- spec/support/abstract_examples.rb
|
104
|
-
- spec/support/connections/active_record.rb
|
105
|
-
- spec/support/permission_set_examples.rb
|
106
|
-
- spec/support/sanitizer_examples.rb
|
107
|
-
has_rdoc: true
|
56
|
+
- README.md
|
108
57
|
homepage: https://github.com/laserlemon/periscope
|
109
58
|
licenses: []
|
110
|
-
|
111
59
|
post_install_message:
|
112
60
|
rdoc_options: []
|
113
|
-
|
114
|
-
require_paths:
|
61
|
+
require_paths:
|
115
62
|
- lib
|
116
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
64
|
none: false
|
118
|
-
requirements:
|
119
|
-
- -
|
120
|
-
- !ruby/object:Gem::Version
|
121
|
-
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
segments:
|
122
70
|
- 0
|
123
|
-
|
124
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
hash: -4385253506114328427
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
73
|
none: false
|
126
|
-
requirements:
|
127
|
-
- -
|
128
|
-
- !ruby/object:Gem::Version
|
129
|
-
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
segments:
|
130
79
|
- 0
|
131
|
-
|
80
|
+
hash: -4385253506114328427
|
132
81
|
requirements: []
|
133
|
-
|
134
|
-
|
135
|
-
rubygems_version: 1.3.7
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.8.24
|
136
84
|
signing_key:
|
137
85
|
specification_version: 3
|
138
|
-
summary:
|
139
|
-
test_files:
|
140
|
-
- spec/periscope/adapters/active_record_spec.rb
|
141
|
-
- spec/periscope/black_list_spec.rb
|
142
|
-
- spec/periscope/version_spec.rb
|
143
|
-
- spec/periscope/white_list_spec.rb
|
144
|
-
- spec/spec_helper.rb
|
145
|
-
- spec/support/abstract_examples.rb
|
146
|
-
- spec/support/connections/active_record.rb
|
147
|
-
- spec/support/permission_set_examples.rb
|
148
|
-
- spec/support/sanitizer_examples.rb
|
86
|
+
summary: Push your models' scopes up to the surface
|
87
|
+
test_files: []
|
data/.gitignore
DELETED
data/Gemfile
DELETED
data/README.rdoc
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
= Periscope
|
2
|
-
|
3
|
-
Bring your models' scopes up above the surface.
|
4
|
-
|
5
|
-
Periscope acts like +attr_accessible+ or +attr_protected+, but for your models' scopes.
|
6
|
-
|
7
|
-
== The Problem
|
8
|
-
|
9
|
-
More often than not, the index action in a RESTful Rails controller is expected to do a lot more than simply return all the records for a given model. We ask it to do all sorts of stuff like filtering, sorting and paginating results. Of course, this is typically done using _scopes_.
|
10
|
-
|
11
|
-
But sometimes it can get ugly building long, complicated chains of scope in the controller, especially when you try to give your users control over the scoping. Picture this:
|
12
|
-
|
13
|
-
def index
|
14
|
-
@articles = Article.scoped
|
15
|
-
@articles = @articles.published_after(params[:published_after]) if params.key?(:published_after)
|
16
|
-
@articles = @articles.published_before(params[:published_before]) if params.key?(:published_before)
|
17
|
-
end
|
18
|
-
|
19
|
-
You can imagine how bad this would get if more than two scopes were involved.
|
20
|
-
|
21
|
-
== The Solution
|
22
|
-
|
23
|
-
With Periscope, you can have this instead:
|
24
|
-
|
25
|
-
def index
|
26
|
-
@articles = Article.periscope(request.query_parameters)
|
27
|
-
end
|
28
|
-
|
29
|
-
The +periscope+ method will find keys in your params matching your scope names and chain your scopes for you.
|
30
|
-
|
31
|
-
<b>Note:</b> We're using <code>request.query_parameters</code> so we can exclude your controller and action params. <code>request.query_parameters</code> will just return the params that show up after the "?" in the URL.
|
32
|
-
|
33
|
-
== But Wait!
|
34
|
-
|
35
|
-
"What if I don't want to make all my scopes publicly accessible?"
|
36
|
-
|
37
|
-
In your model you can use either the +scope_accessible+ or +scope_protected+ method to specify which scopes you want Periscope to pay attention to.
|
38
|
-
|
39
|
-
class User < ActiveRecord::Base
|
40
|
-
attr_accessible :name, :gender, :salary
|
41
|
-
|
42
|
-
scope :gender, lambda{|g| where(:gender => g) }
|
43
|
-
scope :makes_more_than, lambda{|s| where('users.salary >= ?', s) }
|
44
|
-
|
45
|
-
scope_accessible :gender
|
46
|
-
end
|
47
|
-
|
48
|
-
And in your controller:
|
49
|
-
|
50
|
-
class UsersController < ApplicationController
|
51
|
-
def index
|
52
|
-
@users = User.periscope(request.query_parameters)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
Now, requests to <code>/users?gender=male</code> will filter results to only male users. But a request to <code>/users?makes_more_than=1000000</code> will return all users, silently ignoring the protected scope.
|
57
|
-
|
58
|
-
By default, all scopes are protected.
|
data/Rakefile
DELETED
data/init.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
require 'periscope'
|
@@ -1,67 +0,0 @@
|
|
1
|
-
require 'active_support/core_ext/class/attribute.rb'
|
2
|
-
require 'periscope/permission_set'
|
3
|
-
|
4
|
-
module Periscope
|
5
|
-
module Adapters
|
6
|
-
module Abstract
|
7
|
-
extend ActiveSupport::Concern
|
8
|
-
|
9
|
-
included do
|
10
|
-
class_attribute :_accessible_scopes
|
11
|
-
class_attribute :_protected_scopes
|
12
|
-
class_attribute :_periscope_authorizer
|
13
|
-
end
|
14
|
-
|
15
|
-
module ClassMethods
|
16
|
-
def periscope(*)
|
17
|
-
raise NotImplementedError
|
18
|
-
end
|
19
|
-
|
20
|
-
def scope_protected(*scopes)
|
21
|
-
self._protected_scopes = protected_scopes + scopes
|
22
|
-
self._periscope_authorizer = _protected_scopes
|
23
|
-
end
|
24
|
-
|
25
|
-
alias_method :down_periscope, :scope_protected
|
26
|
-
|
27
|
-
def scope_accessible(*scopes)
|
28
|
-
self._accessible_scopes = accessible_scopes + scopes
|
29
|
-
self._periscope_authorizer = _accessible_scopes
|
30
|
-
end
|
31
|
-
|
32
|
-
alias_method :up_periscope, :scope_accessible
|
33
|
-
|
34
|
-
def protected_scopes
|
35
|
-
self._protected_scopes ||= BlackList.new.tap do |list|
|
36
|
-
list.logger = logger if respond_to?(:logger)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def accessible_scopes
|
41
|
-
self._accessible_scopes ||= WhiteList.new(scopes_accessible_by_default).tap do |list|
|
42
|
-
list.logger = logger if respond_to?(:logger)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def periscope_authorizer
|
47
|
-
self._periscope_authorizer ||= accessible_scopes
|
48
|
-
end
|
49
|
-
|
50
|
-
def scopes_accessible_by_default
|
51
|
-
[]
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
module InstanceMethods
|
56
|
-
protected
|
57
|
-
def sanitize_for_search(params)
|
58
|
-
search_authorizer.sanitize(params)
|
59
|
-
end
|
60
|
-
|
61
|
-
def search_authorizer
|
62
|
-
self.class.periscope_authorizer
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
require 'periscope/adapters/abstract'
|
2
|
-
|
3
|
-
module Periscope
|
4
|
-
module Adapters
|
5
|
-
module ActiveRecord
|
6
|
-
extend ActiveSupport::Concern
|
7
|
-
include Abstract
|
8
|
-
|
9
|
-
module ClassMethods
|
10
|
-
def periscope(params = {})
|
11
|
-
periscope_authorizer.sanitize(params).inject(scoped) do |chain, (key, value)|
|
12
|
-
chain.send(key, value)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
require 'set'
|
2
|
-
require 'periscope/sanitizer'
|
3
|
-
|
4
|
-
module Periscope
|
5
|
-
class PermissionSet < Set
|
6
|
-
def initialize(values = nil)
|
7
|
-
super(values, &:to_s)
|
8
|
-
end
|
9
|
-
|
10
|
-
def +(values)
|
11
|
-
super(values.map(&:to_s))
|
12
|
-
end
|
13
|
-
|
14
|
-
def include?(value)
|
15
|
-
super(value.to_s)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
class WhiteList < PermissionSet
|
20
|
-
include Sanitizer
|
21
|
-
|
22
|
-
def deny?(value)
|
23
|
-
!include?(value)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
class BlackList < PermissionSet
|
28
|
-
include Sanitizer
|
29
|
-
|
30
|
-
def deny?(value)
|
31
|
-
include?(value)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
data/lib/periscope/sanitizer.rb
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
module Periscope
|
2
|
-
module Sanitizer
|
3
|
-
extend ActiveSupport::Concern
|
4
|
-
|
5
|
-
included do
|
6
|
-
attr_accessor :logger
|
7
|
-
end
|
8
|
-
|
9
|
-
module InstanceMethods
|
10
|
-
def sanitize(params)
|
11
|
-
params.reject{|k,v| deny?(k) }.tap do |sanitized|
|
12
|
-
debug_protected_scope_removal(params, sanitized)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
protected
|
17
|
-
def debug_protected_scope_removal(params, sanitized)
|
18
|
-
removed = params.keys - sanitized.keys
|
19
|
-
warn!(removed) if removed.any?
|
20
|
-
end
|
21
|
-
|
22
|
-
def warn!(scopes)
|
23
|
-
logger.debug("WARNING: Can't search protected scopes: #{scopes.join(', ')}") if logger
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
data/lib/periscope/version.rb
DELETED
@@ -1,129 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Periscope
|
4
|
-
module Adapters
|
5
|
-
describe ActiveRecord do
|
6
|
-
it_should_behave_like 'an adapter'
|
7
|
-
|
8
|
-
describe :periscope do
|
9
|
-
subject do
|
10
|
-
Class.new(User).tap do |klass|
|
11
|
-
klass.class_eval do
|
12
|
-
scope :gender, lambda{|g| where(:gender => g) }
|
13
|
-
scope :makes, lambda{|s| where('users.salary >= ?', s) }
|
14
|
-
scope :rich, where('users.salary >= 1000000')
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
context 'ignores' do
|
20
|
-
specify 'all scopes by default' do
|
21
|
-
subject.should_receive(:gender).never
|
22
|
-
subject.should_receive(:makes).never
|
23
|
-
|
24
|
-
subject.periscope(:gender => 'male', :makes => 100000)
|
25
|
-
end
|
26
|
-
|
27
|
-
specify 'all scopes when none are accessible' do
|
28
|
-
subject.scope_accessible
|
29
|
-
|
30
|
-
subject.should_receive(:gender).never
|
31
|
-
subject.should_receive(:makes).never
|
32
|
-
|
33
|
-
subject.periscope(:gender => 'male', :makes => 100000)
|
34
|
-
end
|
35
|
-
|
36
|
-
specify 'protected scopes' do
|
37
|
-
subject.scope_protected :makes
|
38
|
-
|
39
|
-
subject.should_receive(:gender).with('male').once
|
40
|
-
subject.should_receive(:makes).never
|
41
|
-
|
42
|
-
subject.periscope(:gender => 'male', :makes => 100000)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
context 'uses' do
|
47
|
-
specify 'all scopes when none are protected' do
|
48
|
-
subject.scope_protected
|
49
|
-
|
50
|
-
subject.should_receive(:gender).with('male').once
|
51
|
-
subject.should_receive(:makes).with(100000).once
|
52
|
-
|
53
|
-
subject.periscope(:gender => 'male', :makes => 100000)
|
54
|
-
end
|
55
|
-
|
56
|
-
specify 'accessible scopes' do
|
57
|
-
subject.scope_accessible :gender
|
58
|
-
|
59
|
-
subject.should_receive(:gender).with('male').once
|
60
|
-
subject.should_receive(:makes).never
|
61
|
-
|
62
|
-
subject.periscope(:gender => 'male', :makes => 100000)
|
63
|
-
end
|
64
|
-
|
65
|
-
specify 'accessible, zero-arity scopes' do
|
66
|
-
subject.scope_accessible :rich
|
67
|
-
|
68
|
-
subject.should_receive(:rich).with(true).once
|
69
|
-
|
70
|
-
lambda{ subject.periscope(:rich => true) }.should_not raise_error
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
context 'returns' do
|
75
|
-
before do
|
76
|
-
subject.delete_all
|
77
|
-
subject.create(:name => 'Henry', :gender => 'male', :salary => 50_000)
|
78
|
-
subject.create(:name => 'Penny', :gender => 'female', :salary => 1_000_000)
|
79
|
-
subject.create(:name => 'Sammy', :gender => 'male', :salary => 100_000)
|
80
|
-
end
|
81
|
-
|
82
|
-
let(:params){ {:gender => 'male', :rich => true} }
|
83
|
-
|
84
|
-
context 'all records' do
|
85
|
-
specify 'for no params' do
|
86
|
-
subject.periscope.map(&:name).should == %w(Henry Penny Sammy)
|
87
|
-
end
|
88
|
-
|
89
|
-
specify 'for empty params' do
|
90
|
-
subject.periscope({}).map(&:name).should == %w(Henry Penny Sammy)
|
91
|
-
end
|
92
|
-
|
93
|
-
specify 'for no accessible scopes' do
|
94
|
-
subject.scope_accessible
|
95
|
-
|
96
|
-
subject.periscope(params).map(&:name).should == %w(Henry Penny Sammy)
|
97
|
-
end
|
98
|
-
|
99
|
-
specify 'for all protected scopes' do
|
100
|
-
subject.scope_protected :gender, :rich
|
101
|
-
|
102
|
-
subject.periscope(params).map(&:name).should == %w(Henry Penny Sammy)
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
context 'scoped results' do
|
107
|
-
specify 'for an accessible scope' do
|
108
|
-
subject.scope_accessible :rich
|
109
|
-
|
110
|
-
subject.periscope(params).map(&:name).should == %w(Penny)
|
111
|
-
end
|
112
|
-
|
113
|
-
specify 'for an unprotected scope' do
|
114
|
-
subject.scope_protected :gender
|
115
|
-
|
116
|
-
subject.periscope(params).map(&:name).should == %w(Penny)
|
117
|
-
end
|
118
|
-
|
119
|
-
specify 'for multiple accessible scopes' do
|
120
|
-
subject.scope_accessible :gender, :rich
|
121
|
-
|
122
|
-
subject.periscope(params).should be_empty
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Periscope
|
4
|
-
describe BlackList do
|
5
|
-
it_should_behave_like 'a permission set'
|
6
|
-
it_should_behave_like 'a sanitizer'
|
7
|
-
|
8
|
-
let(:values){ %w(one two three) }
|
9
|
-
subject{ described_class.new(values) }
|
10
|
-
|
11
|
-
it 'denies an included value' do
|
12
|
-
subject.deny?(values.first).should == true
|
13
|
-
end
|
14
|
-
|
15
|
-
it 'accepts an excluded value' do
|
16
|
-
subject.deny?('four').should == false
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Periscope
|
4
|
-
describe WhiteList do
|
5
|
-
it_should_behave_like 'a permission set'
|
6
|
-
it_should_behave_like 'a sanitizer'
|
7
|
-
|
8
|
-
let(:values){ %w(one two three) }
|
9
|
-
subject{ described_class.new(values) }
|
10
|
-
|
11
|
-
it 'accepts an included value' do
|
12
|
-
subject.deny?(values.first).should == false
|
13
|
-
end
|
14
|
-
|
15
|
-
it 'denies an excluded value' do
|
16
|
-
subject.deny?('four').should == true
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
data/spec/spec_helper.rb
DELETED
@@ -1,89 +0,0 @@
|
|
1
|
-
shared_examples_for 'an adapter' do
|
2
|
-
subject do
|
3
|
-
Class.new.tap do |klass|
|
4
|
-
klass.send(:include, described_class)
|
5
|
-
end
|
6
|
-
end
|
7
|
-
|
8
|
-
it 'adds accessible scopes' do
|
9
|
-
subject.scope_accessible :current, :expired
|
10
|
-
|
11
|
-
subject.accessible_scopes.should include(:current)
|
12
|
-
subject.accessible_scopes.should include(:expired)
|
13
|
-
end
|
14
|
-
|
15
|
-
it 'adds protected scopes' do
|
16
|
-
subject.scope_protected :current, :expired
|
17
|
-
|
18
|
-
subject.protected_scopes.should include(:current)
|
19
|
-
subject.protected_scopes.should include(:expired)
|
20
|
-
end
|
21
|
-
|
22
|
-
it 'stacks accessible scopes' do
|
23
|
-
subject.scope_accessible :current
|
24
|
-
subject.scope_accessible :expired
|
25
|
-
|
26
|
-
subject.accessible_scopes.should include(:current)
|
27
|
-
subject.accessible_scopes.should include(:expired)
|
28
|
-
end
|
29
|
-
|
30
|
-
it 'stacks protected scopes' do
|
31
|
-
subject.scope_protected :current
|
32
|
-
subject.scope_protected :expired
|
33
|
-
|
34
|
-
subject.protected_scopes.should include(:current)
|
35
|
-
subject.protected_scopes.should include(:expired)
|
36
|
-
end
|
37
|
-
|
38
|
-
it 'uses accessible scopes if defined last' do
|
39
|
-
subject.scope_protected :current
|
40
|
-
subject.scope_accessible :expired
|
41
|
-
|
42
|
-
subject.periscope_authorizer.should be_a(Periscope::WhiteList)
|
43
|
-
subject.periscope_authorizer.should == subject.accessible_scopes
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'uses protected scopes if defined last' do
|
47
|
-
subject.scope_accessible :current
|
48
|
-
subject.scope_protected :expired
|
49
|
-
|
50
|
-
subject.periscope_authorizer.should be_a(Periscope::BlackList)
|
51
|
-
subject.periscope_authorizer.should == subject.protected_scopes
|
52
|
-
end
|
53
|
-
|
54
|
-
it 'defaults to using accessible scopes' do
|
55
|
-
subject.periscope_authorizer.should be_a(Periscope::WhiteList)
|
56
|
-
subject.periscope_authorizer.should == subject.accessible_scopes
|
57
|
-
end
|
58
|
-
|
59
|
-
it 'has no accesible scopes by default' do
|
60
|
-
subject.accessible_scopes.should be_empty
|
61
|
-
end
|
62
|
-
|
63
|
-
it 'has no protected scopes by default' do
|
64
|
-
subject.protected_scopes.should be_empty
|
65
|
-
end
|
66
|
-
|
67
|
-
it 'can override the default accessible scopes' do
|
68
|
-
subject.class_eval do
|
69
|
-
def self.scopes_accessible_by_default
|
70
|
-
[:current]
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
subject.accessible_scopes.should include(:current)
|
75
|
-
end
|
76
|
-
|
77
|
-
it 'makes its authorizer available to its instances' do
|
78
|
-
subject.scope_accessible :current
|
79
|
-
|
80
|
-
subject.new.send(:search_authorizer).should == subject.accessible_scopes
|
81
|
-
end
|
82
|
-
|
83
|
-
it 'sanitizes params from its instances' do
|
84
|
-
params = {:current => 'yes', :expired => 'no'}
|
85
|
-
subject.scope_accessible :current
|
86
|
-
|
87
|
-
subject.new.send(:sanitize_for_search, params).should == params.slice(:current)
|
88
|
-
end
|
89
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
require 'active_record'
|
2
|
-
|
3
|
-
ActiveRecord::Base.establish_connection(
|
4
|
-
:adapter => 'sqlite3',
|
5
|
-
:database => ':memory:'
|
6
|
-
)
|
7
|
-
|
8
|
-
ActiveRecord::Schema.define do
|
9
|
-
create_table :users do |t|
|
10
|
-
t.string :name
|
11
|
-
t.string :gender
|
12
|
-
t.integer :salary
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
class User < ActiveRecord::Base
|
17
|
-
end
|
@@ -1,35 +0,0 @@
|
|
1
|
-
shared_examples_for 'a permission set' do
|
2
|
-
it 'has unique values' do
|
3
|
-
values = %w(one two two three)
|
4
|
-
|
5
|
-
permission_set = described_class.new(values)
|
6
|
-
permission_set.to_a.should == values.uniq
|
7
|
-
end
|
8
|
-
|
9
|
-
it 'initializes with no values' do
|
10
|
-
lambda{ described_class.new }.should_not raise_error
|
11
|
-
described_class.new.should be_empty
|
12
|
-
end
|
13
|
-
|
14
|
-
it 'stringifies values when initializing' do
|
15
|
-
values = [:one, :two, :three]
|
16
|
-
|
17
|
-
permission_set = described_class.new(values)
|
18
|
-
permission_set.to_a.should == values.map(&:to_s)
|
19
|
-
end
|
20
|
-
|
21
|
-
it 'stringifies values when adding' do
|
22
|
-
values = [:one, :two, :three]
|
23
|
-
|
24
|
-
permission_set = described_class.new + values
|
25
|
-
permission_set.to_a.should == values.map(&:to_s)
|
26
|
-
end
|
27
|
-
|
28
|
-
it 'stringifies a value when checking for inclusion' do
|
29
|
-
values = %w(one two three)
|
30
|
-
value = values.first.to_sym
|
31
|
-
|
32
|
-
permission_set = described_class.new(values)
|
33
|
-
permission_set.should include(value)
|
34
|
-
end
|
35
|
-
end
|
@@ -1,66 +0,0 @@
|
|
1
|
-
require 'active_support/core_ext/hash/slice'
|
2
|
-
|
3
|
-
shared_examples_for 'a sanitizer' do
|
4
|
-
subject{ described_class.new }
|
5
|
-
|
6
|
-
let(:logger){ FakeLogger.new }
|
7
|
-
let(:params){ {:one => 1, :two => 2} }
|
8
|
-
|
9
|
-
it 'has its own logger' do
|
10
|
-
logger.should_receive(:debug).once
|
11
|
-
|
12
|
-
subject.logger = logger
|
13
|
-
subject.logger.should == logger
|
14
|
-
|
15
|
-
subject.logger.debug
|
16
|
-
end
|
17
|
-
|
18
|
-
it 'removes denied keys from a hash' do
|
19
|
-
subject.stub(:deny?).with(:one).and_return(false)
|
20
|
-
subject.stub(:deny?).with(:two).and_return(true)
|
21
|
-
subject.sanitize(params).should == params.slice(:one)
|
22
|
-
end
|
23
|
-
|
24
|
-
it "doesn't remove keys if none are denied" do
|
25
|
-
subject.stub(:deny? => false)
|
26
|
-
subject.sanitize(params).should == params
|
27
|
-
end
|
28
|
-
|
29
|
-
it 'debugs removed keys' do
|
30
|
-
subject.logger = logger
|
31
|
-
subject.logger.should_receive(:debug).once
|
32
|
-
|
33
|
-
subject.stub(:deny? => true)
|
34
|
-
subject.sanitize(params)
|
35
|
-
end
|
36
|
-
|
37
|
-
it "doesn't debug if there's no logger" do
|
38
|
-
subject.logger.should be_nil
|
39
|
-
|
40
|
-
allow_message_expectations_on_nil
|
41
|
-
subject.logger.should_receive(:debug).never
|
42
|
-
|
43
|
-
subject.stub(:deny? => true)
|
44
|
-
subject.sanitize(params)
|
45
|
-
end
|
46
|
-
|
47
|
-
it "doesn't debug if no keys are removed" do
|
48
|
-
subject.logger = logger
|
49
|
-
subject.logger.should_receive(:debug).never
|
50
|
-
|
51
|
-
subject.stub(:deny? => false)
|
52
|
-
subject.sanitize(params)
|
53
|
-
end
|
54
|
-
|
55
|
-
it 'only debugs the removed keys' do
|
56
|
-
subject.logger = logger
|
57
|
-
|
58
|
-
subject.stub(:deny?).with(:one).and_return(false)
|
59
|
-
subject.stub(:deny?).with(:two).and_return(true)
|
60
|
-
subject.sanitize(params)
|
61
|
-
|
62
|
-
message = subject.logger.output.pop
|
63
|
-
message.should_not match(/\bone\b/)
|
64
|
-
message.should match(/\btwo\b/)
|
65
|
-
end
|
66
|
-
end
|