acts_as_explorable 0.1.0

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.
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: