cancancan-baby_squeel 0.1.0

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: 3b3caedaafaf77f1bcca26171be06f69ab78f32d
4
+ data.tar.gz: 54696f9993172b8801e76c46b910dff1d7d96652
5
+ SHA512:
6
+ metadata.gz: 76c95bd79b2dada16c7b636a3fd31d5ffae04831c13e864a6a26146e291e46f5e0634b704f5a70d46fb7096b61ac8f3444bdf92c6d15add97678923c4aa797d0
7
+ data.tar.gz: 1e14a168786c8b5fe7b6dc29e999126b2d964d961ae217acec2cf7b8901c232148988a77661002b65f48dd8fcb6fae5d52287013d75cc4021d8acc04cf9cd74a
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # Ignore generated code coverage information
12
+ /spec/coverage/*
13
+ /coverage/*
14
+
15
+ # Ignore RSpec state
16
+ /spec/examples.txt
data/.hound.yml ADDED
@@ -0,0 +1,2 @@
1
+ ruby:
2
+ config_file: .rubocop.yml
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
4
+ <exclude-output />
5
+ <content url="file://$MODULE_DIR$">
6
+ <excludeFolder url="file://$MODULE_DIR$/.bundle" />
7
+ </content>
8
+ <orderEntry type="inheritedJdk" />
9
+ <orderEntry type="sourceFolder" forTests="false" />
10
+ </component>
11
+ </module>
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,109 @@
1
+ Style/CollectionMethods:
2
+ Enabled: true
3
+ PreferredMethods:
4
+ reduce:
5
+ inject: 'reduce'
6
+ find:
7
+ detect: 'find'
8
+
9
+ Style/FileName:
10
+ Enabled: true
11
+
12
+ Style/GuardClause:
13
+ Enabled: true
14
+
15
+ Style/IfUnlessModifier:
16
+ Enabled: true
17
+
18
+ Style/PercentLiteralDelimiters:
19
+ Enabled: true
20
+
21
+ Style/SignalException:
22
+ Enabled: true
23
+
24
+ Style/SingleLineBlockParams:
25
+ Enabled: true
26
+
27
+ Style/SingleLineMethods:
28
+ Enabled: true
29
+
30
+ Style/TrailingCommaInArguments:
31
+ Enabled: true
32
+
33
+ Style/TrailingCommaInLiteral:
34
+ Enabled: true
35
+
36
+ Metrics/AbcSize:
37
+ Enabled: true
38
+
39
+ Metrics/ClassLength:
40
+ Enabled: true
41
+
42
+ Metrics/ModuleLength:
43
+ Enabled: true
44
+
45
+ Metrics/CyclomaticComplexity:
46
+ Enabled: true
47
+
48
+ Metrics/MethodLength:
49
+ Enabled: true
50
+
51
+ Metrics/ParameterLists:
52
+ Enabled: true
53
+
54
+ Metrics/PerceivedComplexity:
55
+ Enabled: true
56
+
57
+ Lint/AssignmentInCondition:
58
+ Enabled: true
59
+
60
+ Style/AccessorMethodName:
61
+ Enabled: true
62
+
63
+ Style/Alias:
64
+ Enabled: true
65
+ EnforcedStyle: prefer_alias_method
66
+
67
+ Style/Documentation:
68
+ Enabled: true
69
+
70
+ Style/DoubleNegation:
71
+ Enabled: true
72
+
73
+ Style/EachWithObject:
74
+ Enabled: true
75
+
76
+ Style/EmptyLiteral:
77
+ Enabled: true
78
+
79
+ Style/ModuleFunction:
80
+ Enabled: true
81
+
82
+ Style/OneLineConditional:
83
+ Enabled: true
84
+
85
+ Style/PerlBackrefs:
86
+ Enabled: true
87
+
88
+ Style/SpecialGlobalVars:
89
+ Enabled: true
90
+
91
+ Style/VariableInterpolation:
92
+ Enabled: true
93
+
94
+ Style/WhenThen:
95
+ Enabled: true
96
+
97
+ Lint/EachWithObjectArgument:
98
+ Enabled: true
99
+
100
+ Lint/HandleExceptions:
101
+ Enabled: true
102
+
103
+ Lint/LiteralInCondition:
104
+ Description: Checks of literals used in conditions.
105
+ Enabled: true
106
+
107
+ Lint/LiteralInInterpolation:
108
+ Description: Checks for literals used in interpolation.
109
+ Enabled: true
data/.rubocop.yml ADDED
@@ -0,0 +1,46 @@
1
+ inherit_from:
2
+ - .rubocop.unhound.yml
3
+
4
+ AllCops:
5
+ Include:
6
+ - '**/Gemfile'
7
+ - '**/Rakefile'
8
+ Exclude:
9
+ - 'vendor/bundle/**/*'
10
+ TargetRubyVersion: 2.1
11
+
12
+ Metrics/LineLength:
13
+ Max: 100
14
+
15
+ Style/ClassAndModuleChildren:
16
+ EnforcedStyle: compact
17
+
18
+ Style/DotPosition:
19
+ EnforcedStyle: trailing
20
+
21
+ Style/Documentation:
22
+ Enabled: false
23
+
24
+ Style/IndentHash:
25
+ EnforcedStyle: consistent
26
+
27
+ Style/IfUnlessModifier:
28
+ MaxLineLength: 100
29
+
30
+ Style/ParallelAssignment:
31
+ Enabled: false
32
+
33
+ Style/StringLiterals:
34
+ EnforcedStyle: single_quotes
35
+
36
+ Style/WhileUntilModifier:
37
+ MaxLineLength: 100
38
+
39
+ Style/WordArray:
40
+ Enabled: false
41
+
42
+ Style/RegexpLiteral:
43
+ AllowInnerSlashes: true
44
+
45
+ Style/ClassAndModuleChildren:
46
+ EnforcedStyle: compact
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.2
4
+ - ruby-head
5
+ matrix:
6
+ allow_failures:
7
+ - rvm: ruby-head
8
+ bundler_args: "--jobs=3 --retry=3"
9
+ cache: bundler
10
+
11
+ before_install:
12
+ - gem update bundler
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ## master
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release -
6
+ Modifications to support baby_squeel. [@jeremyyap](https://github.com/jeremyyap)
7
+ Code based on cancancan-squeel, credits to original author. [@lowjoel](https://github.com/lowjoel)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cancancan-squeel.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # CanCanCan-BabySqueel
2
+ [![Build Status](https://travis-ci.org/Coursemology/cancancan-baby_squeel.svg?branch=master)](https://travis-ci.org/Coursemology/cancancan-baby_squeel)
3
+ [![Coverage Status](https://coveralls.io/repos/github/Coursemology/cancancan-baby_squeel/badge.svg?branch=master)](https://coveralls.io/github/Coursemology/cancancan-baby_squeel?branch=master)
4
+
5
+ This is an adapter for the [CanCanCan](https://github.com/CanCanCommunity/cancancan) authorisation
6
+ library to automatically generate SQL queries from ability rules.
7
+
8
+ This differs from the default ActiveRecord implementation in that it uses
9
+ [baby_squeel](https://github.com/rzane/baby_squeel) to generate SQL queries. This no longer
10
+ uses
11
+ - `includes` (which incurs eager loading overhead)
12
+ - `WHERE` fragments, joined lexically using `OR` or `AND` or `NOT`.
13
+
14
+ As a side effect of using `squeel`, this allows self-joins in rule definitions.
15
+
16
+ ## Usage
17
+
18
+ In your `Gemfile`, insert the following line:
19
+
20
+ ```ruby
21
+ gem 'cancancan-baby_squeel'
22
+ ```
23
+
24
+ after you included `cancancan`.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cancancan/baby_squeel/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'cancancan-baby_squeel'
8
+ spec.version = CanCanCan::BabySqueel::VERSION
9
+ spec.authors = ['Jeremy Yap']
10
+ spec.email = ['jeremy.yapjl@gmail.com']
11
+ spec.license = 'MIT'
12
+
13
+ spec.summary = 'BabySqueel database adapter for CanCanCan.'
14
+ spec.description = "Implements CanCanCan's rule-based record fetching using BabySqueel."
15
+ spec.homepage = 'https://github.com/jeremyyap/cancancan-baby_squeel'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").
18
+ reject { |f| f.match(/^(test|spec|features)\//) }
19
+ spec.bindir = 'bin'
20
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.11'
24
+ spec.add_development_dependency 'rake'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
26
+ spec.add_development_dependency 'simplecov'
27
+ spec.add_development_dependency 'coveralls'
28
+
29
+ spec.add_development_dependency 'sqlite3'
30
+
31
+ spec.add_dependency 'activerecord', '>= 4.1'
32
+ spec.add_dependency 'cancancan', '>= 1.10'
33
+ spec.add_dependency 'baby_squeel', '~> 1.1.5'
34
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ require 'active_record'
3
+ require 'cancancan'
4
+ require 'baby_squeel'
5
+
6
+ require 'cancancan/baby_squeel/version'
7
+ require 'cancancan/baby_squeel/active_record_disabler'
8
+ require 'cancancan/baby_squeel/attribute_mapper'
9
+ require 'cancancan/baby_squeel/expression_builder'
10
+ require 'cancancan/baby_squeel/expression_combinator'
11
+ require 'cancancan/baby_squeel/baby_squeel_adapter'
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ class CanCanCan::BabySqueel::ActiveRecordDisabler
3
+ ::CanCan::ModelAdapters::ActiveRecord4Adapter.class_eval do
4
+ def self.for_class?(_)
5
+ false
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+ # Implements mapping attributes, values, and comparators for a given model to appropriate
3
+ # database equivalents.
4
+ #
5
+ # This implements:
6
+ # - comparing values against an array: interpreted as any value for ==, none of the values for !=.
7
+ # - mapping foreign keys to IDs
8
+ module CanCanCan::BabySqueel::AttributeMapper
9
+ module_function
10
+
11
+ # Picks the appropriate column, comparator, and value to use in the Squeel expression.
12
+ #
13
+ # This checks for association references: this will use the appropriate column name.
14
+ # Array values are interpreted as alternative choices allowed or disallowed.
15
+ # Ranges are converted to appropriate comparator pairs.
16
+ #
17
+ # The return value is a tuple:
18
+ #
19
+ # - The first element is a combinator to be used on the comparisons.
20
+ # - The second element is an array of comparisons: each comparison is a tuple of
21
+ # (key, comparator, value)
22
+ #
23
+ # The appropriate expression is the combination of all the comparisons, using the combinator
24
+ # returned.
25
+ #
26
+ # @example Attribute Ranges
27
+ # squeel_comparison_for(User, :id, :eq, 1..5) #=> [:and, [[:id, :gteq, 1], [:id, :lteq, 5]]]
28
+ # @example Association Objects
29
+ # squeel_comparison_for(Post, :comment, :eq, comment) #=> [:and, [[:comment_id, :eq, 1]]]
30
+ #
31
+ # @param [Class] model_class The model class which the key references.
32
+ # @param [Symbol] key The column being compared.
33
+ # @param [Symbol] comparator The comparator to get the appropriate Squeel comparator for.
34
+ # @param value The value to be comparing against.
35
+ # @return [Array<(Symbol, Array<(Symbol, Symbol, Object)>)>] A tuple containing the combinator for
36
+ # the comparisons, and a sequence of comparisons.
37
+ def squeel_comparison_for(model_class, key, comparator, value)
38
+ key, value = map_association(model_class, key, value)
39
+
40
+ combinator, comparisons = squeel_comparator_for(comparator, value)
41
+ [combinator, comparisons.map { |comp| comp.unshift(key) }]
42
+ end
43
+
44
+ # Picks the table column to compare the value against for the given key.
45
+ #
46
+ # This sets associations to use the proper foreign key column.
47
+ #
48
+ # @param [Class] model_class The model class which the key references.
49
+ # @param [Symbol] key The column being compared.
50
+ # @param value The value to be comparing against.
51
+ # @return [Array<(Symbol, Object)>] A tuple containing the column to compare with and the value
52
+ # to compare with.
53
+ def map_association(model_class, key, value)
54
+ if (association = model_class.reflect_on_association(key))
55
+ key = association.foreign_key
56
+ value = value.id
57
+ end
58
+
59
+ [key, value]
60
+ end
61
+
62
+ # Maps the given comparator to a comparator appropriate for the given value.
63
+ #
64
+ # Array values are interpreted as alternative choices allowed or disallowed.
65
+ #
66
+ # Ranges are interpreted as start/end pairs, respecting the exclusion of the end point.
67
+ #
68
+ # @param [Symbol] comparator The comparator to get the appropriate Squeel comparator for.
69
+ # @param value The value to be comparing against.
70
+ # @return [Array<Array<(Symbol, Object)>>] An array of comparisons, each with the comparator
71
+ # to use, and the value to compare against.
72
+ def squeel_comparator_for(comparator, value)
73
+ case value
74
+ when Array then comparator_for_array(comparator, value)
75
+ when Range then comparator_for_range(comparator, value)
76
+ else [:and, [[comparator, value]]]
77
+ end
78
+ end
79
+
80
+ # Maps the given comparator to the IN/NOT IN operator.
81
+ #
82
+ # @param [Symbol] comparator The comparator to get the SqueeL comparator for.
83
+ # @param [Array] value The acceptable/rejected values.
84
+ # @return [Array<(Symbol, Array<(Symbol, Object)>)>] The combinator, and an array of comparisons,
85
+ # each with the comparator to use, and the value to compare against.
86
+ def comparator_for_array(comparator, value)
87
+ case comparator
88
+ when :eq then [:and, [[:in, value]]]
89
+ when :not_eq then [:and, [[:not_in, value]]]
90
+ end
91
+ end
92
+
93
+ # Maps the given comparator to a range comparison.
94
+ #
95
+ # @param [Symbol] comparator The comparator to get the Squeel comparator for.
96
+ # @param [Range] value The acceptable/rejected values.
97
+ # @return [Array<(Symbol, Array<(Symbol, Object)>)>] The combinator, and an array of comparisons,
98
+ # each with the comparator to use, and the value to compare against.
99
+ def comparator_for_range(comparator, value)
100
+ if value.exclude_end?
101
+ comparator_for_exclusive_range(comparator, value)
102
+ else
103
+ comparator_for_inclusive_range(comparator, value)
104
+ end
105
+ end
106
+
107
+ # Maps the given comparator to a range comparison.
108
+ #
109
+ # @param [Symbol] comparator The comparator to get the Squeel comparator for.
110
+ # @param [Range] value The acceptable/rejected values.
111
+ # @return [Array<Array<(Symbol, Object)>>] An array of comparisons, each with the comparator
112
+ # to use, and the value to compare against.
113
+ def comparator_for_exclusive_range(comparator, value)
114
+ case comparator
115
+ when :eq then [:and, [[:gteq, value.first], [:lt, value.last]]]
116
+ when :not_eq then [:or, [[:lt, value.first], [:gteq, value.last]]]
117
+ end
118
+ end
119
+
120
+ # Maps the given comparator to a range comparison.
121
+ #
122
+ # @param [Symbol] comparator The comparator to get the Squeel comparator for.
123
+ # @param [Range] value The acceptable/rejected values.
124
+ # @return [Array<Array<(Symbol, Object)>>] An array of comparisons, each with the comparator
125
+ # to use, and the value to compare against.
126
+ def comparator_for_inclusive_range(comparator, value)
127
+ case comparator
128
+ when :eq then [:and, [[:gteq, value.first], [:lteq, value.last]]]
129
+ when :not_eq then [:or, [[:lt, value.first], [:gt, value.last]]]
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+ class CanCanCan::BabySqueel::BabySqueelAdapter < CanCan::ModelAdapters::AbstractAdapter
3
+ include CanCanCan::BabySqueel::ExpressionCombinator
4
+
5
+ ALWAYS_TRUE = CanCanCan::BabySqueel::ExpressionCombinator::ALWAYS_TRUE
6
+ ALWAYS_FALSE = CanCanCan::BabySqueel::ExpressionCombinator::ALWAYS_FALSE
7
+
8
+ def self.for_class?(model_class)
9
+ model_class <= ActiveRecord::Base
10
+ end
11
+
12
+ def self.override_condition_matching?(subject, name, _)
13
+ match_relation?(subject, name) || match_enum?(subject, name)
14
+ end
15
+
16
+ def self.matches_condition?(subject, name, value)
17
+ if match_relation?(subject, name)
18
+ matches_relation?(subject, name, value)
19
+ elsif match_enum?(subject, name)
20
+ matches_enum?(subject, name, value)
21
+ else
22
+ false
23
+ end
24
+ end
25
+
26
+ # Overrides condition matching for enums.
27
+ def self.match_enum?(subject, name)
28
+ klass = subject.class
29
+ klass.respond_to?(:defined_enums) && klass.defined_enums.include?(name.to_s)
30
+ end
31
+ private_class_method :match_enum?
32
+
33
+ # Overrides condition matching for enums.
34
+ def self.matches_enum?(subject, name, value)
35
+ # Get the mapping from enum strings to values.
36
+ enum = subject.class.public_send(name.to_s.pluralize)
37
+
38
+ # Get the value of the attribute as an integer.
39
+ attribute = enum[subject.public_send(name)]
40
+
41
+ # Check to see if the value matches the condition.
42
+ value.is_a?(Enumerable) ? value.include?(attribute) : attribute == value
43
+ end
44
+ private_class_method :matches_enum?
45
+
46
+ def self.match_relation?(subject, name)
47
+ subject_attribute = subject.public_send(name)
48
+ subject_attribute.is_a?(ActiveRecord::Relation) && !subject_attribute.loaded
49
+ end
50
+ private_class_method :match_relation?
51
+
52
+ def self.matches_relation?(subject, name, value)
53
+ relation = subject.public_send(name)
54
+ klass = subject.class.reflect_on_association(name).klass
55
+ join_list = nil
56
+
57
+ scope = relation.where.has do
58
+ expression, join_list = CanCanCan::BabySqueel::ExpressionBuilder.build(self, klass, :eq, value)
59
+ expression
60
+ end
61
+
62
+ add_joins_to_scope(scope, join_list, :inner).any?
63
+ end
64
+ private_class_method :matches_relation?
65
+
66
+ def database_records
67
+ # TODO: Handle overridden scopes.
68
+ relation.distinct
69
+ end
70
+
71
+ # Builds a relation, outer joined on the provided associations.
72
+ #
73
+ # @param [ActiveRecord::Relation] scope The current scope to add the joins to.
74
+ # @param [Array<Array<Symbol>>] joins The set of associations to outer join with.
75
+ # @param [Symbol] join_type The type of join; defaults to outer joins.
76
+ # @return [ActiveRecord::Relation] The built relation.
77
+ def self.add_joins_to_scope(scope, joins, join_type = :outer)
78
+ joins.reduce(scope) do |result, join|
79
+ result.joining do
80
+ join.reduce(self) do |relation, association|
81
+ relation = relation.__send__(association)
82
+ relation = relation.__send__(join_type) unless join_type == :inner
83
+ relation
84
+ end
85
+ end
86
+ end
87
+ end
88
+ private_class_method :add_joins_to_scope
89
+
90
+ private
91
+
92
+ # Builds a relation that expresses the set of provided rules.
93
+ #
94
+ # The required Squeel expression is built, then the joins which are necessary to satisfy the
95
+ # expressions are added to the query scope.
96
+ def relation
97
+ adapter = self
98
+ join_list = nil
99
+ scope = @model_class.where(nil).where.has do
100
+ expression, join_list = adapter.send(:build_accessible_by_expression, self)
101
+ expression
102
+ end
103
+
104
+ add_joins_to_scope(scope, join_list)
105
+ end
106
+
107
+ # @see CanCanCan::BabySqueel::BabySqueelAdapter.add_joins_to_scope
108
+ def add_joins_to_scope(*args)
109
+ self.class.send(:add_joins_to_scope, *args)
110
+ end
111
+
112
+ # This builds Squeel expression for each rule, and combines the expression with those to the left
113
+ # using a fold-left.
114
+ #
115
+ # The rules provided by Cancancan are in reverse order, i.e. the lowest priority rule is first.
116
+ #
117
+ # @param squeel The Squeel scope.
118
+ # @return [Array<(Squeel::Nodes::Node, Array<Array<Symbol>>)>] A tuple containing the Squeel
119
+ # expression, as well as an array of joins which the Squeel expression must be joined to.
120
+ def build_accessible_by_expression(squeel)
121
+ @rules.reverse.reduce([ALWAYS_FALSE, []]) do |(left_expression, joins), rule|
122
+ combine_expression_with_rule(squeel, left_expression, joins, rule)
123
+ end
124
+ end
125
+
126
+ # Combines the given expression with the new rule.
127
+ #
128
+ # @param squeel The Squeel scope.
129
+ # @param left_expression The Squeel expression for all preceding rules.
130
+ # @param [Array<Array<Symbol>>] joins An array of joins which the Squeel expression must be
131
+ # joined to.
132
+ # @param [CanCan::Rule] rule The rule being added.
133
+ # @return [Array<(Squeel::Nodes::Node, Array<Array<Symbol>>)>] A tuple containing the Squeel
134
+ # expression, as well as an array of joins which the Squeel expression must be joined to.
135
+ def combine_expression_with_rule(squeel, left_expression, joins, rule)
136
+ right_expression, right_expression_joins = build_expression_from_rule(squeel, rule)
137
+ operator = rule.base_behavior ? :or : :and
138
+
139
+ expression, joins = combine_squeel_expressions(left_expression, joins, operator, right_expression,
140
+ right_expression_joins)
141
+
142
+ squeel._scope = add_joins_to_scope(squeel._scope, joins)
143
+ [expression, joins]
144
+ end
145
+
146
+ # Builds a Squeel expression representing the rule's conditions.
147
+ #
148
+ # @param squeel The Squeel scope.
149
+ # @param [CanCan::Rule] rule The rule being built.
150
+ # @return [Array<(Squeel::Nodes::Node, Array<Array<Symbol>>)>] A tuple containing the Squeel
151
+ # expression representing the rule's conditions, as well as an array of joins which the Squeel
152
+ # expression must be joined to.
153
+ def build_expression_from_rule(squeel, rule)
154
+ if rule.conditions.empty?
155
+ [rule.base_behavior ? ALWAYS_TRUE : ALWAYS_FALSE, []]
156
+ else
157
+ comparator = rule.base_behavior ? :eq : :not_eq
158
+ CanCanCan::BabySqueel::ExpressionBuilder.build(squeel, @model_class, comparator, rule.conditions)
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ # Builds Squeel expressions from the given scope, model class, and a hash of conditions.
3
+ #
4
+ # This is used by building a set of rules for retrieving all accessible records, as well as for
5
+ # building queries instead of loading all records into memory.
6
+ module CanCanCan::BabySqueel::ExpressionBuilder
7
+ module_function
8
+
9
+ # Builds a new Squeel expression node given a model class, the comparator, and the conditions.
10
+ #
11
+ # @param squeel The Squeel context. This is the value of +self+ within a +where+ DSL block.
12
+ # @param [Class] model_class The model class which the conditions reference.
13
+ # @param [Symbol] comparator The comparator to use when generating the comparison.
14
+ # @param [Hash] conditions The values to compare the given node's attributes against.
15
+ # @return [Array<(Squeel::Nodes::Node, Array<Array<Symbol>>)>] A tuple containing the Squeel
16
+ # expression representing the rule's conditions, as well as an array of joins which the Squeel
17
+ # expression must be joined to.
18
+ def build(squeel, model_class, comparator, conditions)
19
+ build_expression_node(squeel, model_class, comparator, conditions, true)
20
+ end
21
+
22
+ # Builds a new Squeel expression node.
23
+ #
24
+ # @param node The parent node context.
25
+ # @param [Class] model_class The model class which the conditions reference.
26
+ # @param [Symbol] comparator The comparator to use when generating the comparison.
27
+ # @param [Hash] conditions The values to compare the given node's attributes against.
28
+ # @param [Boolean] root True if the node being built is from the root. The root node is special
29
+ # because it does not mutate itself; all other nodes do.
30
+ # @return [Array<(Squeel::Nodes::Node, Array<Array<Symbol>>)>] A tuple containing the Squeel
31
+ # expression representing the rule's conditions, as well as an array of joins which the Squeel
32
+ # expression must be joined to.
33
+ def build_expression_node(node, model_class, comparator, conditions, root = false)
34
+ conditions.reduce([nil, []]) do |(left_expression, joins), (key, value)|
35
+ comparison_node, node_joins = build_comparison_node(root ? node : node.dup, model_class,
36
+ key, comparator, value)
37
+ if left_expression
38
+ [left_expression.and(comparison_node), joins.concat(node_joins)]
39
+ else
40
+ [comparison_node, node_joins]
41
+ end
42
+ end
43
+ end
44
+
45
+ # Builds a comparison node for the given key and value.
46
+ #
47
+ # @param node The node context to build the comparison.
48
+ # @param [Class] model_class The model class which the conditions reference.
49
+ # @param [Symbol] key The column to compare against.
50
+ # @param [Symbol] comparator The comparator to compare the column against the value.
51
+ # @param value The value to compare the column against.
52
+ def build_comparison_node(node, model_class, key, comparator, value)
53
+ if value.is_a?(Hash)
54
+ build_association_comparison_node(node, model_class, key, comparator, value)
55
+ else
56
+ build_scalar_comparison_node(node, model_class, key, comparator, value)
57
+ end
58
+ end
59
+
60
+ # Builds a comparison node for the given association and association attributes.
61
+ #
62
+ # @param node The node context to build the comparison.
63
+ # @param [Class] model_class The model class which the conditions reference.
64
+ # @param [Symbol] key The association to compare against.
65
+ # @param [Symbol] comparator The comparator to compare the column against the value.
66
+ # @param [Hash] value The attributes to compare the column against.
67
+ def build_association_comparison_node(node, model_class, key, comparator, value)
68
+ reflection_class = model_class.reflect_on_association(key).klass
69
+ expression, joins = build_expression_node(node.__send__(key), reflection_class, comparator,
70
+ value)
71
+ [expression, joins.map { |join| join.unshift(key) }.unshift([key])]
72
+ end
73
+
74
+ # Builds a comparison node for the given attribute and value.
75
+ #
76
+ # @param node The node context to build the comparison.
77
+ # @param [Class] model_class The model class which the conditions reference.
78
+ # @param [Symbol] key The column to compare against.
79
+ # @param [Symbol] comparator The comparator to compare the column against the value.
80
+ # @param value The value to compare the column against.
81
+ def build_scalar_comparison_node(node, model_class, key, comparator, value)
82
+ combinator, comparisons = CanCanCan::BabySqueel::AttributeMapper.
83
+ squeel_comparison_for(model_class, key, comparator, value)
84
+ attribute = node.__send__(comparisons.first.first)
85
+
86
+ expression = comparisons.reduce(nil) do |left_expression, (_, comparator, value)|
87
+ right_expression = attribute.dup.public_send(comparator, value)
88
+ next right_expression unless left_expression
89
+
90
+ left_expression.public_send(combinator, right_expression)
91
+ end
92
+
93
+ [expression, []]
94
+ end
95
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+ module CanCanCan::BabySqueel::ExpressionCombinator
3
+ # This true expression is used to indicate a condition that is always satisfied.
4
+ ALWAYS_TRUE = Arel::Nodes::SqlLiteral.new('1=1').freeze
5
+
6
+ # This true expression is used to indicate a condition that is never satisfied.
7
+ ALWAYS_FALSE = Arel::Nodes::SqlLiteral.new('1=0').freeze
8
+
9
+ # Combines two Squeel expressions. This is aware of the +ALWAYS_TRUE+ and +ALWAYS_FALSE+
10
+ # constants and performs simplification.
11
+ #
12
+ # @param [Squeel::Nodes::Node] left_expression The left expression.
13
+ # @param [Array] left_expression_joins An array of joins which the Squeel expression must be
14
+ # joined to.
15
+ # @param [Symbol] operator The operator to combine with. This must be either +:and+ or +:or+.
16
+ # @param [Squeel::Nodes::Node] right_expression The right expression.
17
+ # @param [Array] right_expression_joins An array of joins which the Squeel expression must be
18
+ # joined to.
19
+ # @return [Array<(Squeel::Nodes::Node, Array)>] A tuple containing the combination of the given
20
+ # expressions, as well as an array of joins which the Squeel expression must be joined to.
21
+ def combine_squeel_expressions(left_expression, left_expression_joins, operator,
22
+ right_expression, right_expression_joins)
23
+ case operator
24
+ when :and then conjunction_expressions(left_expression, left_expression_joins,
25
+ right_expression, right_expression_joins)
26
+ when :or then disjunction_expressions(left_expression, left_expression_joins,
27
+ right_expression, right_expression_joins)
28
+ else
29
+ raise ArgumentError, "#{operator} must either be :and or :or"
30
+ end
31
+ end
32
+
33
+ # Computes the conjunction of the two Squeel expressions.
34
+ #
35
+ # Boolean simplification is done for the +ALWAYS_TRUE+ and +ALWAYS_FALSE+ values.
36
+ # @param [Squeel::Nodes::Node] left_expression The left expression.
37
+ # @param [Array] left_expression_joins An array of joins which the Squeel expression must be
38
+ # joined to.
39
+ # @param [Squeel::Nodes::Node] right_expression The right expression.
40
+ # @param [Array] right_expression_joins An array of joins which the Squeel expression must be
41
+ # joined to.
42
+ # @return [Array<(Squeel::Nodes::Node, Array)>] A tuple containing the conjunction of the left and
43
+ # right expressions, as well as an array of joins which the Squeel expression must be joined to.
44
+ def conjunction_expressions(left_expression, left_expression_joins, right_expression,
45
+ right_expression_joins)
46
+ if left_expression == ALWAYS_FALSE || right_expression == ALWAYS_FALSE
47
+ [ALWAYS_FALSE, []]
48
+ elsif left_expression == ALWAYS_TRUE
49
+ [right_expression, right_expression_joins]
50
+ elsif right_expression == ALWAYS_TRUE
51
+ [left_expression, left_expression_joins]
52
+ else
53
+ [left_expression.and(right_expression), left_expression_joins + right_expression_joins]
54
+ end
55
+ end
56
+
57
+ # Computes the disjunction of the two Squeel expressions.
58
+ #
59
+ # Boolean simplification is done for the +ALWAYS_TRUE+ and +ALWAYS_FALSE+ values.
60
+ # @param [Squeel::Nodes::Node] left_expression The left expression.
61
+ # @param [Array] left_expression_joins An array of joins which the Squeel expression must be
62
+ # joined to.
63
+ # @param [Squeel::Nodes::Node] right_expression The right expression.
64
+ # @param [Array] right_expression_joins An array of joins which the Squeel expression must be
65
+ # joined to.
66
+ # @return [Array<(Squeel::Nodes::Node, Array)>] A tuple containing the disjunction of the left and
67
+ # right expressions, as well as an array of joins which the Squeel expression must be joined to.
68
+ def disjunction_expressions(left_expression, left_expression_joins, right_expression,
69
+ right_expression_joins)
70
+ if left_expression == ALWAYS_TRUE || right_expression == ALWAYS_TRUE
71
+ [ALWAYS_TRUE, []]
72
+ elsif left_expression == ALWAYS_FALSE
73
+ [right_expression, right_expression_joins]
74
+ elsif right_expression == ALWAYS_FALSE
75
+ [left_expression, left_expression_joins]
76
+ else
77
+ [left_expression.or(right_expression), left_expression_joins + right_expression_joins]
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ module CanCanCan; end
3
+ module CanCanCan::BabySqueel
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cancancan-baby_squeel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Yap
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-02-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: coveralls
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activerecord
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '4.1'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '4.1'
111
+ - !ruby/object:Gem::Dependency
112
+ name: cancancan
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '1.10'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '1.10'
125
+ - !ruby/object:Gem::Dependency
126
+ name: baby_squeel
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 1.1.5
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 1.1.5
139
+ description: Implements CanCanCan's rule-based record fetching using BabySqueel.
140
+ email:
141
+ - jeremy.yapjl@gmail.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".gitignore"
147
+ - ".hound.yml"
148
+ - ".idea/cancancan-squeel.iml"
149
+ - ".rspec"
150
+ - ".rubocop.unhound.yml"
151
+ - ".rubocop.yml"
152
+ - ".travis.yml"
153
+ - CHANGELOG.md
154
+ - Gemfile
155
+ - README.md
156
+ - Rakefile
157
+ - cancancan-baby_squeel.gemspec
158
+ - lib/cancancan/baby_squeel.rb
159
+ - lib/cancancan/baby_squeel/active_record_disabler.rb
160
+ - lib/cancancan/baby_squeel/attribute_mapper.rb
161
+ - lib/cancancan/baby_squeel/baby_squeel_adapter.rb
162
+ - lib/cancancan/baby_squeel/expression_builder.rb
163
+ - lib/cancancan/baby_squeel/expression_combinator.rb
164
+ - lib/cancancan/baby_squeel/version.rb
165
+ homepage: https://github.com/jeremyyap/cancancan-baby_squeel
166
+ licenses:
167
+ - MIT
168
+ metadata: {}
169
+ post_install_message:
170
+ rdoc_options: []
171
+ require_paths:
172
+ - lib
173
+ required_ruby_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ required_rubygems_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ requirements: []
184
+ rubyforge_project:
185
+ rubygems_version: 2.6.8
186
+ signing_key:
187
+ specification_version: 4
188
+ summary: BabySqueel database adapter for CanCanCan.
189
+ test_files: []