pursuit 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.
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)