pursuit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4e63f3c017580251d11f1db0994e4e771f9689e63ed2ae8f4c1c98e14ba27e0b
4
+ data.tar.gz: 7956f93f4eea40de80878022796d823b57cbe95da8472a1784ab864fb6b17165
5
+ SHA512:
6
+ metadata.gz: 7cfd6c4114d8e02181a8e74a8865f862cfbc904b56a64819b6993a788bf83f847b0859037baf7f43fa3425acb5b33e937875dfffbb0e57abe7dcb094c78e8dde
7
+ data.tar.gz: ca46b2fd3b10167df8b74419bd1e41ee200a943b3d73ce5842b52429f89b69fd111ab884b6941d5a84a402ecebab5831941838b10eee0454cb8df8d27c4caca0
data/.gitignore ADDED
@@ -0,0 +1,26 @@
1
+ # OS Files
2
+ .DS_Store
3
+ .Trashes
4
+ ehthumbs.db
5
+ Thumbs.db
6
+
7
+ # Bundler
8
+ /.bundle
9
+ /vendor/bundle
10
+
11
+ # RSpec
12
+ /spec/examples.txt
13
+
14
+ # SQLite Database
15
+ /spec/internal/db/*.sqlite3
16
+ /spec/internal/db/*.sqlite3-journal
17
+
18
+ # Log & Temporary Files
19
+ /spec/internal/log/*
20
+ /spec/internal/tmp/*
21
+ !/spec/internal/log/.keep
22
+ !/spec/internal/tmp/.keep
23
+
24
+ # Environment Files
25
+ .env
26
+ .env.*
data/.rbenv-gemsets ADDED
@@ -0,0 +1 @@
1
+ pursuit
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'bin/**/*'
4
+
5
+ Metrics/AbcSize:
6
+ Max: 30
7
+
8
+ Metrics/LineLength:
9
+ Max: 120
10
+
11
+ Metrics/BlockLength:
12
+ Enabled: false
13
+
14
+ Metrics/ClassLength:
15
+ Enabled: false
16
+
17
+ Metrics/MethodLength:
18
+ Enabled: false
19
+
20
+ Style/Documentation:
21
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.2
data/.travis.yml ADDED
@@ -0,0 +1,24 @@
1
+ language: ruby
2
+ rvm:
3
+ - 3.0.2
4
+ gemfile:
5
+ - travis/gemfiles/5.2.gemfile
6
+ - travis/gemfiles/6.0.gemfile
7
+ - travis/gemfiles/6.1.gemfile
8
+ services:
9
+ - postgresql
10
+ before_install:
11
+ - gem update --system
12
+ - gem install bundler
13
+ before_script:
14
+ - psql -c 'CREATE DATABASE pursuit_test;' -U postgres
15
+ addons:
16
+ postgresql: 14
17
+ apt:
18
+ packages:
19
+ - postgresql-14
20
+ - postgresql-client-14
21
+ env:
22
+ global:
23
+ - DATABASE_URL="postgresql://127.0.0.1:5432/pursuit_test"
24
+ - RSPEC_DEFAULT_FORMATTER=doc
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,161 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pursuit (0.1.0)
5
+ activerecord (>= 5.2.0, < 6.1.0)
6
+ activesupport (>= 5.2.0, < 6.1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actionpack (6.0.4.1)
12
+ actionview (= 6.0.4.1)
13
+ activesupport (= 6.0.4.1)
14
+ rack (~> 2.0, >= 2.0.8)
15
+ rack-test (>= 0.6.3)
16
+ rails-dom-testing (~> 2.0)
17
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
18
+ actionview (6.0.4.1)
19
+ activesupport (= 6.0.4.1)
20
+ builder (~> 3.1)
21
+ erubi (~> 1.4)
22
+ rails-dom-testing (~> 2.0)
23
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
24
+ activemodel (6.0.4.1)
25
+ activesupport (= 6.0.4.1)
26
+ activerecord (6.0.4.1)
27
+ activemodel (= 6.0.4.1)
28
+ activesupport (= 6.0.4.1)
29
+ activesupport (6.0.4.1)
30
+ concurrent-ruby (~> 1.0, >= 1.0.2)
31
+ i18n (>= 0.7, < 2)
32
+ minitest (~> 5.1)
33
+ tzinfo (~> 1.1)
34
+ zeitwerk (~> 2.2, >= 2.2.2)
35
+ ast (2.4.2)
36
+ builder (3.2.4)
37
+ coderay (1.1.3)
38
+ combustion (1.3.3)
39
+ activesupport (>= 3.0.0)
40
+ railties (>= 3.0.0)
41
+ thor (>= 0.14.6)
42
+ concurrent-ruby (1.1.9)
43
+ crass (1.0.6)
44
+ diff-lcs (1.4.4)
45
+ erubi (1.10.0)
46
+ ffi (1.15.4)
47
+ formatador (0.3.0)
48
+ guard (2.18.0)
49
+ formatador (>= 0.2.4)
50
+ listen (>= 2.7, < 4.0)
51
+ lumberjack (>= 1.0.12, < 2.0)
52
+ nenv (~> 0.1)
53
+ notiffany (~> 0.0)
54
+ pry (>= 0.13.0)
55
+ shellany (~> 0.0)
56
+ thor (>= 0.18.1)
57
+ guard-compat (1.2.1)
58
+ guard-rspec (4.7.3)
59
+ guard (~> 2.1)
60
+ guard-compat (~> 1.1)
61
+ rspec (>= 2.99.0, < 4.0)
62
+ i18n (1.8.11)
63
+ concurrent-ruby (~> 1.0)
64
+ jaro_winkler (1.5.4)
65
+ listen (3.7.0)
66
+ rb-fsevent (~> 0.10, >= 0.10.3)
67
+ rb-inotify (~> 0.9, >= 0.9.10)
68
+ loofah (2.12.0)
69
+ crass (~> 1.0.2)
70
+ nokogiri (>= 1.5.9)
71
+ lumberjack (1.2.8)
72
+ method_source (1.0.0)
73
+ minitest (5.14.4)
74
+ nenv (0.3.0)
75
+ nokogiri (1.12.5-x86_64-darwin)
76
+ racc (~> 1.4)
77
+ notiffany (0.1.3)
78
+ nenv (~> 0.1)
79
+ shellany (~> 0.0)
80
+ parallel (1.21.0)
81
+ parser (3.0.2.0)
82
+ ast (~> 2.4.1)
83
+ pry (0.14.1)
84
+ coderay (~> 1.1)
85
+ method_source (~> 1.0)
86
+ racc (1.6.0)
87
+ rack (2.2.3)
88
+ rack-test (1.1.0)
89
+ rack (>= 1.0, < 3)
90
+ rails-dom-testing (2.0.3)
91
+ activesupport (>= 4.2.0)
92
+ nokogiri (>= 1.6)
93
+ rails-html-sanitizer (1.4.2)
94
+ loofah (~> 2.3)
95
+ railties (6.0.4.1)
96
+ actionpack (= 6.0.4.1)
97
+ activesupport (= 6.0.4.1)
98
+ method_source
99
+ rake (>= 0.8.7)
100
+ thor (>= 0.20.3, < 2.0)
101
+ rainbow (3.0.0)
102
+ rake (13.0.6)
103
+ rb-fsevent (0.11.0)
104
+ rb-inotify (0.10.1)
105
+ ffi (~> 1.0)
106
+ rspec (3.9.0)
107
+ rspec-core (~> 3.9.0)
108
+ rspec-expectations (~> 3.9.0)
109
+ rspec-mocks (~> 3.9.0)
110
+ rspec-core (3.9.3)
111
+ rspec-support (~> 3.9.3)
112
+ rspec-expectations (3.9.4)
113
+ diff-lcs (>= 1.2.0, < 2.0)
114
+ rspec-support (~> 3.9.0)
115
+ rspec-mocks (3.9.1)
116
+ diff-lcs (>= 1.2.0, < 2.0)
117
+ rspec-support (~> 3.9.0)
118
+ rspec-rails (3.9.1)
119
+ actionpack (>= 3.0)
120
+ activesupport (>= 3.0)
121
+ railties (>= 3.0)
122
+ rspec-core (~> 3.9.0)
123
+ rspec-expectations (~> 3.9.0)
124
+ rspec-mocks (~> 3.9.0)
125
+ rspec-support (~> 3.9.0)
126
+ rspec-support (3.9.4)
127
+ rubocop (0.77.0)
128
+ jaro_winkler (~> 1.5.1)
129
+ parallel (~> 1.10)
130
+ parser (>= 2.6)
131
+ rainbow (>= 2.2.2, < 4.0)
132
+ ruby-progressbar (~> 1.7)
133
+ unicode-display_width (>= 1.4.0, < 1.7)
134
+ ruby-progressbar (1.11.0)
135
+ shellany (0.0.1)
136
+ sqlite3 (1.4.2)
137
+ thor (1.1.0)
138
+ thread_safe (0.3.6)
139
+ tzinfo (1.2.9)
140
+ thread_safe (~> 0.1)
141
+ unicode-display_width (1.6.1)
142
+ yard (0.9.26)
143
+ zeitwerk (2.5.1)
144
+
145
+ PLATFORMS
146
+ x86_64-darwin-21
147
+
148
+ DEPENDENCIES
149
+ bundler (~> 2.0)
150
+ combustion (~> 1.1)
151
+ guard-rspec (~> 4.7)
152
+ pursuit!
153
+ rake (~> 13.0)
154
+ rspec (~> 3.8)
155
+ rspec-rails (~> 3.8)
156
+ rubocop (~> 0.77.0)
157
+ sqlite3 (~> 1.4)
158
+ yard (~> 0.9.20)
159
+
160
+ BUNDLED WITH
161
+ 2.2.22
data/Guardfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: 'bundle exec rspec' do
4
+ require 'guard/rspec/dsl'
5
+
6
+ dsl = Guard::RSpec::Dsl.new(self)
7
+
8
+ # RSpec files
9
+ rspec = dsl.rspec
10
+ watch(rspec.spec_helper) { rspec.spec_dir }
11
+ watch(rspec.spec_support) { rspec.spec_dir }
12
+ watch(rspec.spec_files)
13
+
14
+ # Ruby files
15
+ ruby = dsl.ruby
16
+ dsl.watch_spec_files_for(ruby.lib_files)
17
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Nialto Services
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Pursuit
2
+
3
+ Advanced key-based searching for ActiveRecord objects.
4
+
5
+ ## Installation
6
+
7
+ You can install **Pursuit** using the following command:
8
+
9
+ $ gem install pursuit
10
+
11
+ Or, by adding the following to your `Gemfile`:
12
+
13
+ ```ruby
14
+ gem 'pursuit'
15
+ ```
16
+
17
+ ### Usage
18
+
19
+ You can use the convenient DSL syntax to declare which attributes and relationships are searchable:
20
+
21
+ ```ruby
22
+ class Product < ActiveRecord::Base
23
+ has_search relationships: { variations: %i[title] },
24
+ keyed_attributes: %i[title description rating],
25
+ unkeyed_attributes: %i[title description]
26
+ end
27
+ ```
28
+
29
+ This creates a ```.search``` method on your record class which accepts a single query argument:
30
+
31
+ ```ruby
32
+ Product.search('plain shirt rating>=3')
33
+ ```
34
+
35
+ ## Development
36
+
37
+ After checking out the repo, run `bundle exec rake spec` to run the tests.
38
+
39
+ To install this gem onto your machine, run `bundle exec rake install`.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'pursuit'
5
+ require 'pry'
6
+
7
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
data/config.ru ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.initialize! :all
9
+ run Combustion::Application
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Provides a DSL for the `ActiveRecord::Base` class.
5
+ #
6
+ module ActiveRecordDSL
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def has_search(relationships: {}, keyed_attributes: [], unkeyed_attributes: [])
13
+ raise 'The #search method has already been defined.' if respond_to?(:search)
14
+
15
+ # The value of `self` is a constant for the current `ActiveRecord::Base` subclass. We'll need to capture this
16
+ # in a custom variable to make it accessible from within the #define_method block.
17
+ klass = self
18
+
19
+ define_method(:search) do |query|
20
+ search = Pursuit::ActiveRecordSearch.new(
21
+ klass,
22
+ relationships: relationships,
23
+ keyed_attributes: keyed_attributes,
24
+ unkeyed_attributes: unkeyed_attributes
25
+ )
26
+
27
+ search.search(query)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ class ActiveRecordSearch
5
+ # @return [Class<ActiveRecord::Base>] The `ActiveRecord::Base` child class being searched.
6
+ #
7
+ attr_reader :klass
8
+
9
+ # @return [Hash<Symbol, Array<Symbol>>] The attribute names for relationships which can be queried with a keyed
10
+ # term and the attributes in the relative's table which should be searched.
11
+ #
12
+ attr_accessor :relationships
13
+
14
+ # @return [Array<Symbol>] The attribute names which can be queried with a keyed term (e.g. 'last_name:*herb').
15
+ #
16
+ attr_accessor :keyed_attributes
17
+
18
+ # @return [Array<Symbol>] The attribute names which can be queried with an unkeyed term (e.g. 'herb').
19
+ #
20
+ attr_accessor :unkeyed_attributes
21
+
22
+ # Create a new instance to search a specific ActiveRecord record class.
23
+ #
24
+ # @param klass [Class<ActiveRecord::Base>]
25
+ # @param relationships [Hash<Symbol, Array<Symbol>>]
26
+ # @param keyed_attributes [Array<Symbol>]
27
+ # @param unkeyed_attributes [Array<Symbol>]
28
+ #
29
+ def initialize(klass, relationships: {}, keyed_attributes: [], unkeyed_attributes: [])
30
+ @klass = klass
31
+ @relationships = relationships
32
+ @keyed_attributes = keyed_attributes
33
+ @unkeyed_attributes = unkeyed_attributes
34
+ end
35
+
36
+ # Search the record with the specified query.
37
+ #
38
+ # @param query [String] The query to transform into a SQL search.
39
+ # @return [ActiveRecord::Relation] The search results.
40
+ #
41
+ def search(query)
42
+ klass.where(build_arel(query))
43
+ end
44
+
45
+ private
46
+
47
+ def build_arel(query)
48
+ parser = TermParser.new(query, keys: relationships.keys + keyed_attributes)
49
+ unkeyed_arel = build_arel_for_unkeyed_term(parser.unkeyed_term)
50
+ keyed_arel = build_arel_for_keyed_terms(parser.keyed_terms)
51
+
52
+ if unkeyed_arel && keyed_arel
53
+ unkeyed_arel.and(keyed_arel)
54
+ else
55
+ unkeyed_arel || keyed_arel
56
+ end
57
+ end
58
+
59
+ def build_arel_for_unkeyed_term(value)
60
+ return nil if value.blank?
61
+
62
+ sanitized_value = "%#{klass.sanitize_sql_like(value)}%"
63
+ unkeyed_attributes.reduce(nil) do |chain, attribute_name|
64
+ node = klass.arel_table[attribute_name].matches(sanitized_value)
65
+ chain ? chain.or(node) : node
66
+ end
67
+ end
68
+
69
+ def build_arel_for_keyed_terms(terms)
70
+ return nil if terms.blank?
71
+
72
+ terms.reduce(nil) do |chain, term|
73
+ reflection = klass.reflections[term.key]
74
+ node = if reflection.present?
75
+ keys = relationships[term.key.to_sym].presence || []
76
+ build_arel_for_reflection(reflection, keys, term.operator, term.value)
77
+ else
78
+ build_arel_for_attribute(klass.arel_table[term.key], term.operator, term.value)
79
+ end
80
+
81
+ chain ? chain.and(node) : node
82
+ end
83
+ end
84
+
85
+ def build_arel_for_attribute(attribute, operator, value)
86
+ sanitized_value = ActiveRecord::Base.sanitize_sql_like(value)
87
+
88
+ case operator
89
+ when '>' then attribute.gt(sanitized_value)
90
+ when '>=' then attribute.gteq(sanitized_value)
91
+ when '<' then attribute.lt(sanitized_value)
92
+ when '<=' then attribute.lteq(sanitized_value)
93
+ when '*=' then attribute.matches("%#{sanitized_value}%")
94
+ when '!*=' then attribute.does_not_match("%#{sanitized_value}%")
95
+ when '!=' then attribute.not_eq(sanitized_value)
96
+ when '=='
97
+ if value.present?
98
+ attribute.eq(sanitized_value)
99
+ else
100
+ attribute.eq(nil).or(attribute.eq(''))
101
+ end
102
+ else
103
+ raise "The operator '#{operator}' is not supported."
104
+ end
105
+ end
106
+
107
+ def build_arel_for_reflection(reflection, relation_attributes, operator, value)
108
+ nodes = build_arel_for_reflection_join(reflection)
109
+ count_nodes = build_arel_for_relation_count(nodes, operator, value)
110
+ return count_nodes if count_nodes.present?
111
+
112
+ match_nodes = relation_attributes.reduce(nil) do |chain, attribute_name|
113
+ node = build_arel_for_attribute(reflection.klass.arel_table[attribute_name], operator, value)
114
+ chain ? chain.or(node) : node
115
+ end
116
+
117
+ return nil if match_nodes.blank?
118
+
119
+ nodes.where(match_nodes).project(1).exists
120
+ end
121
+
122
+ def build_arel_for_reflection_join(reflection)
123
+ reflection_table = reflection.klass.arel_table
124
+ reflection_through = reflection.through_reflection
125
+
126
+ if reflection_through.present?
127
+ # :has_one through / :has_many through
128
+ reflection_through_table = reflection_through.klass.arel_table
129
+ reflection_table.join(reflection_through_table).on(
130
+ reflection_through_table[reflection.foreign_key].eq(reflection_table[reflection.klass.primary_key])
131
+ ).where(
132
+ reflection_through_table[reflection_through.foreign_key].eq(klass.arel_table[klass.primary_key])
133
+ )
134
+ else
135
+ # :has_one / :has_many
136
+ reflection_table.where(
137
+ reflection_table[reflection.foreign_key].eq(klass.arel_table[klass.primary_key])
138
+ )
139
+ end
140
+ end
141
+
142
+ def build_arel_for_relation_count(nodes, operator, value)
143
+ build = proc do |klass|
144
+ count = ActiveRecord::Base.sanitize_sql_like(value).to_i
145
+ klass.new(nodes.project(Arel.star.count), count)
146
+ end
147
+
148
+ case operator
149
+ when '>' then build.call(Arel::Nodes::GreaterThan)
150
+ when '>=' then build.call(Arel::Nodes::GreaterThanOrEqual)
151
+ when '<' then build.call(Arel::Nodes::LessThan)
152
+ when '<=' then build.call(Arel::Nodes::LessThanOrEqual)
153
+ else
154
+ return nil unless value =~ /^([0-9]+)$/
155
+
156
+ case operator
157
+ when '==' then build.call(Arel::Nodes::Equality)
158
+ when '!=' then build.call(Arel::Nodes::NotEqual)
159
+ when '*=' then build.call(Arel::Nodes::Matches)
160
+ when '!*=' then build.call(Arel::Nodes::DoesNotMatch)
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # @return [String] The gem's semantic version number.
5
+ #
6
+ VERSION = '0.1.0'
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ class Railtie < Rails::Railtie
5
+ initializer 'pursuit.active_record.inject_dsl' do
6
+ ActiveSupport.on_load(:active_record) do
7
+ require 'pursuit/active_record_dsl'
8
+
9
+ ActiveRecord::Base.include(Pursuit::ActiveRecordDSL)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ class TermParser
5
+ # @return [Struct] Represents a single keyed term extracted from a query.
6
+ #
7
+ KeyedTerm = Struct.new(:key, :operator, :value)
8
+
9
+ # @return [Array<Pursuit::TermParser::KeyedTerm>] The keys which are permitted for use as keyed terms.
10
+ #
11
+ attr_reader :keyed_terms
12
+
13
+ # @return [String] The unkeyed term.
14
+ #
15
+ attr_reader :unkeyed_term
16
+
17
+ # Create a new `TermParser` by parsing the specified query into an 'unkeyed term' and 'keyed terms'.
18
+ #
19
+ # @param query [String] The query to parse.
20
+ # @param keys [Array<Symbol>] The keys which are permitted for use as keyed terms.
21
+ #
22
+ def initialize(query, keys: [])
23
+ keys = keys.map(&:to_s)
24
+
25
+ @keyed_terms = []
26
+ @unkeyed_term = query.gsub(/(\s+)?(\w+)(==|\*=|!=|!\*=|<=|>=|<|>)("([^"]+)?"|'([^']+)?'|[^\s]+)(\s+)?/) do |term|
27
+ key = Regexp.last_match(2)
28
+ next term unless keys.include?(key)
29
+
30
+ operator = Regexp.last_match(3)
31
+ value = Regexp.last_match(4)
32
+ value = value[1..-2] if value =~ /^(".*"|'.*')$/
33
+
34
+ @keyed_terms << KeyedTerm.new(key, operator, value)
35
+
36
+ # Both the starting and ending spaces surrounding the keyed term can be removed, so in this case we'll need to
37
+ # replace with a single space to ensure the unkeyed term's words are separated correctly.
38
+ if term =~ /^\s.*\s$/
39
+ ' '
40
+ else
41
+ ''
42
+ end
43
+ end
44
+
45
+ @unkeyed_term = @unkeyed_term.strip
46
+ end
47
+ end
48
+ end
data/lib/pursuit.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pursuit/constants'
4
+ require 'pursuit/term_parser'
5
+ require 'pursuit/active_record_search'
6
+ require 'pursuit/railtie' if defined?(Rails::Railtie)