acts_as_explorable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +23 -0
  5. data/Appraisals +12 -0
  6. data/Gemfile +15 -0
  7. data/Guardfile +5 -0
  8. data/MIT-LICENSE +20 -0
  9. data/README.md +52 -0
  10. data/Rakefile +22 -0
  11. data/acts_as_explorable.gemspec +28 -0
  12. data/gemfiles/activerecord_4.0.gemfile +19 -0
  13. data/gemfiles/activerecord_4.1.gemfile +19 -0
  14. data/gemfiles/activerecord_4.2.gemfile +20 -0
  15. data/lib/acts_as_explorable/configuration.rb +9 -0
  16. data/lib/acts_as_explorable/element/base.rb +8 -0
  17. data/lib/acts_as_explorable/element/dynamic_filter.rb +15 -0
  18. data/lib/acts_as_explorable/element/in.rb +18 -0
  19. data/lib/acts_as_explorable/element/sort.rb +20 -0
  20. data/lib/acts_as_explorable/element.rb +83 -0
  21. data/lib/acts_as_explorable/explorable.rb +76 -0
  22. data/lib/acts_as_explorable/ext/string.rb +24 -0
  23. data/lib/acts_as_explorable/parser.rb +61 -0
  24. data/lib/acts_as_explorable/query.rb +36 -0
  25. data/lib/acts_as_explorable/version.rb +3 -0
  26. data/lib/acts_as_explorable.rb +45 -0
  27. data/lib/tasks/acts_as_explorable_tasks.rake +4 -0
  28. data/spec/factories/players.rb +57 -0
  29. data/spec/lib/acts_as_explorable/element/dynamic_filter_spec.rb +19 -0
  30. data/spec/lib/acts_as_explorable/element/in.rb +12 -0
  31. data/spec/lib/acts_as_explorable/element_spec.rb +16 -0
  32. data/spec/lib/acts_as_explorable/explorable_spec.rb +28 -0
  33. data/spec/lib/acts_as_explorable/parser_spec.rb +35 -0
  34. data/spec/lib/acts_as_explorable/query_spec.rb +51 -0
  35. data/spec/spec_helper.rb +24 -0
  36. data/spec/support/database.rb +64 -0
  37. data/spec/support/database_cleaner.rb +21 -0
  38. metadata +185 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cf09cef7bef8fb847eceda7eb6ab0ffe5a3d9d72
4
+ data.tar.gz: a7b61b25db90271f6787c8e3140ca097500f5445
5
+ SHA512:
6
+ metadata.gz: 3856e51cda905223c8d4c3d35456d7db75d69573bf810c0d61f911f0432e3069f24ea53c1bb749690d8ad58e4750755d63a46f0c7592fabf258b5f650698d16b
7
+ data.tar.gz: 8f2fec4eb042b5a9f48aa665cc37c51b3a5e7251c9e9f485de2b415db2bc9c8df0a450dd97c74efaacd03011118bbfdea5e19c6f3d9814cef64d50077e6d0f6f
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ *.log
2
+ *.sqlite3
3
+ .bundle
4
+ .ruby-version
5
+ .yarddoc
6
+ pkg/*
7
+ doc/*
8
+ *.gem
9
+ *.lock
10
+ .bundle
11
+ .yardoc
12
+ nbproject/*
13
+ Gemfile.lock
14
+ bin
15
+ spec/*.log
16
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --backtrace
data/.travis.yml ADDED
@@ -0,0 +1,23 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1
4
+ - 2.2
5
+ - 2.0.0
6
+ - rbx-2
7
+
8
+ gemfile:
9
+ - gemfiles/activerecord_4.0.gemfile
10
+ - gemfiles/activerecord_4.1.gemfile
11
+ - gemfiles/activerecord_4.2.gemfile
12
+
13
+ sudo: false
14
+
15
+ bundler_args: '--without local_development yard --jobs 3 --retry 3'
16
+
17
+ script: bundle exec rake
18
+
19
+ matrix:
20
+ fast_finish: true
21
+ allow_failures:
22
+ - gemfile: gemfiles/activerecord_edge.gemfile
23
+ - rvm: rbx-2
data/Appraisals ADDED
@@ -0,0 +1,12 @@
1
+ appraise "activerecord-4.0" do
2
+ gem "activerecord", github: "rails/rails" , branch: '4-0-stable'
3
+ end
4
+
5
+ appraise "activerecord-4.1" do
6
+ gem "activerecord", github: "rails/rails" , branch: '4-1-stable'
7
+ end
8
+
9
+ appraise "activerecord-4.2" do
10
+ gem "railties", github: "rails/rails" , branch: '4-2-stable'
11
+ gem "activerecord", github: "rails/rails" , branch: '4-2-stable'
12
+ end
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :doc do
6
+ gem 'yard'
7
+ end
8
+
9
+ group :local_development do
10
+ gem 'guard'
11
+ gem 'guard-rspec'
12
+ gem 'appraisal'
13
+ gem 'rake'
14
+ gem 'byebug', platform: :mri_21
15
+ end
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'rspec', cmd: 'bundle exec rspec' do
2
+ watch(%r{^spec/.+_spec\.rb})
3
+ watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Mathias Schneider
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Acts As Explorable
2
+
3
+ [![Build Status](https://travis-ci.org/hiasinho/acts_as_explorable.svg?branch=develop)](https://travis-ci.org/hiasinho/acts_as_explorable) [![Code Climate](https://codeclimate.com/github/hiasinho/acts_as_explorable/badges/gpa.svg)](https://codeclimate.com/github/hiasinho/acts_as_explorable) [![Inline docs](http://inch-ci.org/github/hiasinho/acts_as_explorable.svg?branch=develop)](http://inch-ci.org/github/hiasinho/acts_as_explorable)
4
+
5
+ Acts As Explorable is a Ruby Gem specifically written for ActiveRecord models.
6
+
7
+ ## Installation
8
+
9
+ ### Supported Ruby and Rails versions
10
+
11
+ * Ruby 2.0.0, 2.1.0
12
+ * Rails 4.0, 4.1, 4.2+
13
+
14
+ ### Install
15
+
16
+ THIS GEM IS NOT RELEASED YET. SO, THIS WON'T WORK!
17
+
18
+ Just add the following to your Gemfile.
19
+
20
+ ```ruby
21
+ gem 'acts_as_explorable', '~> 0.0.1'
22
+ ```
23
+
24
+ And follow that up with a ``bundle install``.
25
+
26
+ ## Usage
27
+
28
+ TODO
29
+
30
+ ## Testing
31
+
32
+ All tests follow the RSpec format and are located in the spec directory.
33
+ They can be run with:
34
+
35
+ ```
36
+ rake spec
37
+ ```
38
+
39
+ ## License
40
+
41
+ Acts as explorable is released under the [MIT License](http://www.opensource.org/licenses/MIT).
42
+
43
+ ## TODO
44
+
45
+ ### v0.1
46
+ - Fill this README file
47
+ - Release v0.0.1 to rubygems
48
+
49
+ ###v0.x
50
+ - Add tests for postgres and mysql
51
+ - Query string validation helper for use in forms
52
+ - Use methods in addition to fields
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ desc 'Default: run specs.'
5
+ task default: :spec
6
+
7
+ require 'rspec/core/rake_task'
8
+ RSpec::Core::RakeTask.new do |t|
9
+ t.pattern = 'spec/**/*_spec.rb'
10
+ end
11
+
12
+ require 'yard'
13
+ YARD::Rake::YardocTask.new do |t|
14
+ t.options << '--output-dir' << './doc'
15
+ t.options << '--no-private'
16
+ t.options << '--protected'
17
+ t.options << '--readme' << 'README.md'
18
+ t.options << '--hide-tag' << 'return'
19
+ t.options << '--hide-tag' << 'param'
20
+ end
21
+
22
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,28 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+ require 'acts_as_explorable/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'acts_as_explorable'
6
+ s.version = ActsAsExplorable::VERSION
7
+ s.authors = ['Mathias Schneider']
8
+ s.email = ['mathias@hiasinho.com']
9
+ s.homepage = 'https://github.com/hiasinho/acts_as_explorable'
10
+ s.summary = 'Adds GitHub-like search function to your models'
11
+ s.description = 'Adds GitHub-like search function to your models'
12
+ s.license = 'MIT'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
16
+ s.require_paths = ['lib']
17
+ s.required_ruby_version = '>= 2.0.0'
18
+
19
+ s.add_runtime_dependency 'activerecord', ['>= 4.0', '< 5']
20
+
21
+ s.add_development_dependency 'sqlite3'
22
+
23
+ s.add_development_dependency 'rspec-rails'
24
+ s.add_development_dependency 'rspec-its'
25
+ s.add_development_dependency 'rspec'
26
+ s.add_development_dependency 'factory_girl_rails', '~> 4.0'
27
+ s.add_development_dependency 'database_cleaner'
28
+ end
@@ -0,0 +1,19 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", :github => "rails/rails", :branch => "4-0-stable"
6
+
7
+ group :doc do
8
+ gem "yard"
9
+ end
10
+
11
+ group :local_development do
12
+ gem "guard"
13
+ gem "guard-rspec"
14
+ gem "appraisal"
15
+ gem "rake"
16
+ gem "byebug", :platform => :mri_21
17
+ end
18
+
19
+ gemspec :path => "../"
@@ -0,0 +1,19 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", :github => "rails/rails", :branch => "4-1-stable"
6
+
7
+ group :doc do
8
+ gem "yard"
9
+ end
10
+
11
+ group :local_development do
12
+ gem "guard"
13
+ gem "guard-rspec"
14
+ gem "appraisal"
15
+ gem "rake"
16
+ gem "byebug", :platform => :mri_21
17
+ end
18
+
19
+ gemspec :path => "../"
@@ -0,0 +1,20 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "railties", :github => "rails/rails", :branch => "4-2-stable"
6
+ gem "activerecord", :github => "rails/rails", :branch => "4-2-stable"
7
+
8
+ group :doc do
9
+ gem "yard"
10
+ end
11
+
12
+ group :local_development do
13
+ gem "guard"
14
+ gem "guard-rspec"
15
+ gem "appraisal"
16
+ gem "rake"
17
+ gem "byebug", :platform => :mri_21
18
+ end
19
+
20
+ gemspec :path => "../"
@@ -0,0 +1,9 @@
1
+ module ActsAsExplorable
2
+ class Configuration
3
+ attr_accessor :filters
4
+
5
+ def initialize
6
+ @filters = {}
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ module ActsAsExplorable::Element
2
+ #
3
+ # Base class for {ActsAsExplorable::Element Elements}.
4
+ #
5
+ class Base
6
+ include ActsAsExplorable::Element
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ module ActsAsExplorable::Element
2
+ #
3
+ # Generates a +where+ clause to look up the searched string in the given columns
4
+ #
5
+ class DynamicFilter < Base
6
+ def after_init
7
+ @query_type = :where
8
+ end
9
+
10
+ def render
11
+ @query_parts << table[type].lower.in(@parameters)
12
+ @full_query = @query_parts.first
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ module ActsAsExplorable::Element
2
+ #
3
+ # Generates a +where+ clause to look up the searched string in the given columns
4
+ #
5
+ class In < Base
6
+ def after_init
7
+ @query_type = :where
8
+ end
9
+
10
+ def render
11
+ @parameters.each do |f|
12
+ @query_parts <<
13
+ table[f.to_sym].matches_any(@query_string.map { |q| "%#{q}%" })
14
+ end
15
+ @full_query = @query_parts.inject(:or)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module ActsAsExplorable::Element
2
+ #
3
+ # Generates an +order+ query part to sort by the given columns
4
+ #
5
+ class Sort < Base
6
+ def after_init
7
+ @query_type = :reorder
8
+ end
9
+
10
+ def render
11
+ @full_query = @parameters.map do |f|
12
+ if f =~ /(-asc|-desc)/
13
+ { f.split('-').first.to_sym => f.split('-').last.to_sym }
14
+ else
15
+ { f.to_sym => :desc }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,83 @@
1
+ require 'acts_as_explorable/element/base'
2
+ require 'acts_as_explorable/element/in'
3
+ require 'acts_as_explorable/element/sort'
4
+ require 'acts_as_explorable/element/dynamic_filter'
5
+
6
+ module ActsAsExplorable
7
+ module Element
8
+ attr_accessor :query_type, :model, :parameters, :query_string, :query_parts,
9
+ :full_query
10
+
11
+ #
12
+ # This method acts as a factory to build a concrete element
13
+ #
14
+ # @example
15
+ # ActsAsExplorable::Element.build(:in, 'Zlatan in:first_name', Player)
16
+ #
17
+ # @param type [Symbol] The element type to be build
18
+ # @param query [String] The query string
19
+ # @param model [ActiveRecord::Base] Anactive record model
20
+ #
21
+ # @return [ActsAsExplorable::Element] A concrete element type
22
+ def self.build(type, query, model)
23
+ klass = Module.nesting.last.const_get('Element').const_get(type.to_s.camelize)
24
+ instance = klass.new(query, model, type)
25
+ rescue NameError
26
+ DynamicFilter.new(query, model, type)
27
+ end
28
+
29
+ def initialize(query, model, element_type = nil)
30
+ query = query.to_acts_as_explorable(ActsAsExplorable.filters.keys)
31
+
32
+ @type = element_type if element_type
33
+ @model = model
34
+ @query_string = query[:values]
35
+ @query_parts = []
36
+ filter_parameters(query[:params])
37
+ after_init
38
+
39
+ render if @parameters.present?
40
+ end
41
+
42
+ def after_init; end
43
+
44
+ def execute(query_object)
45
+ query_object.send(@query_type, @full_query)
46
+ end
47
+
48
+ protected
49
+
50
+ def filter_parameters(params)
51
+ return unless params[type]
52
+ @parameters = params[type].select do |f|
53
+ filters.find do |e|
54
+ /#{e.to_sym}(?:-\w+)?/ =~ f
55
+ end
56
+ end
57
+ end
58
+
59
+ #
60
+ # Returns the Arel table for the current model
61
+ #
62
+ # @return [Arel::Table] Arel table for the current model
63
+ def table
64
+ @model.arel_table
65
+ end
66
+
67
+ def render
68
+ fail "`#render` needs to be implemented for #{self.class.name}"
69
+ end
70
+
71
+ def type
72
+ @type.to_sym || self.class.name.demodulize.underscore.to_sym
73
+ end
74
+
75
+ #
76
+ # Returns the customized filters
77
+ #
78
+ # @return [Hash] The customized filters
79
+ def filters
80
+ ActsAsExplorable.filters[type]
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,76 @@
1
+ module ActsAsExplorable
2
+ module Explorable
3
+ def self.extended(base)
4
+ base.class_eval do
5
+ def self.explorable?
6
+ false
7
+ end
8
+ end
9
+ end
10
+
11
+ # Configure ActsAsExplorable's behavior in a model.
12
+ #
13
+ # The plugin can be customized using parameters or through a `block`.
14
+ #
15
+ # class Player < ActiveRecord::Base
16
+ # extend ActsAsExplorable
17
+ # explorable in: [:first_name, :last_name, :position, :city, :club],
18
+ # sort: [:first_name, :last_name, :position, :city, :club, :created_at],
19
+ # position: ['GK', 'MF', 'FW']
20
+ # end
21
+ #
22
+ # Using a block (TODO: This will be available in future versions):
23
+ #
24
+ # class Player < ActiveRecord::Base
25
+ # extend ActsAsExplorable
26
+ # explorable do |config|
27
+ # config.filters = {
28
+ # in: [:first_name, :last_name, :position, :city, :club],
29
+ # sort: [:first_name, :last_name, :position, :city, :club, :created_at],
30
+ # position: ['GK', 'MF', 'FW']
31
+ # }
32
+ # end
33
+ # end
34
+ #
35
+ # @yield Provides access to the model class's config, which
36
+ # allows to customize types and filters
37
+ #
38
+ # @yieldparam config The model class's {ActsAsExplorable::Configuration config}.
39
+ def explorable(filters = {}, &_block)
40
+ class_eval do
41
+ def self.explorable?
42
+ true
43
+ end
44
+ end
45
+
46
+ if block_given?
47
+ ActsAsExplorable.setup { |config| yield config }
48
+ else
49
+ explorable_set_filters filters
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ # Configure ActsAsExplorable's permitted filters per type in a model.
56
+ #
57
+ # class Person < ActiveRecord::Base
58
+ # extend ActsAsExplorable
59
+ # explorable_filters in: [:first_name, :last_name, :city],
60
+ # sort: [:first_name, :last_name, :city, :created_at]
61
+ # end
62
+ #
63
+ # @param [Hash] filters Filters for types
64
+ # @return [Array] Permitted filters
65
+ #
66
+ def explorable_set_filters(filters = {})
67
+ ActsAsExplorable.filters = filters if filters.present?
68
+
69
+ ActsAsExplorable.filters.each_pair do |f, _a|
70
+ ActsAsExplorable.filters[f].map!(&:downcase)
71
+ end
72
+
73
+ ActsAsExplorable.filters
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,24 @@
1
+ #
2
+ # A String extension for ActsAsExplorable
3
+ #
4
+ class String
5
+ # Converts the String into a Hash for ActsAsExplorable.
6
+ #
7
+ # == Returns:
8
+ # A Hash providing 2 keys:
9
+ # - <tt>:values</tt> holds the search text values
10
+ # - <tt>:params</tt> holds parameters (fields) to search in
11
+ #
12
+ # @example
13
+ # query = "Foo Bar in:name,body sort:created_at-asc"
14
+ # query.to_acts_as_explorable
15
+ # # => {:values=>["Foo", "Bar"], :params=>{:in=>["name", "body"], :sort=>["created_at-asc"]}}
16
+ #
17
+ # @param keys [Array<String, Symbol>, nil] Array of accepted keys
18
+ #
19
+ # @return [Hash] Converted query
20
+ def to_acts_as_explorable(*keys)
21
+ return nil if self.blank?
22
+ ActsAsExplorable::Parser.transform(self, *keys)
23
+ end
24
+ end
@@ -0,0 +1,61 @@
1
+ module ActsAsExplorable
2
+ ##
3
+ # Transforms query strings to a Hash that can be used by ActsAsExplorable.
4
+ #
5
+ # @example
6
+ # ActsAsExplorable::Parser.transform('Zlatan in:first_name')
7
+ # => { values: ["Zlatan"], params: { in: ["first_name"] } }
8
+ class Parser
9
+ attr_reader :values, :params, :props
10
+
11
+ #
12
+ # Returns a transformed query Hash using the given query string
13
+ # @param query_string [String] A query string
14
+ # @param keys [Array] An Array of transformation rules
15
+ #
16
+ # @return [Hash] Transformed query
17
+ def self.transform(query_string, *keys)
18
+ instance = new(query_string)
19
+ instance.parse(*keys)
20
+ end
21
+
22
+ def initialize(query_string)
23
+ @query_string = query_string
24
+ split_query_string
25
+ end
26
+
27
+ #
28
+ # Parses the query string
29
+ # @param keys [Hash] An Array of transformation rules
30
+ #
31
+ # @return [Hash] Transformed query
32
+ def parse(*keys)
33
+ split_query_string
34
+
35
+ @props.each do |p|
36
+ key, params = p.split(':').first.to_sym, p.split(':').last.split(',')
37
+ next if !keys.flatten.include?(key) && !keys.empty?
38
+ @params[key] ||= []
39
+ @params[key] = @params[key] | params.map(&:downcase)
40
+ end
41
+
42
+ { values: @values, params: @params }
43
+ end
44
+
45
+ private
46
+
47
+ def split_query_string
48
+ @values = []
49
+ @params = {}
50
+ @props = []
51
+
52
+ @query_string.split(/\s+/).each do |q|
53
+ if q =~ /\w+:[\w,-]+/
54
+ @props << q
55
+ else
56
+ @values << q
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,36 @@
1
+ module ActsAsExplorable
2
+ #
3
+ # Adds the search scope to a model
4
+ #
5
+ module Query
6
+ #
7
+ # Initiates a search with the given query string and returns an
8
+ # <tt>ActiveRecord::Relation</tt> scope object.
9
+ #
10
+ # The query string's *syntax* is described in {file:docs/yard/README.md#syntax the Readme File}
11
+ #
12
+ # The search method can be used just like a scope.
13
+ #
14
+ # Foo.search("Foo Bar in:name,body sort:created_at-asc").to_sql
15
+ # # => "SELECT foo.* FROM foo WHERE (foo.body ILIKE '%Foo%' OR foo.body ILIKE '%Bar%') ORDER BY foo.created_at ASC"
16
+ #
17
+ # It is also possible to put it in a scope chain like this:
18
+ #
19
+ # Foo.published.search("Foo Bar in:name,body sort:created_at-asc")
20
+ #
21
+ # @param [String] query_string A query string
22
+ # @return [ActiveRecord::Relation] Returns an <tt>ActiveRecord::Relation</tt> scope object
23
+ #
24
+ def search(query_string)
25
+ parts = ActsAsExplorable.filters.keys.map { |t| Element.build(t, query_string, self) }
26
+
27
+ result = all
28
+
29
+ parts.compact.each do |part|
30
+ result = part.execute(result)
31
+ end
32
+
33
+ result
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module ActsAsExplorable
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,45 @@
1
+ require 'active_record'
2
+ require 'active_support/inflector'
3
+ require 'acts_as_explorable/version'
4
+ require 'acts_as_explorable/configuration'
5
+ require 'acts_as_explorable/parser'
6
+ require 'acts_as_explorable/ext/string'
7
+ require 'acts_as_explorable/element'
8
+ require 'acts_as_explorable/explorable'
9
+ require 'acts_as_explorable/query'
10
+
11
+ #
12
+ # ActsAsExplorable Plugin
13
+ #
14
+ # @author hiasinho
15
+ #
16
+ module ActsAsExplorable
17
+ def self.extended(base)
18
+ base.extend Query
19
+ end
20
+
21
+ def self.method_missing(method_name, *args, &block)
22
+ if @configuration.respond_to?(method_name)
23
+ @configuration.send(method_name, *args, &block)
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def self.respond_to?(method_name, _include_private = false)
30
+ @configuration.respond_to? method_name
31
+ end
32
+
33
+ protected
34
+
35
+ def self.setup
36
+ @configuration ||= Configuration.new
37
+ yield @configuration if block_given?
38
+ end
39
+
40
+ setup
41
+ end
42
+
43
+ ActiveSupport.on_load(:active_record) do
44
+ extend ActsAsExplorable::Explorable
45
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :acts_as_explorable do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,57 @@
1
+ FactoryGirl.define do
2
+ factory :player, aliases: [:zlatan, :god_of_football] do
3
+ first_name 'Zlatan'
4
+ last_name 'Ibrahimovic'
5
+ position 'FW'
6
+ city 'Paris'
7
+ club 'PSG'
8
+
9
+ factory :manuel, aliases: [:goalkeeper] do
10
+ first_name 'Manuel'
11
+ last_name 'Neuer'
12
+ position 'GK'
13
+ city 'München'
14
+ club 'FC Bayern München'
15
+ end
16
+
17
+ factory :bastian, aliases: [:schweini] do
18
+ first_name 'Bastian'
19
+ last_name 'Schweinsteiger'
20
+ position 'MF'
21
+ city 'München'
22
+ club 'FC Bayern München'
23
+ end
24
+
25
+ factory :sascha do
26
+ first_name 'Sascha'
27
+ last_name 'Mölders'
28
+ position 'FW'
29
+ city 'Augsburg'
30
+ club 'FC Augsburg'
31
+ end
32
+
33
+ factory :christiano, aliases: [:forward] do
34
+ first_name 'Christiano'
35
+ last_name 'Ronaldo'
36
+ position 'FW'
37
+ city 'Madrid'
38
+ club 'Real Madrid'
39
+ end
40
+
41
+ factory :toni do
42
+ first_name 'Toni'
43
+ last_name 'Kroos'
44
+ position 'MF'
45
+ city 'Madrid'
46
+ club 'Real Madrid'
47
+ end
48
+
49
+ factory :fernando do
50
+ first_name 'Fernando'
51
+ last_name 'Torres'
52
+ position 'FW'
53
+ city 'Madrid'
54
+ club 'Athletico Madrid'
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActsAsExplorable::Element::DynamicFilter do
4
+
5
+ subject { ActsAsExplorable::Element::DynamicFilter }
6
+ let (:element) { ActsAsExplorable::Element }
7
+
8
+ it 'should return a dynamic filter element' do
9
+ expect(element.build(:position, 'position:gk,mf,fw', Player)).to be_instance_of(subject)
10
+ end
11
+
12
+ it 'should filter by position' do
13
+ create(:goalkeeper)
14
+ create(:forward)
15
+
16
+ expect(Player.search('position:gk').count).to eq(1)
17
+ end
18
+
19
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActsAsExplorable::Element::In do
4
+
5
+ subject { ActsAsExplorable::Element::In }
6
+ let (:element) { ActsAsExplorable::Element }
7
+
8
+ it 'should return a dynamic filter element' do
9
+ expect(element.build(:in, 'test in:first_name', Player)).to be_instance_of(subject)
10
+ end
11
+
12
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActsAsExplorable::Element do
4
+
5
+ subject { ActsAsExplorable::Element }
6
+
7
+ describe '.build' do
8
+ it 'should be callable' do
9
+ expect(subject).to respond_to(:build)
10
+ end
11
+
12
+ it 'should create an element' do
13
+ expect(ActsAsExplorable::Element.build(:position, 'position:gk', Player)).to be_instance_of(ActsAsExplorable::Element::DynamicFilter)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ def create_player(klass = Player, factory: :player)
4
+ klass.create(attributes_for(factory))
5
+ end
6
+
7
+ describe ActsAsExplorable::Explorable do
8
+
9
+ let(:zlatan) { create(:zlatan) }
10
+ let(:manuel) { create(:manuel) }
11
+ let(:bastian) { create(:bastian) }
12
+ let(:christiano) { create(:christiano) }
13
+ let(:toni) { create(:toni) }
14
+ let(:fernando) { create(:fernando) }
15
+
16
+ it 'should not be explorable' do
17
+ expect(NotExplorable).not_to be_explorable
18
+ end
19
+
20
+ it 'should be explorable' do
21
+ expect(Explorable).to be_explorable
22
+ end
23
+
24
+ it 'should be customizable through a block' do
25
+ skip 'TODO: This will be available in future versions'
26
+ end
27
+
28
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActsAsExplorable::Parser do
4
+
5
+ subject { ActsAsExplorable::Parser }
6
+
7
+ describe '#new' do
8
+ it 'should be called with a query string' do
9
+ expect { subject.new }.to raise_error
10
+ end
11
+ end
12
+
13
+ describe '.transform' do
14
+ it 'should respond with a hash' do
15
+ expect(subject.transform('Zlatan in:first_name')).to be_a(Hash)
16
+ end
17
+
18
+ it 'should have a result with :values and :params keys' do
19
+ expect(subject.transform('Zlatan in:first_name'))
20
+ .to have_key(:values)
21
+ .and have_key(:params)
22
+ end
23
+
24
+ it 'should not have a result with a key :props' do
25
+ expect(subject.transform('Zlatan in:first_name'))
26
+ .not_to have_key(:props)
27
+ end
28
+
29
+ it 'should transform the string to a hash' do
30
+ expect(subject.transform('Zlatan in:first_name'))
31
+ .to eq(values: ['Zlatan'], params: { in: ['first_name'] })
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActsAsExplorable::Query do
4
+
5
+ let(:zlatan) { create(:zlatan) }
6
+ let(:manuel) { create(:manuel) }
7
+ let(:bastian) { create(:bastian) }
8
+ let(:christiano) { create(:christiano) }
9
+ let(:toni) { create(:toni) }
10
+ let(:fernando) { create(:fernando) }
11
+
12
+ context 'in:' do
13
+ it '`first_name` should find by first name' do
14
+ [zlatan, christiano]
15
+ expect(Player.search('Zlatan in:first_name').count).to eq(1)
16
+ end
17
+
18
+ it '`club` should find by club' do
19
+ [zlatan, manuel, bastian]
20
+ expect(Player.search('Bayern in:club').count).to eq(2)
21
+ end
22
+
23
+ it '`city` should find by city' do
24
+ [zlatan, bastian, christiano, toni, fernando]
25
+ expect(Player.search('Madrid in:city').count).to eq(3)
26
+ end
27
+ end
28
+
29
+ context 'sort:' do
30
+ before(:each) do
31
+ [christiano, zlatan, bastian]
32
+ end
33
+
34
+ it '`first_name` should order descending by first name' do
35
+ expect(Player.search('sort:first_name').first).to eq(zlatan)
36
+ end
37
+
38
+ it '`first_name-desc` should order descending by first name' do
39
+ expect(Player.search('sort:first_name-desc').first).to eq(zlatan)
40
+ end
41
+
42
+ it '`first_name-asc` should order ascending by first name' do
43
+ expect(Player.search('sort:first_name-asc').last).to eq(zlatan)
44
+ end
45
+
46
+ it '`position-desc,first_name-asc` should order by position then by first name' do
47
+ expect(Player.search('sort:position-desc,first_name-asc').first).to eq(bastian)
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,24 @@
1
+ begin
2
+ require 'byebug'
3
+ rescue LoadError
4
+ end
5
+ $LOAD_PATH << '.' unless $LOAD_PATH.include?('.')
6
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
7
+ require 'logger'
8
+
9
+ require File.expand_path('../../lib/acts_as_explorable', __FILE__)
10
+ I18n.enforce_available_locales = true
11
+ require 'rails'
12
+ require 'rspec/its'
13
+ require 'sqlite3'
14
+ require 'factory_girl'
15
+ require 'database_cleaner'
16
+
17
+ Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
18
+
19
+ RSpec.configure do |config|
20
+ config.include FactoryGirl::Syntax::Methods
21
+ config.raise_errors_for_deprecations!
22
+ end
23
+
24
+ FactoryGirl.find_definitions
@@ -0,0 +1,64 @@
1
+ ActiveRecord::Migration.verbose = false
2
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
3
+ ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), '../debug.log'))
4
+ ActiveRecord::Base.logger.level = ENV['TRAVIS'] ? ::Logger::ERROR : ::Logger::DEBUG
5
+
6
+ ActiveRecord::Schema.define(version: 1) do
7
+ create_table :players do |t|
8
+ t.string :first_name
9
+ t.string :last_name
10
+ t.string :position, limit: 2
11
+ t.string :city
12
+ t.string :club
13
+
14
+ t.timestamps null: false
15
+ end
16
+ end
17
+
18
+ class ActsAsExplorable::TestModelBase < ActiveRecord::Base
19
+ self.table_name = :players
20
+ end
21
+
22
+ class Player < ActsAsExplorable::TestModelBase
23
+ extend ActsAsExplorable
24
+ explorable in: [:first_name, :last_name, :position, :city, :club],
25
+ sort: [:first_name, :last_name, :position, :city, :club, :created_at],
26
+ position: %w(GK MF FW)
27
+ end
28
+
29
+ class ArgumentsPlayer < ActsAsExplorable::TestModelBase
30
+ extend ActsAsExplorable
31
+ explorable in: [:first_name, :last_name, :position, :city, :club],
32
+ sort: [:first_name, :last_name, :position, :city, :club, :created_at],
33
+ position: %w(GK MF FW)
34
+ end
35
+
36
+ class BlockPlayer < ActsAsExplorable::TestModelBase
37
+ extend ActsAsExplorable
38
+ explorable do |config|
39
+ config.filters = {
40
+ in: [:first_name, :last_name, :position, :city, :club],
41
+ sort: [:first_name, :last_name, :position, :city, :club, :created_at],
42
+ position: %w(GK MF FW)
43
+ }
44
+ end
45
+ end
46
+
47
+ class BlockPlayer < ActsAsExplorable::TestModelBase
48
+ extend ActsAsExplorable
49
+ explorable in: [:first_name, :last_name, :position, :city, :club],
50
+ sort: [:first_name, :last_name, :position, :city, :club, :created_at],
51
+ position: %w(GK MF FW)
52
+
53
+ explorable do |_config|
54
+
55
+ end
56
+ end
57
+
58
+ class Explorable < ActsAsExplorable::TestModelBase
59
+ extend ActsAsExplorable
60
+ explorable
61
+ end
62
+
63
+ class NotExplorable < ActsAsExplorable::TestModelBase
64
+ end
@@ -0,0 +1,21 @@
1
+ RSpec.configure do |config|
2
+
3
+ config.before(:suite) do
4
+ DatabaseCleaner.clean_with(:truncation)
5
+ DatabaseCleaner.strategy = :transaction
6
+ DatabaseCleaner.clean
7
+ end
8
+
9
+ config.after(:suite) do
10
+ DatabaseCleaner.clean
11
+ end
12
+
13
+ config.before(:each) do
14
+ DatabaseCleaner.start
15
+ end
16
+
17
+ config.after(:each) do
18
+ DatabaseCleaner.clean
19
+ end
20
+
21
+ end
metadata ADDED
@@ -0,0 +1,185 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_explorable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mathias Schneider
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5'
33
+ - !ruby/object:Gem::Dependency
34
+ name: sqlite3
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec-rails
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rspec-its
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rspec
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: factory_girl_rails
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '4.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '4.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: database_cleaner
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ description: Adds GitHub-like search function to your models
118
+ email:
119
+ - mathias@hiasinho.com
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".gitignore"
125
+ - ".rspec"
126
+ - ".travis.yml"
127
+ - Appraisals
128
+ - Gemfile
129
+ - Guardfile
130
+ - MIT-LICENSE
131
+ - README.md
132
+ - Rakefile
133
+ - acts_as_explorable.gemspec
134
+ - gemfiles/activerecord_4.0.gemfile
135
+ - gemfiles/activerecord_4.1.gemfile
136
+ - gemfiles/activerecord_4.2.gemfile
137
+ - lib/acts_as_explorable.rb
138
+ - lib/acts_as_explorable/configuration.rb
139
+ - lib/acts_as_explorable/element.rb
140
+ - lib/acts_as_explorable/element/base.rb
141
+ - lib/acts_as_explorable/element/dynamic_filter.rb
142
+ - lib/acts_as_explorable/element/in.rb
143
+ - lib/acts_as_explorable/element/sort.rb
144
+ - lib/acts_as_explorable/explorable.rb
145
+ - lib/acts_as_explorable/ext/string.rb
146
+ - lib/acts_as_explorable/parser.rb
147
+ - lib/acts_as_explorable/query.rb
148
+ - lib/acts_as_explorable/version.rb
149
+ - lib/tasks/acts_as_explorable_tasks.rake
150
+ - spec/factories/players.rb
151
+ - spec/lib/acts_as_explorable/element/dynamic_filter_spec.rb
152
+ - spec/lib/acts_as_explorable/element/in.rb
153
+ - spec/lib/acts_as_explorable/element_spec.rb
154
+ - spec/lib/acts_as_explorable/explorable_spec.rb
155
+ - spec/lib/acts_as_explorable/parser_spec.rb
156
+ - spec/lib/acts_as_explorable/query_spec.rb
157
+ - spec/spec_helper.rb
158
+ - spec/support/database.rb
159
+ - spec/support/database_cleaner.rb
160
+ homepage: https://github.com/hiasinho/acts_as_explorable
161
+ licenses:
162
+ - MIT
163
+ metadata: {}
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: 2.0.0
173
+ required_rubygems_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ requirements: []
179
+ rubyforge_project:
180
+ rubygems_version: 2.2.2
181
+ signing_key:
182
+ specification_version: 4
183
+ summary: Adds GitHub-like search function to your models
184
+ test_files: []
185
+ has_rdoc: