classy_filter 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []