classy_filter 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7d39815c9e5cc38bad174755097ce7e62140800e77123b509ffcc0d9194cade0
4
+ data.tar.gz: bffc8a05381de967fd6b051025431413c639820988f71b364f0a7af240516fa1
5
+ SHA512:
6
+ metadata.gz: 4ecdba44e99129941af52a39b8cb2d11c3917ed427a060c0372292e502a3324a046ea13d4d96a56ffcb9bc7211a0184d8d16d457c2bb1a06bc71aca9114c24ee
7
+ data.tar.gz: 45473de6711f8e184a051c74c437394f9905bb5a3c3c6797e776dbbb69ffe83a265f25d49db3f5126c89f53ce1b6d30fd8574c649a71e1f176857b4b3cd08b47
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in classy_filter.gemspec
6
+ gemspec
@@ -0,0 +1,45 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ classy_filter (0.1.1)
5
+ sequel (~> 5.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ coderay (1.1.2)
11
+ diff-lcs (1.3)
12
+ method_source (0.9.0)
13
+ pry (0.11.3)
14
+ coderay (~> 1.1.0)
15
+ method_source (~> 0.9.0)
16
+ rake (10.5.0)
17
+ rspec (3.7.0)
18
+ rspec-core (~> 3.7.0)
19
+ rspec-expectations (~> 3.7.0)
20
+ rspec-mocks (~> 3.7.0)
21
+ rspec-core (3.7.1)
22
+ rspec-support (~> 3.7.0)
23
+ rspec-expectations (3.7.0)
24
+ diff-lcs (>= 1.2.0, < 2.0)
25
+ rspec-support (~> 3.7.0)
26
+ rspec-mocks (3.7.0)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.7.0)
29
+ rspec-support (3.7.1)
30
+ sequel (5.9.0)
31
+ sqlite3 (1.3.13)
32
+
33
+ PLATFORMS
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ bundler (~> 1.16)
38
+ classy_filter!
39
+ pry
40
+ rake (~> 10.0)
41
+ rspec (~> 3.0)
42
+ sqlite3 (~> 1.0)
43
+
44
+ BUNDLED WITH
45
+ 1.16.1
@@ -0,0 +1,92 @@
1
+ # ClassyFilter
2
+
3
+ ClassyFilter is a customizeable class-based filtering library. Currently it's only build for Sequel, but there are plans to expand!
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'classy_filter'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install classy_filter
20
+
21
+ ## Usage
22
+
23
+ ### Writing a filter
24
+
25
+ To begin using this gem, you must first define your filter class, deriving it from `ClassyFilter::Base`. In this class, you'd specify your filter fields, extra predicates and coercions (should you need those).
26
+
27
+ Let's begin with an example. Assuming you have a table named `people` with this structure:
28
+
29
+ | name | type |
30
+ | ---- | ---- |
31
+ | first_name | varchar(1023) |
32
+ | last_name | varchar(1023) |
33
+ | date_of_birth | date |
34
+ | trust_level | integer |
35
+
36
+ You could write a filter like this:
37
+
38
+ ```ruby
39
+ class MyFilter < ClassyFilter::Base
40
+ filter_field :first_name # (1)
41
+ filter_field :last_name_prefix, attribute: :last_name, predicate: :starts_with_i # (2)
42
+ filter_field :dob_after, attribute: :date_of_birth, predicate: :gteq, coercion: :integer # (3)
43
+ filter_field :rough_trust, attribute: :trust_level, predicate: :gt_10x, coercion: :integer # (4)
44
+
45
+ predicate :gt_10x, ->(dataset, attr, input) { dataset.where { |r| Sequel[attr] > input } } # (5)
46
+ end
47
+ ```
48
+
49
+ Let's walk through this filter line by line.
50
+
51
+ Line `(1)` shows us how to define a very basic filter. This definition lets our filter accept the `first_name` parameter
52
+ and perform simple filtering by equality or inclusion.
53
+
54
+ Line `(2)` shows us how to define a filter with a custom predicate and parameter name. Our filter will accept the
55
+ `last_name_prefix` parameter and filter by the `last_name column`, using `ILIKE` to do it.
56
+
57
+ Line `(3)` shows us how to define a filter not just with a custom predicate, but also with a coercion. Before performing
58
+ the filtering, our filter will attempt to coerce the `dob_after` parameter into an `Integer`.
59
+
60
+ Line `(5)` shows us a custom predicate (that's used on the line `(4)`). A predicate is defined using the
61
+ `ClassyFilter::Base.predicate` method that accepts the predicate name and a proc that receives the dataset, the
62
+ attribute name and the parameter value.
63
+
64
+ You can see all existing predicates [here](/lib/classy_filter/predicates.rb) and all existing coercions
65
+ [here.](lib/classy_filter/coercions.rb)
66
+
67
+ ### Performing the filtering
68
+
69
+ This one is actually quite easy. Assuming we have a database named `DB` and receive a hash named `params` from somewhere
70
+ (say, the URL query string), we can use our filter as follows:
71
+
72
+ ```ruby
73
+ ds = DB[:people]
74
+ MyFilter.new(params).call(ds)
75
+ ```
76
+
77
+ ## Development
78
+
79
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. You can also run `pry -rclassy_filter` for an interactive prompt that will allow you to experiment.
80
+
81
+ ## TODO
82
+
83
+ ❗ Add collection (array/hash) coercions
84
+ ❗ Move the Sequel implementation out of the gem
85
+ ⬜ Try to find out a way to simplify predicates definitions
86
+ ⬜ More tests. Never enough tests.
87
+ ❓ Add a better coercion library. Dry-rb? Or maybe Hashie?
88
+
89
+
90
+ ## Contributing
91
+
92
+ Bug reports and pull requests are welcome on GitLab at https://gitlab.com/art-solopov/classy_filter.
@@ -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,29 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "classy_filter/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "classy_filter"
8
+ spec.version = ClassyFilter::VERSION
9
+ spec.authors = ["Artemiy Solopov"]
10
+ spec.email = ["art-solopov@yandex.ru"]
11
+
12
+ spec.summary = %q{A cutomizeable class-based filtering/searching library for Sequel}
13
+ spec.description = spec.summary
14
+ spec.homepage = "https://gitlab.com/art-solopov/classy_filter"
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency 'sequel', '~> 5.0'
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.16"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ spec.add_development_dependency 'sqlite3', '~> 1.0'
28
+ spec.add_development_dependency 'pry'
29
+ end
@@ -0,0 +1,43 @@
1
+ require 'classy_filter/version'
2
+ require 'classy_filter/predicates'
3
+ require 'classy_filter/coercions'
4
+ require 'classy_filter/class_methods'
5
+
6
+ module ClassyFilter
7
+ Field = Struct.new(:param_name, :attribute, :predicate, :coercion)
8
+
9
+ class Base
10
+ extend Forwardable
11
+
12
+ extend ClassMethods
13
+
14
+ attr_reader :params
15
+
16
+ def initialize(params)
17
+ @params = params.transform_keys(&:to_s)
18
+ end
19
+
20
+ def call(dataset)
21
+ filter_fields.reduce(dataset) do |ds, (value, field)|
22
+ value = coercions_module.coerce(value, field.coercion) if field.coercion
23
+ attribute = field.attribute || field.param_name
24
+ predicates_module.send(field.predicate, ds, attribute, value)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def_delegator :"self.class", :filter_fields, :all_filter_fields
31
+ def_delegator :"self.class", :predicates_module
32
+ def_delegator :"self.class", :coercions_module
33
+
34
+ def filter_fields
35
+ all_filter_fields
36
+ .to_a
37
+ .lazy
38
+ .map { |ff| [params[ff.param_name.to_s], ff] }
39
+ .reject { |param, _| param.nil? }
40
+ .reject { |param, _| param.is_a?(String) && param.empty? }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ module ClassyFilter
2
+ module ClassMethods
3
+ attr_reader :filter_fields
4
+
5
+ def filter_field(param_name, attribute: nil,
6
+ predicate: :eq_in, coercion: nil)
7
+ @filter_fields ||= []
8
+ @filter_fields << Field.new(param_name, attribute,
9
+ predicate, coercion)
10
+ end
11
+
12
+ def predicate(name, body)
13
+ predicates_module.define_method(name, &body)
14
+ end
15
+
16
+ def coercion(name, body)
17
+ coercions_module.define_method(name, &body)
18
+ coercions_module.private(name)
19
+ end
20
+
21
+ def predicates_module
22
+ @predicates_module || ClassyFilter::Predicates
23
+ end
24
+
25
+ def coercions_module
26
+ @coercions_module || ClassyFilter::Coercions
27
+ end
28
+
29
+ protected
30
+
31
+ def build_predicates_module!(base)
32
+ @predicates_module = Module.new do
33
+ extend base.predicates_module
34
+ extend self
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def inherited(subclass)
41
+ subclass.build_predicates_module!(self)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,50 @@
1
+ module ClassyFilter
2
+ module Coercions
3
+ extend self
4
+
5
+ def coerce(value, coercion)
6
+ case coercion
7
+ when Symbol then send(coercion, value)
8
+ when ->(x) { x.respond_to?(:call) } then coercion.call(value)
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ # Scalar conversions
15
+ # Those are methods that receive a value and return
16
+ # the coerced value
17
+
18
+ def integer(value)
19
+ Integer(value)
20
+ end
21
+
22
+ def float(value)
23
+ Float(value)
24
+ end
25
+
26
+ def boolean(value)
27
+ case value
28
+ when true, 'true', 'TRUE', 1, '1' then true
29
+ when false, 'false', 'FALSE', 0, '0' then false
30
+ end
31
+ end
32
+
33
+ def date(value)
34
+ case value
35
+ when Date then value
36
+ when Array then Date.new(*value)
37
+ else Date.parse(value.to_s)
38
+ end
39
+ end
40
+
41
+ def timestamp(value)
42
+ case value
43
+ when Time then value
44
+ when DateTime then value.to_time
45
+ when Array then Time.new(*value)
46
+ else DateTime.parse(value.to_s).to_time
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,52 @@
1
+ module ClassyFilter
2
+ module Predicates
3
+ extend self
4
+
5
+ def eq_in(ds, attribute, input)
6
+ ds.where(attribute => input)
7
+ end
8
+
9
+ alias eq eq_in
10
+ alias :in eq_in
11
+
12
+ # TODO: think about refactoring
13
+
14
+ def gt(ds, attribute, input)
15
+ ds.where { |_r| sequel_col(ds, attribute) > input }
16
+ end
17
+
18
+ def lt(ds, attribute, input)
19
+ ds.where { |_r| sequel_col(ds, attribute) < input }
20
+ end
21
+
22
+ def gteq(ds, attribute, input)
23
+ ds.where { |_r| sequel_col(ds, attribute) >= input }
24
+ end
25
+
26
+ def lteq(ds, attribute, input)
27
+ ds.where { |_r| sequel_col(ds, attribute) <= input }
28
+ end
29
+
30
+ def starts_with(ds, attribute, input)
31
+ ds.where { |_r| Sequel.like(sequel_col(ds, attribute), input + '%') }
32
+ end
33
+
34
+ def starts_with_i(ds, attribute, input)
35
+ ds.where { |_r| Sequel.ilike(sequel_col(ds, attribute), input + '%') }
36
+ end
37
+
38
+ def contains(ds, attribute, input)
39
+ ds.where { |_r| Sequel.like(sequel_col(ds, attribute), "%#{input}%") }
40
+ end
41
+
42
+ def contains_i(ds, attribute, input)
43
+ ds.where { |_r| Sequel.ilike(sequel_col(ds, attribute), "%#{input}%") }
44
+ end
45
+
46
+ private
47
+
48
+ def sequel_col(ds, attribute)
49
+ Sequel[ds.first_source_table][attribute.to_sym]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module ClassyFilter
2
+ VERSION = "0.1.1"
3
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: classy_filter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Artemiy Solopov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-07-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sequel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
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
+ description: A cutomizeable class-based filtering/searching library for Sequel
98
+ email:
99
+ - art-solopov@yandex.ru
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - Gemfile
107
+ - Gemfile.lock
108
+ - README.md
109
+ - Rakefile
110
+ - classy_filter.gemspec
111
+ - lib/classy_filter.rb
112
+ - lib/classy_filter/class_methods.rb
113
+ - lib/classy_filter/coercions.rb
114
+ - lib/classy_filter/predicates.rb
115
+ - lib/classy_filter/version.rb
116
+ homepage: https://gitlab.com/art-solopov/classy_filter
117
+ licenses:
118
+ - MIT
119
+ metadata: {}
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubyforge_project:
136
+ rubygems_version: 2.7.3
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: A cutomizeable class-based filtering/searching library for Sequel
140
+ test_files: []