├── .codeclimate.yml ├── lib ├── ruby_simple_search │ ├── version.rb │ ├── like_patterns.rb │ └── errors.rb └── ruby_simple_search.rb ├── test ├── gemfiles │ ├── activerecord51.gemfile │ ├── activerecord50.gemfile │ └── activerecord52.gemfile ├── sqlite_test.rb ├── postgresql_test.rb ├── mysql_test.rb └── test_helper.rb ├── Rakefile ├── .gitignore ├── Gemfile ├── .travis.yml ├── LICENSE.txt ├── ruby_simple_search.gemspec ├── CHANGELOG.md ├── README.md └── .rubocop.yml /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | rubocop: 3 | enabled: true 4 | channel: rubocop-0-76 5 | -------------------------------------------------------------------------------- /lib/ruby_simple_search/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubySimpleSearch 4 | VERSION = "2.0.1" 5 | end 6 | -------------------------------------------------------------------------------- /test/gemfiles/activerecord51.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in groupdate.gemspec 4 | gemspec path: '../../' 5 | 6 | gem 'activerecord', '~> 5.1.0' 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | task :test do 5 | sh "ruby test/sqlite_test.rb" 6 | sh "ruby test/mysql_test.rb" 7 | sh "ruby test/postgresql_test.rb" 8 | end 9 | -------------------------------------------------------------------------------- /lib/ruby_simple_search/like_patterns.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubySimpleSearch 4 | LIKE_PATTERNS = { 5 | plain: "q", 6 | beginning: "q%", 7 | ending: "%q", 8 | containing: "%q%" 9 | } 10 | end 11 | -------------------------------------------------------------------------------- /test/gemfiles/activerecord50.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in groupdate.gemspec 4 | gemspec path: '../../' 5 | 6 | gem 'activerecord', '~> 5.0.0' 7 | gem "sqlite3", "~> 1.3.0" 8 | gem "pg", "< 1" 9 | -------------------------------------------------------------------------------- /test/gemfiles/activerecord52.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in searchkick.gemspec 4 | gemspec path: '../../' 5 | 6 | gem 'activerecord', '~> 5.2.0' 7 | gem "sqlite3", "~> 1.3.0" 8 | gem "pg", "< 1" 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | .ruby-version 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | tags 19 | Gemfile.lock 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in ruby_simple_search.gemspec 6 | gemspec 7 | 8 | gem "activerecord", "~> 6.0.0" 9 | 10 | group :development do 11 | gem "rubocop", ">= 0.47" 12 | gem "rubocop-performance" 13 | gem "rubocop-rails" 14 | end 15 | -------------------------------------------------------------------------------- /test/sqlite_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestSqlite < Minitest::Test 6 | include GemSetupTest 7 | include ExcpetionsTest 8 | include SearchTest 9 | include JoinTest 10 | 11 | def setup 12 | super 13 | @@setup ||= begin 14 | ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:" 15 | create_tables 16 | create_dummy_data 17 | true 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/postgresql_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class PostgresqlTest < Minitest::Test 6 | include GemSetupTest 7 | include ExcpetionsTest 8 | include SearchTest 9 | include JoinTest 10 | 11 | def setup 12 | super 13 | @@setup ||= begin 14 | ActiveRecord::Base.establish_connection adapter: "postgresql", database: "ruby_simple_search_test" 15 | create_tables 16 | create_dummy_data 17 | true 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/ruby_simple_search/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubySimpleSearch 4 | module Errors 5 | ATTRIBUTES_MISSING = "Simple search attributes are missing" 6 | INVALID_CONDITION = "Extended query's array conditions are wrong" 7 | INVALID_TYPE = "Extended query is not an array type" 8 | INVALID_PATTERN = "Looks like given pattern is wrong, valid pattern list is '#{LIKE_PATTERNS.keys}'" 9 | SEARCH_ARG_TYPE = "`search_term` argument is not a string" 10 | WRONG_ATTRIBUTES = "`simple_search_arguments` method's arguments should be in symbol format" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/mysql_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | require "active_record/connection_adapters/mysql2_adapter" 5 | 6 | class MysqlTest < Minitest::Test 7 | include GemSetupTest 8 | include ExcpetionsTest 9 | include SearchTest 10 | include JoinTest 11 | 12 | def setup 13 | super 14 | @@setup ||= begin 15 | ActiveRecord::Base.establish_connection adapter: "mysql2", database: "ruby_simple_search_test", username: "root" 16 | create_tables 17 | create_dummy_data 18 | true 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: true 3 | env: 4 | global: 5 | - CC_TEST_REPORTER_ID=feb97978987d37794883977131b61c2b0a561bb35f4fa62ea30c3c5ce4c0794f 6 | language: ruby 7 | rvm: 8 | - 2.5.3 9 | gemfile: 10 | - Gemfile 11 | - test/gemfiles/activerecord52.gemfile 12 | - test/gemfiles/activerecord51.gemfile 13 | - test/gemfiles/activerecord50.gemfile 14 | before_script: 15 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 16 | - chmod +x ./cc-test-reporter 17 | - ./cc-test-reporter before-build 18 | script: bundle exec rake test 19 | after_script: 20 | - ./cc-test-reporter after-build --debug --exit-code $TRAVIS_TEST_RESULT 21 | services: 22 | - postgresql 23 | - mysql 24 | addons: 25 | postgresql: 10 26 | before_install: 27 | - mysqladmin create ruby_simple_search_test 28 | - mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql 29 | - createdb ruby_simple_search_test 30 | notifications: 31 | email: 32 | on_success: never 33 | on_failure: change 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2018 Santosh Wadghule 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /ruby_simple_search.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "ruby_simple_search/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "ruby_simple_search" 9 | spec.version = RubySimpleSearch::VERSION 10 | spec.summary = "The simplest way to search the data" 11 | spec.homepage = "https://github.com/mechanicles/ruby_simple_search" 12 | spec.license = "MIT" 13 | 14 | spec.authors = "Santosh Wadghule" 15 | spec.email = "santosh.wadghule@gmail.com" 16 | 17 | spec.files = Dir["*.{md,txt}", "{lib}/**/*"] 18 | spec.require_paths = "lib" 19 | 20 | spec.required_ruby_version = ">= 2.2" 21 | 22 | spec.add_dependency "activerecord", ">= 5" 23 | 24 | spec.add_development_dependency "bundler" 25 | spec.add_development_dependency "rake" 26 | spec.add_development_dependency "minitest" 27 | spec.add_development_dependency "simplecov" 28 | spec.add_development_dependency "activerecord" 29 | 30 | spec.add_development_dependency "pg", "< 1" 31 | spec.add_development_dependency "mysql2", "< 0.5" 32 | spec.add_development_dependency "sqlite3" 33 | end 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## 2.0.1 9 | - Supported Rails 6 10 | - Dropped support Rails < 5 11 | 12 | ## 2.0.0 13 | ### Added 14 | - Supports `attributes` parameter to `simple_search` method. [PR#4](https://github.com/mechanicles/ruby_simple_search/pull/4) 15 | - Supports data types other than `string` and `text` to `simple_search_attributes` 16 | - Now tests cover **SQLite**, **MySQL**, and **PostgreSQL** databases 17 | 18 | ### Changed 19 | - Used Minitest over RSpec 20 | - Refactored the code 21 | - Used Travis CI over CircleCi 22 | 23 | ### Removed 24 | - Removed `plain` pattern from `LIKE` query 25 | 26 | ## 0.0.3 27 | ### Fixed 28 | - Fixed problem when using simple search with joins. [GI#1](https://github.com/mechanicles/ruby_simple_search/issues/1) 29 | 30 | ### Changed 31 | - Moved pattern option to `simple_search` method and removed it from `simple_search_attributes` method 32 | - Updated specs accordingly 33 | 34 | ## 0.0.2 35 | ### Added 36 | - Added support for `LIKE` patterns e.g. 'beginning', 'ending', 'containing', 'underscore', and 'plain' 37 | - Added block support to `simple_search` method so user can extend it based on its need 38 | - Added specs 39 | - Added some exceptions handling 40 | 41 | ## 0.0.1 42 | ### Added 43 | - First major release 44 | -------------------------------------------------------------------------------- /lib/ruby_simple_search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ruby_simple_search/version" 4 | require "ruby_simple_search/like_patterns" 5 | require "ruby_simple_search/errors" 6 | require "active_support/concern" 7 | 8 | module RubySimpleSearch 9 | extend ActiveSupport::Concern 10 | 11 | included do 12 | instance_eval do 13 | private 14 | def simple_search_attributes(*args) 15 | @simple_search_attributes = [] 16 | args.each do |arg| 17 | raise ArgumentError, Errors::WRONG_ATTRIBUTES unless arg.is_a? Symbol 18 | 19 | @simple_search_attributes << arg 20 | end 21 | end 22 | end 23 | end 24 | 25 | module ClassMethods 26 | def simple_search(search_term, options = {}, &block) 27 | raise Errors::ATTRIBUTES_MISSING if @simple_search_attributes.blank? 28 | raise ArgumentError, Errors::SEARCH_ARG_TYPE unless search_term.is_a? String 29 | 30 | @simple_search_term = search_term 31 | @simple_search_pattern = get_pattern(options[:pattern]) 32 | @simple_search_patterned_text = @simple_search_pattern.gsub("q", @simple_search_term.try(:downcase)) 33 | @simple_search_query_conditions = [] 34 | @simple_search_query_values = [] 35 | 36 | build_query_conditions_and_values(options) 37 | extend_query(block) if block.is_a? Proc 38 | 39 | sql_query = [@simple_search_query_conditions.join, @simple_search_query_values] 40 | where(sql_query.flatten) 41 | end 42 | 43 | private 44 | def get_pattern(pattern) 45 | if pattern.nil? 46 | # default pattern is '%q%' 47 | LIKE_PATTERNS[:containing] 48 | else 49 | pattern = LIKE_PATTERNS[pattern.to_sym] 50 | raise Errors::INVALID_PATTERN if pattern.nil? 51 | 52 | pattern 53 | end 54 | end 55 | 56 | def build_query_conditions_and_values(options) 57 | attributes = if options[:attributes].nil? 58 | @simple_search_attributes 59 | else 60 | _attr = *options[:attributes] 61 | end 62 | 63 | attributes.each do |attribute| 64 | condition, value = build_query_condition_and_value(attribute) 65 | 66 | @simple_search_query_conditions << condition 67 | @simple_search_query_values << value 68 | end 69 | end 70 | 71 | def build_query_condition_and_value(attribute) 72 | condition = if %i[string text].include?(columns_hash[attribute.to_s].type) 73 | build_query_for_string_and_text_types(attribute) 74 | else 75 | build_query_non_string_and_text_types(attribute) 76 | end 77 | 78 | [condition, @simple_search_patterned_text] 79 | end 80 | 81 | def build_query_for_string_and_text_types(attribute) 82 | if @simple_search_query_conditions.blank? 83 | "LOWER(#{table_name}.#{attribute}) LIKE ?" 84 | else 85 | " OR LOWER(#{table_name}.#{attribute}) LIKE ?" 86 | end 87 | end 88 | 89 | def build_query_non_string_and_text_types(attribute) 90 | if @simple_search_query_conditions.blank? 91 | "CAST(#{table_name}.#{attribute} AS CHAR(255)) LIKE ?" 92 | else 93 | " OR CAST(#{table_name}.#{attribute} AS CHAR(255)) LIKE ?" 94 | end 95 | end 96 | 97 | def extend_query(block) 98 | @simple_search_query_conditions = ["(#{@simple_search_query_conditions.join})"] 99 | extended_query = block.call @simple_search_term 100 | extend_simple_search(extended_query) if extended_query 101 | end 102 | 103 | def extend_simple_search(extended_query) 104 | raise Errors::INVALID_TYPE unless extended_query.is_a?(Array) 105 | 106 | extended_query_condition = extended_query[0] 107 | extended_query_values = extended_query - [extended_query[0]] 108 | 109 | if extended_query_condition.count("?") != extended_query_values.size 110 | raise Errors::INVALID_CONDITION 111 | end 112 | 113 | @simple_search_query_conditions << " #{extended_query_condition}" 114 | @simple_search_query_values += extended_query_values 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RubySimpleSearch 2 | 3 | 4 | [![Build Status](https://travis-ci.org/mechanicles/ruby_simple_search.svg?branch=master)](https://travis-ci.org/mechanicles/ruby_simple_search) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/20e84a4c3be302b07653/maintainability)](https://codeclimate.com/github/mechanicles/ruby_simple_search/maintainability) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/20e84a4c3be302b07653/test_coverage)](https://codeclimate.com/github/mechanicles/ruby_simple_search/test_coverage) 7 | 8 | The simplest way to search the data in ActiveRecord models. 9 | 10 | It offers simple but useful features: 11 | 12 | - [Search on the default attributes](#search-on-the-default-attributes) 13 | - [Override default search attributes to specific attributes ](#override-default-search-attributes-to-specific-attributes) (Credit goes to [@abdullahtariq1171](https://github.com/abdullahtariq1171)) 14 | - [Search using patterns](#search-using-patterns) 15 | - [Ruby block support to extend the search query](#ruby-block-support-to-extend-the-search-query) 16 | - [Simple search returns an `ActiveRecord::Relation` object](#simple-search-returns-an-activerecordrelation-object) 17 | 18 | Mostly on the admin side, we do have a standard text field to search the data on the table. 19 | Sometimes we want to search through the attributes like title, content and ratings on the 20 | post model or email, username and description on the user model. For those searches, we use 21 | MySQL's or PostgreSQL's `LIKE` operator to get the results. While doing the same thing again 22 | and again on the different models, you add lots of duplication in your code. 23 | 24 | #### Do not repeat yourself, use RubySimpleSearch. 25 | 26 | ## Installation 27 | 28 | Add this line to your application's Gemfile: 29 | 30 | gem 'ruby_simple_search' 31 | 32 | And then execute: 33 | 34 | $ bundle install 35 | 36 | Or install it yourself as: 37 | 38 | $ gem install ruby_simple_search 39 | 40 | ## Usage 41 | 42 | Define attributes that you want to search on it 43 | 44 | ```Ruby 45 | class Post < ActiveActiveRecord::Base 46 | include RubySimpleSearch 47 | 48 | simple_search_attributes :title, :description 49 | end 50 | ``` 51 | 52 | ```Ruby 53 | class User < ActiveActiveRecord::Base 54 | include RubySimpleSearch 55 | 56 | simple_search_attributes :email, :username, :address, :age 57 | end 58 | ``` 59 | 60 | ## Features 61 | 62 | ### Search on the default attributes 63 | If you don't provide any attribute at the time of searching, it will use `simple_search_attributes` from the model. 64 | 65 | ```ruby 66 | class User < ActiveActiveRecord::Base 67 | include RubySimpleSearch 68 | 69 | simple_search_attributes :email, :username, :address 70 | end 71 | 72 | 73 | Post.simple_search('york') 74 | # It will search in :email, :username and :address only 75 | ``` 76 | 77 | ### Override default search attributes to specific attributes 78 | 79 | If you want to perform a specific search on particular attributes, you can pass specific attributes with `attributes` option. 80 | 81 | ```ruby 82 | class User < ActiveActiveRecord::Base 83 | include RubySimpleSearch 84 | 85 | simple_search_attributes :email, :username, :address 86 | end 87 | 88 | Post.simple_search('york') 89 | # It will search in :email, :username and :address only 90 | 91 | Post.simple_search('york', attributes: :address) 92 | # It will search in :address only 93 | 94 | User.simple_search('york', pattern: :ending, attributes: [:email, :address]) 95 | # It will search in :email and :address only with 'ending' pattern 96 | ``` 97 | 98 | ### Search using patterns 99 | You can pass a `LIKE` pattern to the `simple_search` method. 100 | 101 | Patterns: 102 | 103 | - beginning 104 | - ending 105 | - containing (Default pattern) 106 | - plain 107 | 108 | ```ruby 109 | Post.simple_search('york', pattern: :beginning) 110 | # It will search like 'york%' and finds any values that start with "york" 111 | 112 | Post.simple_search('york', pattern: :ending) 113 | # It will search like '%york' and finds any values that end with "york" 114 | 115 | Post.simple_search('york', pattern: :containing) 116 | # It will search like '%york%' and finds any values that have "york" in any position 117 | 118 | Post.simple_search('york', pattern: :plain) 119 | # It will search like 'york' and finds any values that have "york" word 120 | ``` 121 | 122 | ### Ruby block support to extend the search query 123 | 124 | ```Ruby 125 | User.simple_search('35') do |search_term| 126 | ["AND age = ?", search_term] 127 | end 128 | ``` 129 | Block should return an array of search condition and values. 130 | 131 | ### Simple search returns an `ActiveRecord::Relation` object 132 | 133 | ```Ruby 134 | Model.simple_search('string') # => ActiveRecord::Relation object 135 | 136 | Model.simple_search('string').to_sql 137 | 138 | # OR 139 | 140 | User.simple_search('mechanicles') do |search_term| 141 | ["AND address != ?", search_term] 142 | end.to_sql 143 | 144 | # => It will return an SQL query in string format 145 | ``` 146 | 147 | ## Contributing 148 | 149 | 1. Fork it 150 | 2. Create your feature branch (`git checkout -b my-new-feature`) 151 | 3. Commit your changes (`git commit -am 'Add some feature'`) 152 | 4. Push to the branch (`git push origin my-new-feature`) 153 | 5. Create new Pull Request 154 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Copied from https://raw.githubusercontent.com/rails/rails/master/.rubocop.yml 2 | 3 | require: 4 | - rubocop-performance 5 | - rubocop-rails 6 | 7 | AllCops: 8 | TargetRubyVersion: 2.5 9 | # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop 10 | # to ignore them, so only the ones explicitly set in this file are enabled. 11 | DisabledByDefault: true 12 | Exclude: 13 | - '**/tmp/**/*' 14 | - '**/templates/**/*' 15 | - '**/vendor/**/*' 16 | - 'actionpack/lib/action_dispatch/journey/parser.rb' 17 | - 'railties/test/fixtures/tmp/**/*' 18 | - 'actionmailbox/test/dummy/**/*' 19 | - 'actiontext/test/dummy/**/*' 20 | - '**/node_modules/**/*' 21 | 22 | Performance: 23 | Exclude: 24 | - '**/test/**/*' 25 | 26 | # Prefer assert_not over assert ! 27 | Rails/AssertNot: 28 | Include: 29 | - '**/test/**/*' 30 | 31 | # Prefer assert_not_x over refute_x 32 | Rails/RefuteMethods: 33 | Include: 34 | - '**/test/**/*' 35 | 36 | # Prefer &&/|| over and/or. 37 | Style/AndOr: 38 | Enabled: true 39 | 40 | # Align `when` with `case`. 41 | Layout/CaseIndentation: 42 | Enabled: true 43 | 44 | # Align comments with method definitions. 45 | Layout/CommentIndentation: 46 | Enabled: true 47 | 48 | Layout/ElseAlignment: 49 | Enabled: true 50 | 51 | # Align `end` with the matching keyword or starting expression except for 52 | # assignments, where it should be aligned with the LHS. 53 | Layout/EndAlignment: 54 | Enabled: true 55 | EnforcedStyleAlignWith: variable 56 | AutoCorrect: true 57 | 58 | Layout/EmptyLineAfterMagicComment: 59 | Enabled: true 60 | 61 | Layout/EmptyLinesAroundAccessModifier: 62 | Enabled: true 63 | EnforcedStyle: only_before 64 | 65 | Layout/EmptyLinesAroundBlockBody: 66 | Enabled: true 67 | 68 | # In a regular class definition, no empty lines around the body. 69 | Layout/EmptyLinesAroundClassBody: 70 | Enabled: true 71 | 72 | # In a regular method definition, no empty lines around the body. 73 | Layout/EmptyLinesAroundMethodBody: 74 | Enabled: true 75 | 76 | # In a regular module definition, no empty lines around the body. 77 | Layout/EmptyLinesAroundModuleBody: 78 | Enabled: true 79 | 80 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. 81 | Style/HashSyntax: 82 | Enabled: true 83 | 84 | Layout/FirstArgumentIndentation: 85 | Enabled: true 86 | 87 | # Method definitions after `private` or `protected` isolated calls need one 88 | # extra level of indentation. 89 | Layout/IndentationConsistency: 90 | Enabled: true 91 | EnforcedStyle: indented_internal_methods 92 | 93 | # Two spaces, no tabs (for indentation). 94 | Layout/IndentationWidth: 95 | Enabled: true 96 | 97 | Layout/LeadingCommentSpace: 98 | Enabled: true 99 | 100 | Layout/SpaceAfterColon: 101 | Enabled: true 102 | 103 | Layout/SpaceAfterComma: 104 | Enabled: true 105 | 106 | Layout/SpaceAfterSemicolon: 107 | Enabled: true 108 | 109 | Layout/SpaceAroundEqualsInParameterDefault: 110 | Enabled: true 111 | 112 | Layout/SpaceAroundKeyword: 113 | Enabled: true 114 | 115 | Layout/SpaceBeforeComma: 116 | Enabled: true 117 | 118 | Layout/SpaceBeforeComment: 119 | Enabled: true 120 | 121 | Layout/SpaceBeforeFirstArg: 122 | Enabled: true 123 | 124 | Style/DefWithParentheses: 125 | Enabled: true 126 | 127 | # Defining a method with parameters needs parentheses. 128 | Style/MethodDefParentheses: 129 | Enabled: true 130 | 131 | Style/FrozenStringLiteralComment: 132 | Enabled: true 133 | EnforcedStyle: always 134 | Exclude: 135 | - 'actionview/test/**/*.builder' 136 | - 'actionview/test/**/*.ruby' 137 | - 'actionpack/test/**/*.builder' 138 | - 'actionpack/test/**/*.ruby' 139 | - 'activestorage/db/migrate/**/*.rb' 140 | - 'activestorage/db/update_migrate/**/*.rb' 141 | - 'actionmailbox/db/migrate/**/*.rb' 142 | - 'actiontext/db/migrate/**/*.rb' 143 | 144 | Style/RedundantFreeze: 145 | Enabled: true 146 | 147 | # Use `foo {}` not `foo{}`. 148 | Layout/SpaceBeforeBlockBraces: 149 | Enabled: true 150 | 151 | # Use `foo { bar }` not `foo {bar}`. 152 | Layout/SpaceInsideBlockBraces: 153 | Enabled: true 154 | EnforcedStyleForEmptyBraces: space 155 | 156 | # Use `{ a: 1 }` not `{a:1}`. 157 | Layout/SpaceInsideHashLiteralBraces: 158 | Enabled: true 159 | 160 | Layout/SpaceInsideParens: 161 | Enabled: true 162 | 163 | # Check quotes usage according to lint rule below. 164 | Style/StringLiterals: 165 | Enabled: true 166 | EnforcedStyle: double_quotes 167 | 168 | # Detect hard tabs, no hard tabs. 169 | Layout/Tab: 170 | Enabled: true 171 | 172 | # Blank lines should not have any spaces. 173 | Layout/TrailingEmptyLines: 174 | Enabled: true 175 | 176 | # No trailing whitespace. 177 | Layout/TrailingWhitespace: 178 | Enabled: true 179 | 180 | # Use quotes for string literals when they are enough. 181 | Style/RedundantPercentQ: 182 | Enabled: true 183 | 184 | Lint/AmbiguousOperator: 185 | Enabled: true 186 | 187 | Lint/AmbiguousRegexpLiteral: 188 | Enabled: true 189 | 190 | Lint/ErbNewArguments: 191 | Enabled: true 192 | 193 | # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 194 | Lint/RequireParentheses: 195 | Enabled: true 196 | 197 | Lint/ShadowingOuterLocalVariable: 198 | Enabled: true 199 | 200 | Lint/RedundantStringCoercion: 201 | Enabled: true 202 | 203 | Lint/UriEscapeUnescape: 204 | Enabled: true 205 | 206 | Lint/UselessAssignment: 207 | Enabled: true 208 | 209 | Lint/DeprecatedClassMethods: 210 | Enabled: true 211 | 212 | Style/ParenthesesAroundCondition: 213 | Enabled: true 214 | 215 | Style/RedundantBegin: 216 | Enabled: true 217 | 218 | Style/RedundantReturn: 219 | Enabled: true 220 | AllowMultipleReturnValues: true 221 | 222 | Style/Semicolon: 223 | Enabled: true 224 | AllowAsExpressionSeparator: true 225 | 226 | # Prefer Foo.method over Foo::method 227 | Style/ColonMethodCall: 228 | Enabled: true 229 | 230 | Style/TrivialAccessors: 231 | Enabled: true 232 | 233 | Performance/FlatMap: 234 | Enabled: true 235 | 236 | Performance/RedundantMerge: 237 | Enabled: true 238 | 239 | Performance/StartWith: 240 | Enabled: true 241 | 242 | Performance/EndWith: 243 | Enabled: true 244 | 245 | Performance/RegexpMatch: 246 | Enabled: true 247 | 248 | Performance/ReverseEach: 249 | Enabled: true 250 | 251 | Performance/UnfreezeString: 252 | Enabled: true 253 | 254 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "simplecov" 4 | SimpleCov.command_name "Unit Tests" 5 | SimpleCov.start 6 | require "bundler/setup" 7 | Bundler.require(:default) 8 | require "minitest/autorun" 9 | require "minitest/pride" 10 | require "logger" 11 | require "active_record" 12 | 13 | Minitest::Test = Minitest::Unit::TestCase unless defined?(Minitest::Test) 14 | 15 | # for debugging 16 | ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"] 17 | 18 | class User < ActiveRecord::Base 19 | include RubySimpleSearch 20 | has_many :posts 21 | 22 | simple_search_attributes :name, :email, :contact, :address 23 | end 24 | 25 | class Post < ActiveRecord::Base 26 | belongs_to :user 27 | end 28 | 29 | class User2 < ActiveRecord::Base 30 | include RubySimpleSearch 31 | end 32 | 33 | def create_tables 34 | ActiveRecord::Migration.verbose = false 35 | 36 | ActiveRecord::Migration.create_table :users, force: true do |t| 37 | t.string :name, null: false 38 | t.text :address 39 | t.text :contact 40 | t.string :email 41 | t.integer :age 42 | t.string :username 43 | t.timestamps 44 | end 45 | 46 | ActiveRecord::Migration.create_table :posts, force: true do |t| 47 | t.string :name, null: false 48 | t.references :user 49 | t.timestamps 50 | end 51 | end 52 | 53 | def create_dummy_data 54 | alice = User.create! email: "alice@example.com", 55 | name: "alice", 56 | address: "usa", 57 | contact: "12345", 58 | age: 60, 59 | username: "alicestar" 60 | 61 | User.create! email: "bob@example.com", 62 | name: "bob", 63 | address: "india", 64 | contact: "56789", 65 | age: 26, 66 | username: "rockstar" 67 | 68 | User.create! email: "bob@something.com", 69 | name: "bob", 70 | address: "uk", 71 | contact: "45786", 72 | age: 21, 73 | username: "kingkhan" 74 | 75 | Post.create! name: "Ruby is simple", user: alice 76 | end 77 | 78 | module GemSetupTest 79 | def test_no_exception_is_raised_if_simple_search_attributes_is_called_in_the_model 80 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 81 | assert_silent { User.simple_search("USA") } 82 | end 83 | 84 | def test_it_sets_attributes_properly 85 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 86 | 87 | assert_equal %i[name email contact address], User.instance_variable_get("@simple_search_attributes") 88 | end 89 | 90 | def test_it_has_default_like_pattern 91 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 92 | User.simple_search("Alice") 93 | 94 | assert_equal "%q%", User.instance_variable_get("@simple_search_pattern") 95 | end 96 | 97 | def test_it_can_have_patterns_like_plain_beginning_ending_and_containing 98 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 99 | 100 | User.simple_search("alice", pattern: :plain) 101 | assert_equal "q", User.instance_variable_get("@simple_search_pattern") 102 | 103 | User.simple_search("al", pattern: :beginning) 104 | assert_equal "q%", User.instance_variable_get("@simple_search_pattern") 105 | User.simple_search("alice", pattern: :ending) 106 | assert_equal "%q", User.instance_variable_get("@simple_search_pattern") 107 | 108 | User.simple_search("alice", pattern: :containing) 109 | assert_equal "%q%", User.instance_variable_get("@simple_search_pattern") 110 | end 111 | end 112 | 113 | module SearchTest 114 | def test_it_searches_the_users_whose_names_are_alice 115 | User.send(:simple_search_attributes, :name, :email, :contact, :address, :age) 116 | 117 | user = User.find_by(name: "alice") 118 | users = User.simple_search("alice") 119 | 120 | assert_includes users, user 121 | end 122 | 123 | def test_it_raises_an_exception_if_pattern_is_wrong 124 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 125 | 126 | error = assert_raises(RuntimeError) do 127 | User.simple_search("alice", pattern: "wrong") 128 | end 129 | 130 | assert_equal RubySimpleSearch::Errors::INVALID_PATTERN, error.message 131 | end 132 | 133 | def test_it_searches_the_users_whose_names_are_alice_with_beginning_pattern 134 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 135 | 136 | user = User.find_by(name: "alice") 137 | users = User.simple_search("al", pattern: :beginning) 138 | 139 | assert_includes users, user 140 | end 141 | 142 | def test_it_returns_empty_users_if_pattern_is_beginning_but_query_has_non_beginning_characters 143 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 144 | 145 | users = User.simple_search("ce", pattern: :beginning) 146 | 147 | assert_empty users 148 | end 149 | 150 | def test_it_returns_empty_records_if_contact_number_does_not_exist 151 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 152 | 153 | users = User.simple_search("343434") 154 | 155 | assert_empty users 156 | end 157 | 158 | def test_it_searches_user_records_if_users_belong_to_usa 159 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 160 | 161 | users = User.where(address: "usa") 162 | searched_users = User.simple_search("USA") 163 | 164 | assert_equal users.pluck(:id), searched_users.pluck(:id) 165 | end 166 | 167 | def test_it_searches_the_records_with_beginning_pattern 168 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 169 | 170 | users = User.where("name like ?", "bo%") 171 | searched_users = User.simple_search("bo", pattern: :beginning) 172 | 173 | assert_equal searched_users.count, users.count 174 | end 175 | 176 | def test_searches_the_records_with_ending_pattern 177 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 178 | 179 | users = User.where("name like ?", "%ce") 180 | searched_users = User.simple_search("ce", pattern: :ending) 181 | 182 | assert_equal searched_users.count, users.count 183 | end 184 | 185 | def test_searches_the_records_with_plain_pattern 186 | User.send(:simple_search_attributes, :name, :email, :contact, :address) 187 | 188 | users = User.where("name like ?", "bob") 189 | searched_users = User.simple_search("bob", pattern: :plain) 190 | 191 | assert_equal searched_users.count, users.count 192 | end 193 | 194 | def test_returns_users_who_live_in_the_usa_and_their_age_is_greater_than_50 195 | User.send(:simple_search_attributes, :name, :contact, :address) 196 | 197 | searched_users = User.simple_search("usa", pattern: :plain) do 198 | ["AND age > ?", 50] 199 | end 200 | 201 | assert_equal searched_users.pluck(:address), ["usa"] 202 | assert_equal searched_users.pluck(:age), [60] 203 | end 204 | 205 | def test_returns_an_exception_if_array_condition_is_wrong_in_simple_search_block 206 | User.send(:simple_search_attributes, :name, :contact, :address) 207 | 208 | error = assert_raises(RuntimeError) do 209 | User.simple_search("usa") do 210 | ["AND age > ?", 50, 60] 211 | end 212 | end 213 | 214 | assert_equal RubySimpleSearch::Errors::INVALID_CONDITION, error.message 215 | end 216 | 217 | def test_returns_an_exception_if_condition_is_not_an_array_type 218 | User.send(:simple_search_attributes, :name, :contact, :address) 219 | 220 | error = assert_raises(RuntimeError) do 221 | User.simple_search("usa") do 222 | "Wrong return" 223 | end 224 | end 225 | 226 | assert_equal RubySimpleSearch::Errors::INVALID_TYPE, error.message 227 | end 228 | 229 | def test_searches_the_users_with_age_is_26 230 | User.send(:simple_search_attributes, :name, :contact, :address) 231 | 232 | searched_users = User.simple_search("26", pattern: :containing, attributes: :age) 233 | 234 | assert_equal [26], searched_users.pluck(:age) 235 | end 236 | 237 | def test_it_searches_the_users_with_username_and_it_ends_with_khan_word 238 | User.send(:simple_search_attributes, :name, :contact, :address) 239 | 240 | searched_users = User.simple_search("khan", pattern: :ending, attributes: [:username]) 241 | 242 | assert_equal ["kingkhan"], searched_users.pluck(:username) 243 | end 244 | 245 | def test_it_searches_the_users_with_email_containing_example_word 246 | User.send(:simple_search_attributes, :name, :contact, :address) 247 | 248 | searched_users = User.simple_search("example", pattern: :containing, attributes: [:email]) 249 | 250 | assert_equal ["alice@example.com", "bob@example.com"], searched_users.pluck(:email) 251 | end 252 | 253 | def test_it_searches_user_with_name_or_address_containing_a_word 254 | User.send(:simple_search_attributes, :name, :contact, :address) 255 | 256 | searched_users = User.simple_search("a", pattern: :containing, attributes: [:name, :address]) 257 | 258 | assert_equal ["alice", "bob"], searched_users.pluck(:name) 259 | assert_equal ["usa", "india"], searched_users.pluck(:address) 260 | end 261 | 262 | def test_it_searches_the_records_from_non_string_and_text_types_data 263 | User.send(:simple_search_attributes, :age) 264 | 265 | 266 | user = User.find_by(age: "60") 267 | searched_users = User.simple_search("60") 268 | 269 | assert_includes searched_users, user 270 | 271 | User.send(:simple_search_attributes, :age, :created_at) 272 | searched_users = User.simple_search(Date.today.year.to_s) 273 | 274 | assert_equal searched_users.count, User.count 275 | end 276 | end 277 | 278 | module JoinTest 279 | def test_simple_search_using_join 280 | User.send(:simple_search_attributes, :name, :contact, :address) 281 | 282 | searched_users = User.joins(:posts).simple_search("alice") do |_| 283 | ["AND posts.name = ? ", "Ruby is simple" ] 284 | end 285 | 286 | assert_equal ["alice"], searched_users.pluck(:name) 287 | end 288 | end 289 | 290 | module ExcpetionsTest 291 | def test_returns_an_exception_if_simple_search_attributes_method_is_not_called_while_loading_the_model 292 | User2.send(:simple_search_attributes) 293 | 294 | error = assert_raises(RuntimeError) { User2.simple_search("usa") } 295 | 296 | assert_equal RubySimpleSearch::Errors::ATTRIBUTES_MISSING, error.message 297 | end 298 | 299 | def test_returns_an_exception_if_search_term_argument_is_not_string_type 300 | User2.send(:simple_search_attributes, :name, :contact) 301 | 302 | error = assert_raises(ArgumentError) { User2.simple_search(1) } 303 | 304 | assert_equal RubySimpleSearch::Errors::SEARCH_ARG_TYPE, error.message 305 | end 306 | 307 | def test_returns_an_exception_if_simple_search_attributes_method_has_wrong_attribute_type 308 | error = assert_raises(ArgumentError) { User2.send(:simple_search_attributes, :name, "24") } 309 | 310 | assert_equal RubySimpleSearch::Errors::WRONG_ATTRIBUTES, error.message 311 | end 312 | 313 | def test_it_sets_attributes_internally_if_simple_search_attributes_method_is_called_on_the_model 314 | User2.send(:simple_search_attributes, :name, :contact) 315 | 316 | assert_equal [:name, :contact], User2.instance_variable_get("@simple_search_attributes") 317 | end 318 | end 319 | --------------------------------------------------------------------------------