Initial commit
26
Gemfile
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'activesupport', '~> 4.2', require: false
|
||||
# backports left_joins from rails 5 to rails 4
|
||||
gem 'squeel', '~> 1.2', require: false
|
||||
|
||||
# Add dependencies to develop your gem here.
|
||||
# Include everything needed to run rake, tests, features, etc.
|
||||
group :development do
|
||||
gem 'activerecord', '~> 4.2', require: false
|
||||
# gem used by juwelier/github_api. Needs to be version locked to work on Ruby 2.1
|
||||
gem 'addressable', '~> 2.4', '< 3.0', require: false
|
||||
gem 'bundler', '~> 1.0', require: false
|
||||
gem 'database_cleaner', '~> 1.7', require: false
|
||||
gem 'dotenv', '~> 2.7', require: false
|
||||
gem 'juwelier', '~> 2.1', require: false
|
||||
gem 'pg', '~> 0.15', require: false
|
||||
gem 'pry', '~> 0.12', require: false
|
||||
gem 'rdoc', '~> 3.12', require: false
|
||||
gem 'rspec', '~> 3.8', require: false
|
||||
gem 'rubocop', '~> 0.54', require: false
|
||||
gem 'rubocop-rspec', '~> 1.23', require: false
|
||||
gem 'simplecov', '~> 0.16', require: false
|
||||
end
|
||||
143
Gemfile.lock
Normal file
@@ -0,0 +1,143 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activemodel (4.2.11.1)
|
||||
activesupport (= 4.2.11.1)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.2.11.1)
|
||||
activemodel (= 4.2.11.1)
|
||||
activesupport (= 4.2.11.1)
|
||||
arel (~> 6.0)
|
||||
activesupport (4.2.11.1)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.6.0)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
arel (6.0.4)
|
||||
ast (2.4.0)
|
||||
builder (3.2.3)
|
||||
coderay (1.1.2)
|
||||
concurrent-ruby (1.1.5)
|
||||
database_cleaner (1.7.0)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
diff-lcs (1.3)
|
||||
docile (1.3.2)
|
||||
dotenv (2.7.2)
|
||||
faraday (0.15.4)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
git (1.5.0)
|
||||
github_api (0.18.2)
|
||||
addressable (~> 2.4)
|
||||
descendants_tracker (~> 0.0.4)
|
||||
faraday (~> 0.8)
|
||||
hashie (~> 3.5, >= 3.5.2)
|
||||
oauth2 (~> 1.0)
|
||||
hashie (3.6.0)
|
||||
highline (2.0.2)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (1.8.6)
|
||||
juwelier (2.1.3)
|
||||
builder
|
||||
bundler (>= 1.13)
|
||||
git (>= 1.2.5)
|
||||
github_api
|
||||
highline (>= 1.6.15)
|
||||
nokogiri (>= 1.5.10)
|
||||
rake
|
||||
rdoc
|
||||
semver
|
||||
jwt (2.2.1)
|
||||
method_source (0.9.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.11.3)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
nokogiri (1.9.1)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
oauth2 (1.4.1)
|
||||
faraday (>= 0.8, < 0.16.0)
|
||||
jwt (>= 1.0, < 3.0)
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
parallel (1.12.1)
|
||||
parser (2.5.1.0)
|
||||
ast (~> 2.4.0)
|
||||
pg (0.18.4)
|
||||
polyamorous (1.1.0)
|
||||
activerecord (>= 3.0)
|
||||
powerpack (0.1.1)
|
||||
pry (0.12.2)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.9.0)
|
||||
public_suffix (3.1.1)
|
||||
rack (1.6.11)
|
||||
rainbow (3.0.0)
|
||||
rake (12.3.2)
|
||||
rdoc (3.12.2)
|
||||
json (~> 1.4)
|
||||
rspec (3.8.0)
|
||||
rspec-core (~> 3.8.0)
|
||||
rspec-expectations (~> 3.8.0)
|
||||
rspec-mocks (~> 3.8.0)
|
||||
rspec-core (3.8.1)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-expectations (3.8.4)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-mocks (3.8.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-support (3.8.2)
|
||||
rubocop (0.54.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.5)
|
||||
powerpack (~> 0.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||
rubocop-rspec (1.23.0)
|
||||
rubocop (>= 0.52.1)
|
||||
ruby-progressbar (1.9.0)
|
||||
semver (1.0.1)
|
||||
simplecov (0.16.1)
|
||||
docile (~> 1.1)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
squeel (1.2.3)
|
||||
activerecord (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
polyamorous (~> 1.1.0)
|
||||
thread_safe (0.3.6)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
unicode-display_width (1.3.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
activerecord (~> 4.2)
|
||||
activesupport (~> 4.2)
|
||||
addressable (~> 2.4, < 3.0)
|
||||
bundler (~> 1.0)
|
||||
database_cleaner (~> 1.7)
|
||||
dotenv (~> 2.7)
|
||||
juwelier (~> 2.1)
|
||||
pg (~> 0.15)
|
||||
pry (~> 0.12)
|
||||
rdoc (~> 3.12)
|
||||
rspec (~> 3.8)
|
||||
rubocop (~> 0.54)
|
||||
rubocop-rspec (~> 1.23)
|
||||
simplecov (~> 0.16)
|
||||
squeel (~> 1.2)
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.2
|
||||
62
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
@Library('jenkins-common') _
|
||||
|
||||
utils.initProperties()
|
||||
|
||||
def appName = 'pg_searchable'
|
||||
|
||||
def databaseName = 'pg_searchable'
|
||||
def databaseHost = 'localhost'
|
||||
def databasePort = '5432'
|
||||
def databasePool = '10'
|
||||
def databaseUser = 'postgres'
|
||||
def databasePass = 'Q99d5Aikh2VR'
|
||||
def nexusUser = 'jenkins'
|
||||
def nexusPass = 'ASTrIAnDIdEONVan'
|
||||
|
||||
utils.onNode(nodeLabel: 'ubuntu-ruby-215') {
|
||||
String version
|
||||
|
||||
version = utils.stagesGitCheckout()
|
||||
|
||||
stage('Save version') {
|
||||
writeFile file: 'VERSION', text: version
|
||||
}
|
||||
|
||||
stage('Run tests') {
|
||||
withEnv([
|
||||
"DATABASE_NAME=${databaseName}",
|
||||
"DATABASE_HOST=${databaseHost}",
|
||||
"DATABASE_PORT=${databasePort}",
|
||||
"DATABASE_POOL=${databasePool}",
|
||||
"DATABASE_USERNAME=${databaseUser}",
|
||||
"DATABASE_PASSWORD=${databasePass}"
|
||||
]) {
|
||||
sh '''
|
||||
apt-get update
|
||||
apt-get -y install postgresql-client libcurl3
|
||||
docker run --name postgres-pg-searchable -p${DATABASE_PORT}:5432 -e POSTGRES_PASSWORD=${DATABASE_PASSWORD} -d postgres:9.5
|
||||
gem install bundler --version=`sed -e '$!d' Gemfile.lock | xargs` --no-ri --no-rdoc
|
||||
gem install nexus --no-ri --no-rdoc
|
||||
bundle install --path vendor --without production --with test development
|
||||
bundle exec rake spec
|
||||
'''.stripIndent()
|
||||
}
|
||||
ruby.stepsRcovPublish()
|
||||
}
|
||||
|
||||
stage('Run rubocop') {
|
||||
sh '''
|
||||
bundle exec rubocop
|
||||
'''
|
||||
}
|
||||
|
||||
stage('Build gem') {
|
||||
sh '''
|
||||
bundle exec rake build
|
||||
'''
|
||||
}
|
||||
|
||||
stage('Publish gem') {
|
||||
sh "gem nexus --url https://nexus.onsmartdev.media/repository/rubygems-hosted --credential ${nexusUser}:${nexusPass} pkg/*.gem"
|
||||
}
|
||||
}
|
||||
121
README.md
@@ -1,2 +1,121 @@
|
||||
# gem
|
||||
## DESCRIPTION
|
||||
|
||||
This ruby gem adds a PgSearchable Model Concern to be included into Rails models that allows easy (simple and limited) usage of english like queries using postgres's builtin fulltext search capabilities.
|
||||
|
||||
## REQUIREMENTS
|
||||
|
||||
- Ruby 2.1+
|
||||
- ActiveRecord 4.2+
|
||||
- Postgresql 9.2+
|
||||
|
||||
## INSTALL
|
||||
|
||||
Add this to your Gemfile:
|
||||
|
||||
```ruby
|
||||
gem 'pg_searchable'
|
||||
```
|
||||
|
||||
## USAGE
|
||||
|
||||
To add PgSearch to an Active Record model, include the PgSearchable module and call the added `pg_search` method:
|
||||
|
||||
```ruby
|
||||
class Model < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[name description]
|
||||
end
|
||||
|
||||
Model.scope_search('cats or dogs') # Will find models that have either 'cat' or 'dog' keyword in their name or description
|
||||
```
|
||||
|
||||
The pg_search method accepts the following properties:
|
||||
|
||||
- `fields`: An array of fields that are gonna be matched by the query. The field type needs to able to converted into text (using psql `field::text`)
|
||||
- `scope`: The name of the scope method that will be added to the model. Defaults to `scope_search`
|
||||
- `cache`: If present, then instead of dynamically calculating the tsvector during runtime, it will use the tsvector field provided instead. See "USING CACHE FIELD" below
|
||||
- `skip_callback`: When a cache field is defined and this value is true, the cache field will not be updated automatically
|
||||
- `language`: language of the dictionary to be used to generate the lexemes. Defaults to `english`, which uses lexemes comparison. Another useful value is `simple` which only removes stopwords but does lower case exact word comparison
|
||||
- `wildcard`: enables or disabled wildcard search feature for the keywords. Defaults to true
|
||||
- `external_cache_data`: a method that returns a String or an Array of Strings of extra data to be added to the cache value when updating the cache value.
|
||||
- `joins`: specifies an array of relation names, or a hash (to do multiple associations on) and does a left outer join with them, allowing those fields to be dynamically searched upon. Can only be used without a cache field, and if relation data is required while using the cache use `external_cache_data` instead to populate the cache field. When using joins, its advisable to prefix the field names in the field options with the table names to avoid collisions (which would result in errors). Examples.:
|
||||
|
||||
```ruby
|
||||
class Product < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[products.name tags.value categories.name sections.name],
|
||||
joins: [{tags: :category}, :sections]
|
||||
has_many :tags
|
||||
has_many :sections, through: :tags
|
||||
end
|
||||
|
||||
class Tag < ActiveRecord::Base
|
||||
belongs_to :product
|
||||
belongs_to :category
|
||||
has_many :sections
|
||||
end
|
||||
|
||||
class Section < ActiveRecord::Base
|
||||
belongs_to :tag
|
||||
end
|
||||
|
||||
class Category < ActiveRecord::Base
|
||||
has_many :tags
|
||||
end
|
||||
```
|
||||
|
||||
Do note that if you specify the table names on the fields property, they should refer after the table names (usually pluralized in rails), while on the joins property it follows the relation name (which can be singular or plural depending on the relation type)
|
||||
|
||||
## USING CACHE FIELD
|
||||
|
||||
For perfomance improvements, a cache field can be added and configured for pg_search to use it instead of dynamically generating it during runtime.
|
||||
|
||||
To use it, first add the vector field to the model with a migration:
|
||||
|
||||
```ruby
|
||||
class AddSearchVectorToModels < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :models, :search_cache, :tsvector
|
||||
add_index :models, :search_cache, using: :gin
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Then set the `cache` property on the `pg_search` call:
|
||||
|
||||
```ruby
|
||||
pg_search fields: %i[name description], cache: :search_cache
|
||||
```
|
||||
|
||||
This will add an after_save callback to the model which will automatically update the cache field with the new values everytime the record is saved. If you wanna search the vector field but manually update the cache, you can do so by passing `skip_callback` to false, and then manually running the `update_pg_search_cache` method on a model instance.
|
||||
|
||||
If you required to index data of external relationships, this can be accomplished by using the cache field with the `external_cache_data` option, passing the name of an instance method for the model that retrieves the external data.
|
||||
|
||||
For example, considering a Tag model that has a value column and a 1:N relation with the Product model, this can be achieved by doing:
|
||||
|
||||
```ruby
|
||||
class Product < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[name], cache: :search_cache, external_cache_data: :tag_values
|
||||
has_many :tags
|
||||
|
||||
def tag_values
|
||||
tags.pluck(:value)
|
||||
end
|
||||
end
|
||||
|
||||
class Tag < ActiveRecord::Base
|
||||
belongs_to :product
|
||||
after_save { product.update_pg_search_cache if product.present? }
|
||||
end
|
||||
```
|
||||
|
||||
Since its an external data, everytime the external data has changed you need to make sure to call `update_pg_search_cache` method or a save/update that will trigger the update method in order for that value to be cached and searchable.
|
||||
|
||||
## DEVELOPING
|
||||
|
||||
To run the test suite create `.env.test.local` file containing the same entries as with `.env.test` but with the correct local settings to a postgres database, run `bundle install` to download dependencies and run `rake`
|
||||
|
||||
## CONTRIBUTING
|
||||
|
||||
Make sure the test coverage remains at 100%, there are no rubocop complaints (`bundle exec rubocop`) and make a Pull Request.
|
||||
|
||||
46
Rakefile
Normal file
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rubygems'
|
||||
require 'bundler'
|
||||
begin
|
||||
Bundler.setup(:default, :development)
|
||||
rescue Bundler::BundlerError => e
|
||||
$stderr.warn e.message
|
||||
$stderr.warn 'Run `bundle install` to install missing gems'
|
||||
exit e.status_code
|
||||
end
|
||||
require 'rake'
|
||||
require 'juwelier'
|
||||
require_relative './version'
|
||||
Juwelier::Tasks.new do |gem|
|
||||
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
||||
gem.name = 'pg_searchable_regex'
|
||||
gem.homepage = ''
|
||||
gem.license = 'Nonstandard'
|
||||
gem.summary = 'A simple ActiveModel concern that can be added to do fulltext search with postgres databases'
|
||||
gem.description = 'A simple ActiveModel concern that can be added to do fulltext search with postgres databases'
|
||||
gem.email = 'dev@outfrontmedia.com'
|
||||
gem.authors = ['Outfront Media']
|
||||
gem.files.include('lib/**/*.rb')
|
||||
gem.version = Version.to_s
|
||||
|
||||
# dependencies defined in Gemfile
|
||||
end
|
||||
Juwelier::RubygemsDotOrgTasks.new
|
||||
require 'rspec/core/rake_task'
|
||||
RSpec::Core::RakeTask.new(:spec) do |t|
|
||||
t.pattern = Dir.glob('spec/**/*_spec.rb')
|
||||
t.rspec_opts = '--format documentation'
|
||||
end
|
||||
|
||||
task default: :spec
|
||||
|
||||
require 'rdoc/task'
|
||||
Rake::RDocTask.new do |rdoc|
|
||||
version = File.exist?('VERSION') ? File.read('VERSION') : ''
|
||||
|
||||
rdoc.rdoc_dir = 'rdoc'
|
||||
rdoc.title = "pg_searchable #{version}"
|
||||
rdoc.rdoc_files.include('README*')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
end
|
||||
5
coverage/.last_run.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"result": {
|
||||
"covered_percent": 66.92
|
||||
}
|
||||
}
|
||||
254
coverage/.resultset.json
Normal file
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"RSpec": {
|
||||
"coverage": {
|
||||
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/pg_searchable_regex.rb": [
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
12,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
12,
|
||||
43,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
31,
|
||||
30,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
12,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/text_to_tsquery.rb": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/text_to_regex_query.rb": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
1,
|
||||
35,
|
||||
35,
|
||||
35,
|
||||
35,
|
||||
37,
|
||||
37,
|
||||
7,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
5,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
5,
|
||||
null,
|
||||
5,
|
||||
7,
|
||||
7,
|
||||
5,
|
||||
5,
|
||||
null,
|
||||
5,
|
||||
3,
|
||||
null,
|
||||
5,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
5,
|
||||
13,
|
||||
13,
|
||||
null,
|
||||
5,
|
||||
8,
|
||||
null,
|
||||
5,
|
||||
null,
|
||||
5,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"timestamp": 1579269179
|
||||
}
|
||||
}
|
||||
0
coverage/.resultset.json.lock
Normal file
799
coverage/assets/0.10.2/application.css
Normal file
@@ -0,0 +1,799 @@
|
||||
/* -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Blueprint CSS Framework 0.9
|
||||
http://blueprintcss.org
|
||||
|
||||
* Copyright (c) 2007-Present. See LICENSE for more info.
|
||||
* See README for instructions on how to use Blueprint.
|
||||
* For credits and origins, see AUTHORS.
|
||||
* This is a compressed file. See the sources in the 'src' directory.
|
||||
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
/* reset.css */
|
||||
|
||||
html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, dialog, figure, footer, header, hgroup, nav, section {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;}
|
||||
article, aside, dialog, figure, footer, header, hgroup, nav, section {display:block;}
|
||||
body {line-height:1.5;}
|
||||
table {border-collapse:separate;border-spacing:0;}
|
||||
caption, th, td {text-align:left;font-weight:normal;}
|
||||
table, td, th {vertical-align:middle;}
|
||||
blockquote:before, blockquote:after, q:before, q:after {content:"";}
|
||||
blockquote, q {quotes:"" "";}
|
||||
a img {border:none;}
|
||||
|
||||
/* typography.css */
|
||||
html {font-size:100.01%;}
|
||||
body {font-size:82%;color:#222;background:#fff;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;}
|
||||
h1, h2, h3, h4, h5, h6 {font-weight:normal;color:#111;}
|
||||
h1 {font-size:3em;line-height:1;margin-bottom:0.5em;}
|
||||
h2 {font-size:2em;margin-bottom:0.75em;}
|
||||
h3 {font-size:1.5em;line-height:1;margin-bottom:1em;}
|
||||
h4 {font-size:1.2em;line-height:1.25;margin-bottom:1.25em;}
|
||||
h5 {font-size:1em;font-weight:bold;margin-bottom:1.5em;}
|
||||
h6 {font-size:1em;font-weight:bold;}
|
||||
h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {margin:0;}
|
||||
p {margin:0 0 1.5em;}
|
||||
p img.left {float:left;margin:1.5em 1.5em 1.5em 0;padding:0;}
|
||||
p img.right {float:right;margin:1.5em 0 1.5em 1.5em;}
|
||||
a:focus, a:hover {color:#000;}
|
||||
a {color:#009;text-decoration:underline;}
|
||||
blockquote {margin:1.5em;color:#666;font-style:italic;}
|
||||
strong {font-weight:bold;}
|
||||
em, dfn {font-style:italic;}
|
||||
dfn {font-weight:bold;}
|
||||
sup, sub {line-height:0;}
|
||||
abbr, acronym {border-bottom:1px dotted #666;}
|
||||
address {margin:0 0 1.5em;font-style:italic;}
|
||||
del {color:#666;}
|
||||
pre {margin:1.5em 0;white-space:pre;}
|
||||
pre, code, tt {font:1em 'andale mono', 'lucida console', monospace;line-height:1.5;}
|
||||
li ul, li ol {margin:0;}
|
||||
ul, ol {margin:0 1.5em 1.5em 0;padding-left:3.333em;}
|
||||
ul {list-style-type:disc;}
|
||||
ol {list-style-type:decimal;}
|
||||
dl {margin:0 0 1.5em 0;}
|
||||
dl dt {font-weight:bold;}
|
||||
dd {margin-left:1.5em;}
|
||||
table {margin-bottom:1.4em;width:100%;}
|
||||
th {font-weight:bold;}
|
||||
thead th {background:#c3d9ff;}
|
||||
th, td, caption {padding:4px 10px 4px 5px;}
|
||||
tr.even td {background:#efefef;}
|
||||
tfoot {font-style:italic;}
|
||||
caption {background:#eee;}
|
||||
.small {font-size:.8em;margin-bottom:1.875em;line-height:1.875em;}
|
||||
.large {font-size:1.2em;line-height:2.5em;margin-bottom:1.25em;}
|
||||
.hide {display:none;}
|
||||
.quiet {color:#666;}
|
||||
.loud {color:#000;}
|
||||
.highlight {background:#ff0;}
|
||||
.added {background:#060;color:#fff;}
|
||||
.removed {background:#900;color:#fff;}
|
||||
.first {margin-left:0;padding-left:0;}
|
||||
.last {margin-right:0;padding-right:0;}
|
||||
.top {margin-top:0;padding-top:0;}
|
||||
.bottom {margin-bottom:0;padding-bottom:0;}
|
||||
|
||||
/* forms.css */
|
||||
label {font-weight:bold;}
|
||||
fieldset {padding:1.4em;margin:0 0 1.5em 0;border:1px solid #ccc;}
|
||||
legend {font-weight:bold;font-size:1.2em;}
|
||||
input[type=text], input[type=password], input.text, input.title, textarea, select {background-color:#fff;border:1px solid #bbb;}
|
||||
input[type=text]:focus, input[type=password]:focus, input.text:focus, input.title:focus, textarea:focus, select:focus {border-color:#666;}
|
||||
input[type=text], input[type=password], input.text, input.title, textarea, select {margin:0.5em 0;}
|
||||
input.text, input.title {width:300px;padding:5px;}
|
||||
input.title {font-size:1.5em;}
|
||||
textarea {width:390px;height:250px;padding:5px;}
|
||||
input[type=checkbox], input[type=radio], input.checkbox, input.radio {position:relative;top:.25em;}
|
||||
form.inline {line-height:3;}
|
||||
form.inline p {margin-bottom:0;}
|
||||
.error, .notice, .success {padding:.8em;margin-bottom:1em;border:2px solid #ddd;}
|
||||
.error {background:#FBE3E4;color:#8a1f11;border-color:#FBC2C4;}
|
||||
.notice {background:#FFF6BF;color:#514721;border-color:#FFD324;}
|
||||
.success {background:#E6EFC2;color:#264409;border-color:#C6D880;}
|
||||
.error a {color:#8a1f11;}
|
||||
.notice a {color:#514721;}
|
||||
.success a {color:#264409;}
|
||||
.box {padding:1.5em;margin-bottom:1.5em;background:#E5ECF9;}
|
||||
hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;}
|
||||
hr.space {background:#fff;color:#fff;visibility:hidden;}
|
||||
.clearfix:after, .container:after {content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden;}
|
||||
.clearfix, .container {display:block;}
|
||||
.clear {clear:both;}
|
||||
/*
|
||||
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
|
||||
*/
|
||||
|
||||
|
||||
pre code {
|
||||
}
|
||||
|
||||
pre .comment,
|
||||
pre .template_comment,
|
||||
pre .diff .header,
|
||||
pre .javadoc {
|
||||
color: #998;
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
pre .keyword,
|
||||
pre .css .rule .keyword,
|
||||
pre .winutils,
|
||||
pre .javascript .title,
|
||||
pre .lisp .title {
|
||||
color: #000;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
pre .number,
|
||||
pre .hexcolor {
|
||||
color: #458
|
||||
}
|
||||
|
||||
|
||||
pre .string,
|
||||
pre .tag .value,
|
||||
pre .phpdoc,
|
||||
pre .tex .formula {
|
||||
color: #d14
|
||||
}
|
||||
|
||||
pre .subst {
|
||||
color: #712;
|
||||
}
|
||||
|
||||
pre .constant,
|
||||
pre .title,
|
||||
pre .id {
|
||||
color: #900;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
pre .javascript .title,
|
||||
pre .lisp .title,
|
||||
pre .subst {
|
||||
font-weight: normal
|
||||
}
|
||||
|
||||
pre .class .title,
|
||||
pre .haskell .label,
|
||||
pre .tex .command {
|
||||
color: #458;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
pre .tag,
|
||||
pre .tag .title,
|
||||
pre .rules .property,
|
||||
pre .django .tag .keyword {
|
||||
color: #000080;
|
||||
font-weight: normal
|
||||
}
|
||||
|
||||
pre .attribute,
|
||||
pre .variable,
|
||||
pre .instancevar,
|
||||
pre .lisp .body {
|
||||
color: #008080
|
||||
}
|
||||
|
||||
pre .regexp {
|
||||
color: #009926
|
||||
}
|
||||
|
||||
pre .class {
|
||||
color: #458;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
pre .symbol,
|
||||
pre .ruby .symbol .string,
|
||||
pre .ruby .symbol .keyword,
|
||||
pre .ruby .symbol .keymethods,
|
||||
pre .lisp .keyword,
|
||||
pre .tex .special,
|
||||
pre .input_number {
|
||||
color: #990073
|
||||
}
|
||||
|
||||
pre .builtin,
|
||||
pre .built_in,
|
||||
pre .lisp .title {
|
||||
color: #0086b3
|
||||
}
|
||||
|
||||
pre .preprocessor,
|
||||
pre .pi,
|
||||
pre .doctype,
|
||||
pre .shebang,
|
||||
pre .cdata {
|
||||
color: #999;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
pre .deletion {
|
||||
background: #fdd
|
||||
}
|
||||
|
||||
pre .addition {
|
||||
background: #dfd
|
||||
}
|
||||
|
||||
pre .diff .change {
|
||||
background: #0086b3
|
||||
}
|
||||
|
||||
pre .chunk {
|
||||
color: #aaa
|
||||
}
|
||||
|
||||
pre .tex .formula {
|
||||
opacity: 0.5;
|
||||
}
|
||||
/*
|
||||
* jQuery UI CSS Framework @VERSION
|
||||
*
|
||||
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Theming/API
|
||||
*/
|
||||
|
||||
/* Layout helpers
|
||||
----------------------------------*/
|
||||
|
||||
.ui-helper-hidden { display: none; }
|
||||
.ui-helper-hidden-accessible { position: absolute; left: -99999999px; }
|
||||
.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
|
||||
.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
|
||||
.ui-helper-clearfix { display: inline-block; }
|
||||
/* required comment for clearfix to work in Opera \*/
|
||||
* html .ui-helper-clearfix { height:1%; }
|
||||
.ui-helper-clearfix { display:block; }
|
||||
/* end clearfix */
|
||||
.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
|
||||
|
||||
|
||||
/* Interaction Cues
|
||||
----------------------------------*/
|
||||
.ui-state-disabled { cursor: default !important; }
|
||||
|
||||
|
||||
/* Icons
|
||||
----------------------------------*/
|
||||
|
||||
/* states and images */
|
||||
.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
|
||||
|
||||
|
||||
/* Misc visuals
|
||||
----------------------------------*/
|
||||
|
||||
/* Overlays */
|
||||
.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
||||
|
||||
|
||||
/*
|
||||
* jQuery UI CSS Framework @VERSION
|
||||
*
|
||||
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Theming/API
|
||||
*
|
||||
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=02_glass.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
|
||||
*/
|
||||
|
||||
|
||||
/* Component containers
|
||||
----------------------------------*/
|
||||
.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; }
|
||||
.ui-widget .ui-widget { font-size: 1em; }
|
||||
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; }
|
||||
.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; }
|
||||
.ui-widget-content a { color: #222222; }
|
||||
.ui-widget-header { border: 1px solid #aaaaaa; background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }
|
||||
.ui-widget-header a { color: #222222; }
|
||||
|
||||
/* Interaction states
|
||||
----------------------------------*/
|
||||
.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3; background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #555555; }
|
||||
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555; text-decoration: none; }
|
||||
.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #999999; background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; }
|
||||
.ui-state-hover a, .ui-state-hover a:hover { color: #212121; text-decoration: none; }
|
||||
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; }
|
||||
.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121; text-decoration: none; }
|
||||
.ui-widget :active { outline: none; }
|
||||
|
||||
/* Interaction Cues
|
||||
----------------------------------*/
|
||||
.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fcefa1; background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; color: #363636; }
|
||||
.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; }
|
||||
.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; color: #cd0a0a; }
|
||||
.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a; }
|
||||
.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a; }
|
||||
.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
|
||||
.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
|
||||
.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
|
||||
|
||||
/* Icons
|
||||
----------------------------------*/
|
||||
|
||||
/* states and images */
|
||||
.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); }
|
||||
.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
|
||||
.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
|
||||
.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png); }
|
||||
.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
|
||||
.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
|
||||
.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); }
|
||||
.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png); }
|
||||
|
||||
/* positioning */
|
||||
.ui-icon-carat-1-n { background-position: 0 0; }
|
||||
.ui-icon-carat-1-ne { background-position: -16px 0; }
|
||||
.ui-icon-carat-1-e { background-position: -32px 0; }
|
||||
.ui-icon-carat-1-se { background-position: -48px 0; }
|
||||
.ui-icon-carat-1-s { background-position: -64px 0; }
|
||||
.ui-icon-carat-1-sw { background-position: -80px 0; }
|
||||
.ui-icon-carat-1-w { background-position: -96px 0; }
|
||||
.ui-icon-carat-1-nw { background-position: -112px 0; }
|
||||
.ui-icon-carat-2-n-s { background-position: -128px 0; }
|
||||
.ui-icon-carat-2-e-w { background-position: -144px 0; }
|
||||
.ui-icon-triangle-1-n { background-position: 0 -16px; }
|
||||
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
|
||||
.ui-icon-triangle-1-e { background-position: -32px -16px; }
|
||||
.ui-icon-triangle-1-se { background-position: -48px -16px; }
|
||||
.ui-icon-triangle-1-s { background-position: -64px -16px; }
|
||||
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
|
||||
.ui-icon-triangle-1-w { background-position: -96px -16px; }
|
||||
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
|
||||
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
|
||||
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
|
||||
.ui-icon-arrow-1-n { background-position: 0 -32px; }
|
||||
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
|
||||
.ui-icon-arrow-1-e { background-position: -32px -32px; }
|
||||
.ui-icon-arrow-1-se { background-position: -48px -32px; }
|
||||
.ui-icon-arrow-1-s { background-position: -64px -32px; }
|
||||
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
|
||||
.ui-icon-arrow-1-w { background-position: -96px -32px; }
|
||||
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
|
||||
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
|
||||
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
|
||||
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
|
||||
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
|
||||
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
|
||||
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
|
||||
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
|
||||
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
|
||||
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
|
||||
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
|
||||
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
|
||||
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
|
||||
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
|
||||
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
|
||||
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
|
||||
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
|
||||
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
|
||||
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
|
||||
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
|
||||
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
|
||||
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
|
||||
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
|
||||
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
|
||||
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
|
||||
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
|
||||
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
|
||||
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
|
||||
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
|
||||
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
|
||||
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
|
||||
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
|
||||
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
|
||||
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
|
||||
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
|
||||
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
|
||||
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
|
||||
.ui-icon-arrow-4 { background-position: 0 -80px; }
|
||||
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
|
||||
.ui-icon-extlink { background-position: -32px -80px; }
|
||||
.ui-icon-newwin { background-position: -48px -80px; }
|
||||
.ui-icon-refresh { background-position: -64px -80px; }
|
||||
.ui-icon-shuffle { background-position: -80px -80px; }
|
||||
.ui-icon-transfer-e-w { background-position: -96px -80px; }
|
||||
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
|
||||
.ui-icon-folder-collapsed { background-position: 0 -96px; }
|
||||
.ui-icon-folder-open { background-position: -16px -96px; }
|
||||
.ui-icon-document { background-position: -32px -96px; }
|
||||
.ui-icon-document-b { background-position: -48px -96px; }
|
||||
.ui-icon-note { background-position: -64px -96px; }
|
||||
.ui-icon-mail-closed { background-position: -80px -96px; }
|
||||
.ui-icon-mail-open { background-position: -96px -96px; }
|
||||
.ui-icon-suitcase { background-position: -112px -96px; }
|
||||
.ui-icon-comment { background-position: -128px -96px; }
|
||||
.ui-icon-person { background-position: -144px -96px; }
|
||||
.ui-icon-print { background-position: -160px -96px; }
|
||||
.ui-icon-trash { background-position: -176px -96px; }
|
||||
.ui-icon-locked { background-position: -192px -96px; }
|
||||
.ui-icon-unlocked { background-position: -208px -96px; }
|
||||
.ui-icon-bookmark { background-position: -224px -96px; }
|
||||
.ui-icon-tag { background-position: -240px -96px; }
|
||||
.ui-icon-home { background-position: 0 -112px; }
|
||||
.ui-icon-flag { background-position: -16px -112px; }
|
||||
.ui-icon-calendar { background-position: -32px -112px; }
|
||||
.ui-icon-cart { background-position: -48px -112px; }
|
||||
.ui-icon-pencil { background-position: -64px -112px; }
|
||||
.ui-icon-clock { background-position: -80px -112px; }
|
||||
.ui-icon-disk { background-position: -96px -112px; }
|
||||
.ui-icon-calculator { background-position: -112px -112px; }
|
||||
.ui-icon-zoomin { background-position: -128px -112px; }
|
||||
.ui-icon-zoomout { background-position: -144px -112px; }
|
||||
.ui-icon-search { background-position: -160px -112px; }
|
||||
.ui-icon-wrench { background-position: -176px -112px; }
|
||||
.ui-icon-gear { background-position: -192px -112px; }
|
||||
.ui-icon-heart { background-position: -208px -112px; }
|
||||
.ui-icon-star { background-position: -224px -112px; }
|
||||
.ui-icon-link { background-position: -240px -112px; }
|
||||
.ui-icon-cancel { background-position: 0 -128px; }
|
||||
.ui-icon-plus { background-position: -16px -128px; }
|
||||
.ui-icon-plusthick { background-position: -32px -128px; }
|
||||
.ui-icon-minus { background-position: -48px -128px; }
|
||||
.ui-icon-minusthick { background-position: -64px -128px; }
|
||||
.ui-icon-close { background-position: -80px -128px; }
|
||||
.ui-icon-closethick { background-position: -96px -128px; }
|
||||
.ui-icon-key { background-position: -112px -128px; }
|
||||
.ui-icon-lightbulb { background-position: -128px -128px; }
|
||||
.ui-icon-scissors { background-position: -144px -128px; }
|
||||
.ui-icon-clipboard { background-position: -160px -128px; }
|
||||
.ui-icon-copy { background-position: -176px -128px; }
|
||||
.ui-icon-contact { background-position: -192px -128px; }
|
||||
.ui-icon-image { background-position: -208px -128px; }
|
||||
.ui-icon-video { background-position: -224px -128px; }
|
||||
.ui-icon-script { background-position: -240px -128px; }
|
||||
.ui-icon-alert { background-position: 0 -144px; }
|
||||
.ui-icon-info { background-position: -16px -144px; }
|
||||
.ui-icon-notice { background-position: -32px -144px; }
|
||||
.ui-icon-help { background-position: -48px -144px; }
|
||||
.ui-icon-check { background-position: -64px -144px; }
|
||||
.ui-icon-bullet { background-position: -80px -144px; }
|
||||
.ui-icon-radio-off { background-position: -96px -144px; }
|
||||
.ui-icon-radio-on { background-position: -112px -144px; }
|
||||
.ui-icon-pin-w { background-position: -128px -144px; }
|
||||
.ui-icon-pin-s { background-position: -144px -144px; }
|
||||
.ui-icon-play { background-position: 0 -160px; }
|
||||
.ui-icon-pause { background-position: -16px -160px; }
|
||||
.ui-icon-seek-next { background-position: -32px -160px; }
|
||||
.ui-icon-seek-prev { background-position: -48px -160px; }
|
||||
.ui-icon-seek-end { background-position: -64px -160px; }
|
||||
.ui-icon-seek-start { background-position: -80px -160px; }
|
||||
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
|
||||
.ui-icon-seek-first { background-position: -80px -160px; }
|
||||
.ui-icon-stop { background-position: -96px -160px; }
|
||||
.ui-icon-eject { background-position: -112px -160px; }
|
||||
.ui-icon-volume-off { background-position: -128px -160px; }
|
||||
.ui-icon-volume-on { background-position: -144px -160px; }
|
||||
.ui-icon-power { background-position: 0 -176px; }
|
||||
.ui-icon-signal-diag { background-position: -16px -176px; }
|
||||
.ui-icon-signal { background-position: -32px -176px; }
|
||||
.ui-icon-battery-0 { background-position: -48px -176px; }
|
||||
.ui-icon-battery-1 { background-position: -64px -176px; }
|
||||
.ui-icon-battery-2 { background-position: -80px -176px; }
|
||||
.ui-icon-battery-3 { background-position: -96px -176px; }
|
||||
.ui-icon-circle-plus { background-position: 0 -192px; }
|
||||
.ui-icon-circle-minus { background-position: -16px -192px; }
|
||||
.ui-icon-circle-close { background-position: -32px -192px; }
|
||||
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
|
||||
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
|
||||
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
|
||||
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
|
||||
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
|
||||
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
|
||||
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
|
||||
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
|
||||
.ui-icon-circle-zoomin { background-position: -176px -192px; }
|
||||
.ui-icon-circle-zoomout { background-position: -192px -192px; }
|
||||
.ui-icon-circle-check { background-position: -208px -192px; }
|
||||
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
|
||||
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
|
||||
.ui-icon-circlesmall-close { background-position: -32px -208px; }
|
||||
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
|
||||
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
|
||||
.ui-icon-squaresmall-close { background-position: -80px -208px; }
|
||||
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
|
||||
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
|
||||
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
|
||||
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
|
||||
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
|
||||
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
|
||||
|
||||
|
||||
/* Misc visuals
|
||||
----------------------------------*/
|
||||
|
||||
/* Corner radius */
|
||||
.ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; }
|
||||
.ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; }
|
||||
.ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
|
||||
.ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
|
||||
.ui-corner-top { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; }
|
||||
.ui-corner-bottom { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
|
||||
.ui-corner-right { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
|
||||
.ui-corner-left { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
|
||||
.ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; border-radius: 4px; }
|
||||
|
||||
/* Overlays */
|
||||
.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
|
||||
.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }
|
||||
/*
|
||||
ColorBox Core Style:
|
||||
The following CSS is consistent between example themes and should not be altered.
|
||||
*/
|
||||
#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;}
|
||||
#cboxOverlay{position:fixed; width:100%; height:100%;}
|
||||
#cboxMiddleLeft, #cboxBottomLeft{clear:left;}
|
||||
#cboxContent{position:relative;}
|
||||
#cboxLoadedContent{overflow:auto;}
|
||||
#cboxTitle{margin:0;}
|
||||
#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%; height:100%;}
|
||||
#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;}
|
||||
.cboxPhoto{float:left; margin:auto; border:0; display:block; max-width:none;}
|
||||
.cboxIframe{width:100%; height:100%; display:block; border:0;}
|
||||
#colorbox, #cboxContent, #cboxLoadedContent{box-sizing:content-box;}
|
||||
|
||||
/*
|
||||
User Style:
|
||||
Change the following styles to modify the appearance of ColorBox. They are
|
||||
ordered & tabbed in a way that represents the nesting of the generated HTML.
|
||||
*/
|
||||
#cboxOverlay{background:#000;}
|
||||
#colorbox{}
|
||||
#cboxTopLeft{width:14px; height:14px; background:url(colorbox/controls.png) no-repeat 0 0;}
|
||||
#cboxTopCenter{height:14px; background:url(colorbox/border.png) repeat-x top left;}
|
||||
#cboxTopRight{width:14px; height:14px; background:url(colorbox/controls.png) no-repeat -36px 0;}
|
||||
#cboxBottomLeft{width:14px; height:43px; background:url(colorbox/controls.png) no-repeat 0 -32px;}
|
||||
#cboxBottomCenter{height:43px; background:url(colorbox/border.png) repeat-x bottom left;}
|
||||
#cboxBottomRight{width:14px; height:43px; background:url(colorbox/controls.png) no-repeat -36px -32px;}
|
||||
#cboxMiddleLeft{width:14px; background:url(colorbox/controls.png) repeat-y -175px 0;}
|
||||
#cboxMiddleRight{width:14px; background:url(colorbox/controls.png) repeat-y -211px 0;}
|
||||
#cboxContent{background:#fff; overflow:visible;}
|
||||
.cboxIframe{background:#fff;}
|
||||
#cboxError{padding:50px; border:1px solid #ccc;}
|
||||
#cboxLoadedContent{margin-bottom:5px;}
|
||||
#cboxLoadingOverlay{background:url(colorbox/loading_background.png) no-repeat center center;}
|
||||
#cboxLoadingGraphic{background:url(colorbox/loading.gif) no-repeat center center;}
|
||||
#cboxTitle{position:absolute; bottom:-25px; left:0; text-align:center; width:100%; font-weight:bold; color:#7C7C7C;}
|
||||
#cboxCurrent{position:absolute; bottom:-25px; left:58px; font-weight:bold; color:#7C7C7C;}
|
||||
|
||||
#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{position:absolute; bottom:-29px; background:url(colorbox/controls.png) no-repeat 0px 0px; width:23px; height:23px; text-indent:-9999px;}
|
||||
#cboxPrevious{left:0px; background-position: -51px -25px;}
|
||||
#cboxPrevious:hover{background-position:-51px 0px;}
|
||||
#cboxNext{left:27px; background-position:-75px -25px;}
|
||||
#cboxNext:hover{background-position:-75px 0px;}
|
||||
#cboxClose{right:0; background-position:-100px -25px;}
|
||||
#cboxClose:hover{background-position:-100px 0px;}
|
||||
|
||||
.cboxSlideshow_on #cboxSlideshow{background-position:-125px 0px; right:27px;}
|
||||
.cboxSlideshow_on #cboxSlideshow:hover{background-position:-150px 0px;}
|
||||
.cboxSlideshow_off #cboxSlideshow{background-position:-150px -25px; right:27px;}
|
||||
.cboxSlideshow_off #cboxSlideshow:hover{background-position:-125px 0px;}
|
||||
#loading {
|
||||
position: fixed;
|
||||
left: 40%;
|
||||
top: 50%; }
|
||||
|
||||
a {
|
||||
color: #333333;
|
||||
text-decoration: none; }
|
||||
a:hover {
|
||||
color: black;
|
||||
text-decoration: underline; }
|
||||
|
||||
body {
|
||||
font-family: "Lucida Grande", Helvetica, "Helvetica Neue", Arial, sans-serif;
|
||||
padding: 12px;
|
||||
background-color: #333333; }
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
color: #1c2324;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 12px; }
|
||||
|
||||
table {
|
||||
width: 100%; }
|
||||
|
||||
#content {
|
||||
clear: left;
|
||||
background-color: white;
|
||||
border: 2px solid #dddddd;
|
||||
border-top: 8px solid #dddddd;
|
||||
padding: 18px;
|
||||
-webkit-border-bottom-left-radius: 5px;
|
||||
-webkit-border-bottom-right-radius: 5px;
|
||||
-webkit-border-top-right-radius: 5px;
|
||||
-moz-border-radius-bottomleft: 5px;
|
||||
-moz-border-radius-bottomright: 5px;
|
||||
-moz-border-radius-topright: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
border-top-right-radius: 5px; }
|
||||
|
||||
.dataTables_filter, .dataTables_info {
|
||||
padding: 2px 6px; }
|
||||
|
||||
abbr.timeago {
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
font-weight: bold; }
|
||||
|
||||
.timestamp {
|
||||
float: right;
|
||||
color: #dddddd; }
|
||||
|
||||
.group_tabs {
|
||||
list-style: none;
|
||||
float: left;
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
.group_tabs li {
|
||||
display: inline;
|
||||
float: left; }
|
||||
.group_tabs li a {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
display: block;
|
||||
float: left;
|
||||
text-decoration: none;
|
||||
padding: 4px 8px;
|
||||
background-color: #aaaaaa;
|
||||
background: -webkit-gradient(linear, 0 0, 0 bottom, from(#dddddd), to(#aaaaaa));
|
||||
background: -moz-linear-gradient(#dddddd, #aaaaaa);
|
||||
background: linear-gradient(#dddddd, #aaaaaa);
|
||||
text-shadow: #e5e5e5 1px 1px 0px;
|
||||
border-bottom: none;
|
||||
color: #333333;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
border-top: 1px solid #efefef;
|
||||
-webkit-border-top-left-radius: 2px;
|
||||
-webkit-border-top-right-radius: 2px;
|
||||
-moz-border-radius-topleft: 2px;
|
||||
-moz-border-radius-topright: 2px;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px; }
|
||||
.group_tabs li a:hover {
|
||||
background-color: #cccccc;
|
||||
background: -webkit-gradient(linear, 0 0, 0 bottom, from(#eeeeee), to(#aaaaaa));
|
||||
background: -moz-linear-gradient(#eeeeee, #aaaaaa);
|
||||
background: linear-gradient(#eeeeee, #aaaaaa); }
|
||||
.group_tabs li a:active {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 3px; }
|
||||
.group_tabs li.active a {
|
||||
color: black;
|
||||
text-shadow: white 1px 1px 0px;
|
||||
background-color: #dddddd;
|
||||
background: -webkit-gradient(linear, 0 0, 0 bottom, from(white), to(#dddddd));
|
||||
background: -moz-linear-gradient(white, #dddddd);
|
||||
background: linear-gradient(white, #dddddd); }
|
||||
|
||||
.file_list {
|
||||
margin-bottom: 18px; }
|
||||
|
||||
a.src_link {
|
||||
background: url("./magnify.png") no-repeat left 50%;
|
||||
padding-left: 18px; }
|
||||
|
||||
tr, td {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
th {
|
||||
white-space: nowrap; }
|
||||
th.ui-state-default {
|
||||
cursor: pointer; }
|
||||
th span.ui-icon {
|
||||
float: left; }
|
||||
|
||||
td {
|
||||
padding: 4px 8px; }
|
||||
td.strong {
|
||||
font-weight: bold; }
|
||||
|
||||
.source_table h3, .source_table h4 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-bottom: 4px; }
|
||||
.source_table .header {
|
||||
padding: 10px; }
|
||||
.source_table pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
color: black;
|
||||
font-family: "Monaco", "Inconsolata", "Consolas", monospace; }
|
||||
.source_table code {
|
||||
color: black;
|
||||
font-family: "Monaco", "Inconsolata", "Consolas", monospace; }
|
||||
.source_table pre {
|
||||
background-color: #333333; }
|
||||
.source_table pre ol {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
margin-left: 45px;
|
||||
font-size: 12px;
|
||||
color: white; }
|
||||
.source_table pre li {
|
||||
margin: 0px;
|
||||
padding: 2px 6px;
|
||||
border-left: 5px solid white; }
|
||||
.source_table pre li code {
|
||||
white-space: pre;
|
||||
white-space: pre-wrap; }
|
||||
.source_table pre .hits {
|
||||
float: right;
|
||||
margin-left: 10px;
|
||||
padding: 2px 4px;
|
||||
background-color: #444444;
|
||||
background: -webkit-gradient(linear, 0 0, 0 bottom, from(#222222), to(#666666));
|
||||
background: -moz-linear-gradient(#222222, #666666);
|
||||
background: linear-gradient(#222222, #666666);
|
||||
color: white;
|
||||
font-family: Helvetica, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-radius: 6px; }
|
||||
|
||||
#footer {
|
||||
color: #dddddd;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
text-align: right; }
|
||||
#footer a {
|
||||
color: #eeeeee;
|
||||
text-decoration: underline; }
|
||||
#footer a:hover {
|
||||
color: white;
|
||||
text-decoration: none; }
|
||||
|
||||
.green {
|
||||
color: #009900; }
|
||||
|
||||
.red {
|
||||
color: #990000; }
|
||||
|
||||
.yellow {
|
||||
color: #ddaa00; }
|
||||
|
||||
.source_table .covered {
|
||||
border-color: #009900; }
|
||||
.source_table .missed {
|
||||
border-color: #990000; }
|
||||
.source_table .never {
|
||||
border-color: black; }
|
||||
.source_table .skipped {
|
||||
border-color: #ffcc00; }
|
||||
.source_table .covered:nth-child(odd) {
|
||||
background-color: #cdf2cd; }
|
||||
.source_table .covered:nth-child(even) {
|
||||
background-color: #dbf2db; }
|
||||
.source_table .missed:nth-child(odd) {
|
||||
background-color: #f7c0c0; }
|
||||
.source_table .missed:nth-child(even) {
|
||||
background-color: #f7cfcf; }
|
||||
.source_table .never:nth-child(odd) {
|
||||
background-color: #efefef; }
|
||||
.source_table .never:nth-child(even) {
|
||||
background-color: #f4f4f4; }
|
||||
.source_table .skipped:nth-child(odd) {
|
||||
background-color: #fbf0c0; }
|
||||
.source_table .skipped:nth-child(even) {
|
||||
background-color: #fbffcf; }
|
||||
|
||||
|
||||
|
||||
1707
coverage/assets/0.10.2/application.js
Normal file
BIN
coverage/assets/0.10.2/colorbox/border.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
coverage/assets/0.10.2/colorbox/controls.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
coverage/assets/0.10.2/colorbox/loading.gif
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
coverage/assets/0.10.2/colorbox/loading_background.png
Normal file
|
After Width: | Height: | Size: 166 B |
BIN
coverage/assets/0.10.2/favicon_green.png
Normal file
|
After Width: | Height: | Size: 1009 B |
BIN
coverage/assets/0.10.2/favicon_red.png
Normal file
|
After Width: | Height: | Size: 1009 B |
BIN
coverage/assets/0.10.2/favicon_yellow.png
Normal file
|
After Width: | Height: | Size: 1009 B |
BIN
coverage/assets/0.10.2/loading.gif
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
coverage/assets/0.10.2/magnify.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 180 B |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 120 B |
|
After Width: | Height: | Size: 105 B |
|
After Width: | Height: | Size: 111 B |
|
After Width: | Height: | Size: 110 B |
|
After Width: | Height: | Size: 119 B |
|
After Width: | Height: | Size: 101 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
1605
coverage/index.html
Normal file
26
lib/grammar.y
Normal file
@@ -0,0 +1,26 @@
|
||||
class Query
|
||||
prechigh
|
||||
left OPERATOR_NOT
|
||||
left OPERATOR_AND
|
||||
left OPERATOR_OR
|
||||
preclow
|
||||
rule
|
||||
target: expression
|
||||
| /* none */ { result = 0 }
|
||||
|
||||
expression: TERM_WITHOUT_QUOTES { result = {:DEFAULT_COLUMN => val[0]} }
|
||||
| TERM_WITH_QUOTES { result = {:DEFAULT_COLUMN => val[0]} }
|
||||
| TERM_WITHOUT_QUOTES COLON TERM_WITHOUT_QUOTES { result = {val[0] => val[2]} }
|
||||
| TERM_WITHOUT_QUOTES COLON TERM_WITH_QUOTES { result = {val[0] => val[2]} }
|
||||
| expression OPERATOR_OR expression { result = {:OPERATOR_OR => [val[0], val[2]]} }
|
||||
| expression OPERATOR_AND expression { result = {:OPERATOR_AND => [val[0], val[2]]} }
|
||||
| L_BRACKET expression R_BRACKET { result = val[1] }
|
||||
end
|
||||
|
||||
---- header
|
||||
require_relative 'lexer'
|
||||
|
||||
---- inner
|
||||
def parse(input)
|
||||
scan_str(input)
|
||||
end
|
||||
4
lib/parser-parser-part/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.idea
|
||||
|
||||
lexer.rb
|
||||
parser.rb
|
||||
19
lib/parser-parser-part/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# parser
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* Rexical (rex)
|
||||
* Racc
|
||||
|
||||
### Available commands
|
||||
|
||||
* `rake lexer` - generates `lexer.rb` file based on `specification.rex` file
|
||||
* `rake parser` - generates `parser.rb` file based on `grammar.y` file
|
||||
* `rake generate` - generates `lexer.rb` and `parser.rb` files
|
||||
|
||||
### Testing
|
||||
|
||||
To run only `lexer` tests, execute : `rspec spec/query_lexer_spec.rb`
|
||||
To run only `parser` tests, execute : `rspec spec/query_parser_spec.rb`
|
||||
|
||||
To run all tests, execute : `rake spec`
|
||||
20
lib/parser-parser-part/Rakefile
Normal file
@@ -0,0 +1,20 @@
|
||||
require 'rspec/core/rake_task'
|
||||
|
||||
RSpec::Core::RakeTask.new do |c|
|
||||
options = ['--color']
|
||||
options += %w[--format documentation]
|
||||
c.rspec_opts = options
|
||||
end
|
||||
|
||||
desc 'Generate Lexer'
|
||||
task :lexer do
|
||||
`rex specification.rex -o lexer.rb`
|
||||
end
|
||||
|
||||
desc 'Generate Parser'
|
||||
task :parser do
|
||||
`racc grammar.y -o parser.rb`
|
||||
end
|
||||
|
||||
desc 'Generate Lexer and Parser'
|
||||
task generate: %i[lexer parser]
|
||||
26
lib/parser-parser-part/grammar.y
Normal file
@@ -0,0 +1,26 @@
|
||||
class Query
|
||||
prechigh
|
||||
left OPERATOR_NOT
|
||||
left OPERATOR_AND
|
||||
left OPERATOR_OR
|
||||
preclow
|
||||
rule
|
||||
target: expression
|
||||
| /* none */ { result = 0 }
|
||||
|
||||
expression: TERM_WITHOUT_QUOTES { result = {:DEFAULT_COLUMN => val[0]} }
|
||||
| TERM_WITH_QUOTES { result = {:DEFAULT_COLUMN => val[0]} }
|
||||
| TERM_WITHOUT_QUOTES COLON TERM_WITHOUT_QUOTES { result = {val[0] => val[2]} }
|
||||
| TERM_WITHOUT_QUOTES COLON TERM_WITH_QUOTES { result = {val[0] => val[2]} }
|
||||
| expression OPERATOR_OR expression { result = {:OPERATOR_OR => [val[0], val[2]]} }
|
||||
| expression OPERATOR_AND expression { result = {:OPERATOR_AND => [val[0], val[2]]} }
|
||||
| L_BRACKET expression R_BRACKET { result = val[1] }
|
||||
end
|
||||
|
||||
---- header
|
||||
require_relative 'lexer'
|
||||
|
||||
---- inner
|
||||
def parse(input)
|
||||
scan_str(input)
|
||||
end
|
||||
259
lib/parser-parser-part/spec/query_lexer_spec.rb
Normal file
@@ -0,0 +1,259 @@
|
||||
require './lexer'
|
||||
|
||||
class QueryLexerTester
|
||||
describe 'Testing the Lexer' do
|
||||
before do
|
||||
@evaluator = Query.new
|
||||
end
|
||||
|
||||
it 'tests bracket expression' do
|
||||
@result = @evaluator.tokenize('()')
|
||||
expect(@result.length).to eq 2
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests bracket expression with spaces' do
|
||||
@result = @evaluator.tokenize(' ( ) ')
|
||||
expect(@result.length).to eq 2
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with OR operator' do
|
||||
@result = @evaluator.tokenize('() or () OR ()')
|
||||
expect(@result.length).to eq 8
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_OR
|
||||
expect(@result[3][0]).to eq :L_BRACKET
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_OR
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with AND operator' do
|
||||
@result = @evaluator.tokenize('() AND () and ()')
|
||||
expect(@result.length).to eq 8
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_AND
|
||||
expect(@result[3][0]).to eq :L_BRACKET
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_AND
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with NOT OR and NOT AND operator' do
|
||||
@result = @evaluator.tokenize('() NOT or () not AND ()')
|
||||
expect(@result.length).to eq 10
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[3][0]).to eq :OPERATOR_OR
|
||||
expect(@result[4][0]).to eq :L_BRACKET
|
||||
expect(@result[5][0]).to eq :R_BRACKET
|
||||
expect(@result[6][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[7][0]).to eq :OPERATOR_AND
|
||||
expect(@result[8][0]).to eq :L_BRACKET
|
||||
expect(@result[9][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests search term under quotes' do
|
||||
@result = @evaluator.tokenize('"123-456"')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"123-456"'
|
||||
end
|
||||
|
||||
it 'tests term without quotes' do
|
||||
@result = @evaluator.tokenize('device_id')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'device_id'
|
||||
end
|
||||
|
||||
it 'tests integer term without quotes' do
|
||||
@result = @evaluator.tokenize('123')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq '123'
|
||||
end
|
||||
|
||||
it 'tests multiple terms without quotes' do
|
||||
@result = @evaluator.tokenize('device_id tag 123-456 name123')
|
||||
|
||||
expect(@result.length).to eq 4
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'device_id'
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'tag'
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq '123-456'
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'name123'
|
||||
end
|
||||
|
||||
it 'tests simple query with column name and search term without quotes' do
|
||||
@result = @evaluator.tokenize('name:JF')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'JF'
|
||||
end
|
||||
|
||||
it 'tests simple query with two columns with name and search terms without quotes' do
|
||||
@result = @evaluator.tokenize('name:JF tag:mta')
|
||||
|
||||
expect(@result.length).to eq 6
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'JF'
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'tag'
|
||||
expect(@result[4][0]).to eq :COLON
|
||||
expect(@result[5][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[5][1]).to eq 'mta'
|
||||
|
||||
end
|
||||
|
||||
it 'tests simple query with column name and search term with quotes' do
|
||||
@result = @evaluator.tokenize('name:"name with space"')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[2][1]).to eq '"name with space"'
|
||||
end
|
||||
|
||||
it 'tests search term with quotes containing non alphanumerical characters' do
|
||||
@result = @evaluator.tokenize('"|*|/\()#-!=<>&$"')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"|*|/\()#-!=<>&$"'
|
||||
end
|
||||
|
||||
it 'tests simple query in brackets' do
|
||||
@result = @evaluator.tokenize('(name:"name with space")')
|
||||
|
||||
expect(@result.length).to eq 5
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'name'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"name with space"'
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests multiple query wtih brackets' do
|
||||
@result = @evaluator.tokenize('(name:"name with space") or (tag:mta)')
|
||||
|
||||
expect(@result.length).to eq 11
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'name'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"name with space"'
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_OR
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[7][1]).to eq 'tag'
|
||||
expect(@result[8][0]).to eq :COLON
|
||||
expect(@result[9][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[9][1]).to eq 'mta'
|
||||
expect(@result[10][0]).to eq :R_BRACKET
|
||||
|
||||
end
|
||||
|
||||
it 'tests complex query' do
|
||||
@result = @evaluator.tokenize('(device-id:"with space" tag:mta no-quotes-id-123)'\
|
||||
'or "id with quotes-5" and ( ("id with q 10" or "id with q 20")'\
|
||||
'and ("id with Q 30" "id with Q 40") and not id-without-Q-50)')
|
||||
|
||||
expect(@result.length).to eq 27
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'device-id'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"with space"'
|
||||
expect(@result[4][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[4][1]).to eq 'tag'
|
||||
expect(@result[5][0]).to eq :COLON
|
||||
expect(@result[6][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[6][1]).to eq 'mta'
|
||||
expect(@result[7][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[7][1]).to eq 'no-quotes-id-123'
|
||||
expect(@result[8][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[9][0]).to eq :OPERATOR_OR
|
||||
expect(@result[10][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[10][1]).to eq '"id with quotes-5"'
|
||||
expect(@result[11][0]).to eq :OPERATOR_AND
|
||||
|
||||
expect(@result[12][0]).to eq :L_BRACKET
|
||||
expect(@result[13][0]).to eq :L_BRACKET
|
||||
expect(@result[14][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[14][1]).to eq '"id with q 10"'
|
||||
expect(@result[15][0]).to eq :OPERATOR_OR
|
||||
expect(@result[16][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[16][1]).to eq '"id with q 20"'
|
||||
expect(@result[17][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[18][0]).to eq :OPERATOR_AND
|
||||
expect(@result[19][0]).to eq :L_BRACKET
|
||||
expect(@result[20][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[20][1]).to eq '"id with Q 30"'
|
||||
expect(@result[21][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[21][1]).to eq '"id with Q 40"'
|
||||
expect(@result[22][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[23][0]).to eq :OPERATOR_AND
|
||||
expect(@result[24][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[25][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[25][1]).to eq 'id-without-Q-50'
|
||||
expect(@result[26][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests query with -or-, -and- and -not- words inside quoted expression' do
|
||||
@result = @evaluator.tokenize('tag:"tag with or and not inside"')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'tag'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[2][1]).to eq '"tag with or and not inside"'
|
||||
end
|
||||
end
|
||||
end
|
||||
164
lib/parser-parser-part/spec/query_parser_spec.rb
Normal file
@@ -0,0 +1,164 @@
|
||||
require './parser'
|
||||
|
||||
class QueryParserTester
|
||||
describe 'Testing the Parser' do
|
||||
before do
|
||||
@evaluator = Query.new
|
||||
end
|
||||
|
||||
it 'tests query with only one search term without quotes and without column name' do
|
||||
@result = @evaluator.parse('-123')
|
||||
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '-123'
|
||||
end
|
||||
|
||||
it 'tests query with only one search term with quotes and without column name' do
|
||||
@result = @evaluator.parse('"OR 128"')
|
||||
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '"OR 128"'
|
||||
end
|
||||
|
||||
it 'tests query with one column and search term without quotes' do
|
||||
@result = @evaluator.parse('tag:mta')
|
||||
|
||||
expect(@result['tag']).to eq 'mta'
|
||||
end
|
||||
|
||||
it 'tests query with one column and search term with quotes' do
|
||||
@result = @evaluator.parse('tag:"tag 120"')
|
||||
|
||||
expect(@result['tag']).to eq '"tag 120"'
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with OR and search terms without quotes' do
|
||||
@result = @evaluator.parse('tag:mta OR tag:12')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '12' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with OR and search terms with quotes' do
|
||||
@result = @evaluator.parse('tag:mta OR tag:"tag 12"')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '"tag 12"' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with AND and search terms without quotes' do
|
||||
@result = @evaluator.parse('tag:mta AND tag:12')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '12' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with AND and search terms with quotes' do
|
||||
@result = @evaluator.parse('tag:mta and tag:"tag 12"')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '"tag 12"' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests simple query with brackets' do
|
||||
@result = @evaluator.parse('(123)')
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '123'
|
||||
end
|
||||
|
||||
it 'tests simple query with brackets and with a column name' do
|
||||
@result = @evaluator.parse('(name:JF)')
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result['name']).to eq 'JF'
|
||||
end
|
||||
|
||||
it 'tests query with OR operator in brackets' do
|
||||
@result = @evaluator.parse('(name:JF or tag:mta)')
|
||||
|
||||
@expected_array = [
|
||||
{ 'name' => 'JF' },
|
||||
{ 'tag' => 'mta' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two simple brackets expressions' do
|
||||
@result = @evaluator.parse('(name:JF) and (-456)')
|
||||
|
||||
@expected_array = [
|
||||
{ 'name' => 'JF' },
|
||||
{ :DEFAULT_COLUMN => '-456' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two brackets expressions' do
|
||||
@result = @evaluator.parse('(name:JF or tag:"tag 0") and (-456)')
|
||||
|
||||
@expected_array_part_1 = [
|
||||
{ 'name' => 'JF' },
|
||||
{ 'tag' => '"tag 0"' }
|
||||
]
|
||||
|
||||
@expected_array_total = [
|
||||
{:OPERATOR_OR => @expected_array_part_1},
|
||||
{:DEFAULT_COLUMN => '-456'}
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array_total
|
||||
end
|
||||
|
||||
it 'tests operator precedence' do
|
||||
@result1 = @evaluator.parse('tag:mta or name:JF and 12_4')
|
||||
@result2 = @evaluator.parse('tag:mta or (name:JF and 12_4)')
|
||||
|
||||
expect(@result1).to eq @result2
|
||||
|
||||
expect(@result1.length).to eq 1
|
||||
|
||||
@expected_array_part_2 = [
|
||||
{'name' => 'JF'},
|
||||
{:DEFAULT_COLUMN => '12_4'}
|
||||
]
|
||||
|
||||
@expected_array_total = [
|
||||
{'tag' => 'mta'},
|
||||
{:OPERATOR_AND => @expected_array_part_2}
|
||||
]
|
||||
|
||||
expect(@result1[:OPERATOR_OR]).to eq @expected_array_total
|
||||
|
||||
end
|
||||
|
||||
# Tests to write :
|
||||
# * query with multiple column names and search terms without logical operators
|
||||
# * AND NOT, OR NOT tests
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
35
lib/parser-parser-part/specification.rex
Normal file
@@ -0,0 +1,35 @@
|
||||
class Query
|
||||
macro
|
||||
L_BRACKET \(
|
||||
R_BRACKET \)
|
||||
SPACE \ + # Space char
|
||||
OPERATOR_OR (?i)or
|
||||
OPERATOR_AND (?i)and
|
||||
OPERATOR_NOT (?i)not
|
||||
TERM_WITH_QUOTES "([^"]*)"
|
||||
TERM_WITHOUT_QUOTES [a-zA-Z0-9-_]+
|
||||
COLON \:
|
||||
|
||||
|
||||
|
||||
rule
|
||||
{SPACE} # No action
|
||||
{L_BRACKET} { return [:L_BRACKET, text] }
|
||||
{R_BRACKET} { return [:R_BRACKET, text] }
|
||||
{OPERATOR_OR} { return [:OPERATOR_OR, text] }
|
||||
{OPERATOR_AND} { return [:OPERATOR_AND, text] }
|
||||
{OPERATOR_NOT} { return [:OPERATOR_NOT, text] }
|
||||
{TERM_WITH_QUOTES} { return [:TERM_WITH_QUOTES, text] }
|
||||
{TERM_WITHOUT_QUOTES} { return [:TERM_WITHOUT_QUOTES, text] }
|
||||
{COLON} { return [:COLON, text] }
|
||||
|
||||
inner
|
||||
def tokenize(code)
|
||||
scan_setup(code)
|
||||
tokens = []
|
||||
while token = next_token
|
||||
tokens << token
|
||||
end
|
||||
tokens
|
||||
end
|
||||
end
|
||||
76
lib/pg_searchable_regex.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'active_support'
|
||||
require 'squeel'
|
||||
require_relative './text_to_tsquery'
|
||||
require_relative './text_to_regex_query'
|
||||
|
||||
module PgSearchable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
def update_pg_search_cache
|
||||
# kept just for compatibility with pg_searchable
|
||||
# noop in this implementation
|
||||
end
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def pg_search(
|
||||
fields: [],
|
||||
fields_mappings: {},
|
||||
cache: nil,
|
||||
language: 'english',
|
||||
scope: 'scope_search',
|
||||
skip_callback: false,
|
||||
wildcard: true,
|
||||
external_cache_data: nil,
|
||||
joins: [],
|
||||
default_field: ""
|
||||
)
|
||||
@ts_search_fields = fields
|
||||
@ts_search_fields_mappings = fields_mappings
|
||||
@ts_cache_field = cache
|
||||
@ts_language = language
|
||||
@ts_scope_method = scope
|
||||
@ts_skip_cache_update = skip_callback
|
||||
@ts_wildcard = wildcard
|
||||
@ts_joins = joins
|
||||
@default_field = (default_field.to_sym || fields.first)
|
||||
ts_add_scope
|
||||
end
|
||||
|
||||
def ts_add_scope
|
||||
class_eval do
|
||||
scope ts_scope_method, ->(value) { ts_search(value) }
|
||||
end
|
||||
end
|
||||
|
||||
def ts_search(value)
|
||||
return if @ts_search_fields.blank? || value.blank?
|
||||
TextToRegexQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause( includes(@ts_joins).references(:all))
|
||||
end
|
||||
|
||||
def should_update_cache_field?
|
||||
!@ts_skip_cache_update && @ts_cache_field.present?
|
||||
end
|
||||
|
||||
def ts_cache_field
|
||||
@ts_cache_field
|
||||
end
|
||||
|
||||
def ts_scope_method
|
||||
@ts_scope_method
|
||||
end
|
||||
|
||||
def ts_cache_method
|
||||
@ts_cache_method
|
||||
end
|
||||
|
||||
def ts_fields_to_vector(extra_data = [])
|
||||
field_to_vector = ->(field) { "to_tsvector('#{@ts_language}', coalesce(#{field}::text, ''))" }
|
||||
data_to_vector = ->(data) { "to_tsvector('#{@ts_language}', '#{data}')" }
|
||||
(@ts_search_fields.map(&field_to_vector) + extra_data.map(&data_to_vector)).join(' || ')
|
||||
end
|
||||
end
|
||||
end
|
||||
35
lib/specification.rex
Normal file
@@ -0,0 +1,35 @@
|
||||
class Query
|
||||
macro
|
||||
L_BRACKET \(
|
||||
R_BRACKET \)
|
||||
SPACE \ + # Space char
|
||||
OPERATOR_OR (?i)or
|
||||
OPERATOR_AND (?i)and
|
||||
OPERATOR_NOT (?i)not
|
||||
TERM_WITH_QUOTES "([^"]*)"
|
||||
TERM_WITHOUT_QUOTES [a-zA-Z0-9-_]+
|
||||
COLON \:
|
||||
|
||||
|
||||
|
||||
rule
|
||||
{SPACE} # No action
|
||||
{L_BRACKET} { return [:L_BRACKET, text] }
|
||||
{R_BRACKET} { return [:R_BRACKET, text] }
|
||||
{OPERATOR_OR} { return [:OPERATOR_OR, text] }
|
||||
{OPERATOR_AND} { return [:OPERATOR_AND, text] }
|
||||
{OPERATOR_NOT} { return [:OPERATOR_NOT, text] }
|
||||
{TERM_WITH_QUOTES} { return [:TERM_WITH_QUOTES, text] }
|
||||
{TERM_WITHOUT_QUOTES} { return [:TERM_WITHOUT_QUOTES, text] }
|
||||
{COLON} { return [:COLON, text] }
|
||||
|
||||
inner
|
||||
def tokenize(code)
|
||||
scan_setup(code)
|
||||
tokens = []
|
||||
while token = next_token
|
||||
tokens << token
|
||||
end
|
||||
tokens
|
||||
end
|
||||
end
|
||||
67
lib/text_to_regex_query.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# transforms "english like" text queries into a where clause with regex
|
||||
# https://www.postgresql.org/docs/9.5/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
|
||||
|
||||
class TextToRegexQuery
|
||||
def initialize(text, fields, default_field, fields_mappings = {})
|
||||
@text = text.to_s.strip
|
||||
@fields = fields.map(&:to_sym)
|
||||
@default_field = default_field.to_sym
|
||||
@fields_mappings = fields_mappings.merge(@fields.reduce({}) do |mappings, field|
|
||||
table_name, field_name = field.to_s.split(".")
|
||||
mappings[field_name.to_sym] = field
|
||||
mappings
|
||||
end)
|
||||
end
|
||||
|
||||
def where_clause(query)
|
||||
@cleared_text = @text.dup
|
||||
@column_chunks = []
|
||||
remove_duplicated_spaces
|
||||
extract_columns
|
||||
escape_special_characters
|
||||
generate_where_clause(query)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove_duplicated_spaces
|
||||
@cleared_text.gsub!(/\s+/, ' ')
|
||||
end
|
||||
|
||||
def escape_special_characters
|
||||
@cleared_text.gsub!(/\_/, '\_')
|
||||
@cleared_text.tr!('\\', '\\')
|
||||
@cleared_text.gsub!(/%/, '\%')
|
||||
end
|
||||
|
||||
def extract_columns
|
||||
column_search_term_pairs = @cleared_text.scan(/([A-Za-z0-9_]+:[\w\_-]+)/)
|
||||
|
||||
@column_chunks = (column_search_term_pairs.flatten.map do |pair|
|
||||
column, term = pair.split(':')
|
||||
next unless @fields_mappings.include?(column.to_sym)
|
||||
@cleared_text.gsub!(pair, '')
|
||||
{ @fields_mappings[column.to_sym] => term }
|
||||
end).compact
|
||||
unless @cleared_text.strip.empty?
|
||||
@column_chunks = [{ @default_field.to_s => @cleared_text.strip }] + @column_chunks
|
||||
end
|
||||
@column_chunks
|
||||
end
|
||||
|
||||
def generate_where_clause(query)
|
||||
where_clause = ''
|
||||
columns = @column_chunks.map { |c| c.keys.first }
|
||||
values = @column_chunks.map { |c| c.values.first }
|
||||
|
||||
columns.each do |column|
|
||||
quoted_column = '"' + column.to_s.gsub(".",'"."') + '"'
|
||||
where_clause += "#{quoted_column} ILIKE ? OR "
|
||||
end
|
||||
where_clause += " 1<>1 "
|
||||
regexed_values = values.map { |v| "%#{v}%" }
|
||||
query.where([where_clause] + regexed_values)
|
||||
end
|
||||
end
|
||||
99
lib/text_to_tsquery.rb
Normal file
@@ -0,0 +1,99 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# transforms "english like" text queries into a tsquery operation
|
||||
# https://www.postgresql.org/docs/9.5/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
|
||||
class TextToTsquery
|
||||
attr_reader :text
|
||||
|
||||
def initialize(text, wildcard: true)
|
||||
@text = text.to_s.strip
|
||||
@wildcard = wildcard
|
||||
@exact_matches = []
|
||||
validate!
|
||||
end
|
||||
|
||||
EXACT_WORD_CHAR = '§'.freeze
|
||||
|
||||
def tsquery
|
||||
@tsquery = @text
|
||||
strip_exact_words
|
||||
remove_duplicated_spaces
|
||||
transform_or_into_operator
|
||||
transform_and_into_operator
|
||||
strip_spaces_from_parenthesis
|
||||
transform_remaining_spaces_into_and_operator
|
||||
transform_keywords
|
||||
join_operators_with_and
|
||||
remove_partial_match_from_not_keywords
|
||||
add_exact_words
|
||||
@tsquery
|
||||
end
|
||||
|
||||
def validate!
|
||||
parenthesis_error unless self.class.valid_search_parenthesis?(@text)
|
||||
end
|
||||
|
||||
def self.valid_search_parenthesis?(text)
|
||||
text.split('').reduce(0) do |acc, char|
|
||||
return false if acc < 0
|
||||
|
||||
if char == '('
|
||||
acc + 1
|
||||
elsif char == ')'
|
||||
acc - 1
|
||||
else
|
||||
acc
|
||||
end
|
||||
end.zero?
|
||||
end
|
||||
|
||||
def parenthesis_error
|
||||
raise ArgumentError, "incorrect number/order of parenthesis in search query: '#{@text}'"
|
||||
end
|
||||
|
||||
def strip_exact_words
|
||||
@exact_matches << Regexp.last_match(1) while @tsquery.sub!(/"(.*?)"/, EXACT_WORD_CHAR)
|
||||
end
|
||||
|
||||
def remove_duplicated_spaces
|
||||
@tsquery = @tsquery.gsub(/\s+/, ' ')
|
||||
end
|
||||
|
||||
# transforms or/OR/|/|| into | operator
|
||||
def transform_or_into_operator
|
||||
@tsquery = @tsquery.gsub(/ ((or|\|+) )+/i, '|').gsub(/ *\|+ */, '|')
|
||||
end
|
||||
|
||||
# transforms and/AND/&/&& into & operator
|
||||
def transform_and_into_operator
|
||||
@tsquery = @tsquery.gsub(/ ((and|\&+) )+/i, '&')
|
||||
end
|
||||
|
||||
def strip_spaces_from_parenthesis
|
||||
@tsquery = @tsquery.gsub(/ *([()]) */, '\1')
|
||||
end
|
||||
|
||||
def transform_remaining_spaces_into_and_operator
|
||||
@tsquery = @tsquery.tr(' ', '&')
|
||||
end
|
||||
|
||||
# adds :* for partial match of words
|
||||
def transform_keywords
|
||||
keyword = @wildcard ? '\1:*' : '\1:'
|
||||
@tsquery = @tsquery.gsub(/([^#{EXACT_WORD_CHAR}|&!())]+)/, keyword)
|
||||
end
|
||||
|
||||
# adds & between operations
|
||||
def join_operators_with_and
|
||||
@tsquery = @tsquery.gsub(/:(\**)\!/, ':\1&!').gsub(/:(\**)\(/, ':\1&(').gsub(/\&+/, '&')
|
||||
end
|
||||
|
||||
# removes partial match from NOT operations
|
||||
def remove_partial_match_from_not_keywords
|
||||
@tsquery = @tsquery.gsub(/\!([^|&!())]+):\**/, '!\1')
|
||||
end
|
||||
|
||||
def add_exact_words
|
||||
@exact_matches.each { |phrase| @tsquery = @tsquery.sub(EXACT_WORD_CHAR, "'#{phrase}'") }
|
||||
end
|
||||
end
|
||||
BIN
pkg/pg_searchable_regex-1.0.13.gem
Normal file
115
spec/lib/pg_searchable_spec.rb
Normal file
@@ -0,0 +1,115 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe PgSearchable do
|
||||
include_examples 'pg_search', VectorModel
|
||||
include_examples 'pg_search', VectorWithCustomPrimaryKeyModel
|
||||
include_examples 'pg_search', VectorWithCustomCallback
|
||||
include_examples 'pg_search', SimpleVectorModel
|
||||
include_examples 'pg_search', VectorWithoutWildcardModel
|
||||
include_examples 'pg_search', VectorModelWithCustomSearchScope, 'fulltext'
|
||||
include_examples 'pg_search', VectorModelWithTagValues
|
||||
include_examples 'pg_search', DynamicModel
|
||||
include_examples 'pg_search', DynamicModelWithTagValues
|
||||
include_examples 'pg_search', DynamicModelWithCategory
|
||||
include_examples 'pg_search', DynamicModelWithSectionsTrhough
|
||||
|
||||
describe 'pg_search' do
|
||||
describe 'joins' do
|
||||
it 'can dynamically query compound relation' do
|
||||
record = DynamicModelWithCategory.create(name: 'something', value: 'amazing')
|
||||
category = Category.create(name: 'searchable')
|
||||
Tag.create(value: 'impressive', category: category, taggable: record)
|
||||
expect(DynamicModelWithCategory.scope_search('searchable')).to include(record)
|
||||
end
|
||||
|
||||
it 'can use has_many :through relation' do
|
||||
record = DynamicModelWithSectionsTrhough.create(name: 'something', value: 'amazing')
|
||||
tag = Tag.create(value: 'impressive', taggable: record)
|
||||
Section.create(name: 'searchable', tag: tag)
|
||||
expect(DynamicModelWithSectionsTrhough.scope_search('searchable')).to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'properties' do
|
||||
describe 'skip_callback' do
|
||||
context 'when enabled' do
|
||||
let(:record) { VectorModel.create(name: 'something', value: 'amazing') }
|
||||
|
||||
it 'can find the record after it updates' do
|
||||
record.update(name: 'cookie')
|
||||
expect(VectorModel.scope_search('cookie')).to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when disabled' do
|
||||
let(:record) { VectorModelWithoutCallback.create(name: 'something', value: 'amazing') }
|
||||
|
||||
it 'cannot find the record after it updates' do
|
||||
record.update(name: 'cookie')
|
||||
expect(VectorModelWithoutCallback.scope_search('cookie')).not_to include(record)
|
||||
end
|
||||
|
||||
it 'can find the record after manually calling .update_pg_search_cache' do
|
||||
record.update(name: 'cookie')
|
||||
record.update_pg_search_cache
|
||||
expect(VectorModelWithoutCallback.scope_search('cookie')).to include(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'scope' do
|
||||
it 'defaults to "scope_search"' do
|
||||
expect(VectorModel).to respond_to(:scope_search)
|
||||
end
|
||||
|
||||
it 'can use a different scope name' do
|
||||
expect(VectorModelWithCustomSearchScope).to respond_to(:fulltext)
|
||||
end
|
||||
|
||||
it 'doesnt pollutes the default method name if customized' do
|
||||
expect(VectorModelWithCustomSearchScope).not_to respond_to(:scope_search)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'language' do
|
||||
it 'defaults to english lexemes' do
|
||||
record = VectorModel.create name: 'something', value: 'amazing'
|
||||
expect(VectorModel.scope_search('amaz')).to include(record)
|
||||
end
|
||||
|
||||
it 'can be changed to simple to avoid lexeme truncation' do
|
||||
record = SimpleVectorModel.create name: 'something', value: 'amazing'
|
||||
expect(SimpleVectorModel.scope_search('amazings')).not_to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'wildcard' do
|
||||
it 'by default uses it' do
|
||||
record = VectorModel.create name: '12345', value: 'amazing'
|
||||
expect(VectorModel.scope_search('123')).to include(record)
|
||||
end
|
||||
|
||||
it 'can be set to false' do
|
||||
record = VectorWithoutWildcardModel.create name: '12345', value: 'amazing'
|
||||
expect(VectorWithoutWildcardModel.scope_search('123')).not_to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'tags' do
|
||||
it 'allow indexing fields of other associations' do
|
||||
record = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
Tag.create(taggable: record, value: 'red')
|
||||
expect(DynamicModelWithTagValues.scope_search('red')).to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'external_cache_data' do
|
||||
it 'can index external data using a method' do
|
||||
record = VectorModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
Tag.create(taggable: record, value: 'red')
|
||||
expect(VectorModelWithTagValues.scope_search('red')).to include(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
259
spec/lib/query_lexer_spec.rb
Normal file
@@ -0,0 +1,259 @@
|
||||
require './lexer'
|
||||
|
||||
class QueryLexerTester
|
||||
describe 'Testing the Lexer' do
|
||||
before do
|
||||
@evaluator = Query.new
|
||||
end
|
||||
|
||||
it 'tests bracket expression' do
|
||||
@result = @evaluator.tokenize('()')
|
||||
expect(@result.length).to eq 2
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests bracket expression with spaces' do
|
||||
@result = @evaluator.tokenize(' ( ) ')
|
||||
expect(@result.length).to eq 2
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with OR operator' do
|
||||
@result = @evaluator.tokenize('() or () OR ()')
|
||||
expect(@result.length).to eq 8
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_OR
|
||||
expect(@result[3][0]).to eq :L_BRACKET
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_OR
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with AND operator' do
|
||||
@result = @evaluator.tokenize('() AND () and ()')
|
||||
expect(@result.length).to eq 8
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_AND
|
||||
expect(@result[3][0]).to eq :L_BRACKET
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_AND
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with NOT OR and NOT AND operator' do
|
||||
@result = @evaluator.tokenize('() NOT or () not AND ()')
|
||||
expect(@result.length).to eq 10
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[3][0]).to eq :OPERATOR_OR
|
||||
expect(@result[4][0]).to eq :L_BRACKET
|
||||
expect(@result[5][0]).to eq :R_BRACKET
|
||||
expect(@result[6][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[7][0]).to eq :OPERATOR_AND
|
||||
expect(@result[8][0]).to eq :L_BRACKET
|
||||
expect(@result[9][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests search term under quotes' do
|
||||
@result = @evaluator.tokenize('"123-456"')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"123-456"'
|
||||
end
|
||||
|
||||
it 'tests term without quotes' do
|
||||
@result = @evaluator.tokenize('device_id')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'device_id'
|
||||
end
|
||||
|
||||
it 'tests integer term without quotes' do
|
||||
@result = @evaluator.tokenize('123')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq '123'
|
||||
end
|
||||
|
||||
it 'tests multiple terms without quotes' do
|
||||
@result = @evaluator.tokenize('device_id tag 123-456 name123')
|
||||
|
||||
expect(@result.length).to eq 4
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'device_id'
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'tag'
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq '123-456'
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'name123'
|
||||
end
|
||||
|
||||
it 'tests simple query with column name and search term without quotes' do
|
||||
@result = @evaluator.tokenize('name:JF')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'JF'
|
||||
end
|
||||
|
||||
it 'tests simple query with two columns with name and search terms without quotes' do
|
||||
@result = @evaluator.tokenize('name:JF tag:mta')
|
||||
|
||||
expect(@result.length).to eq 6
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'JF'
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'tag'
|
||||
expect(@result[4][0]).to eq :COLON
|
||||
expect(@result[5][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[5][1]).to eq 'mta'
|
||||
|
||||
end
|
||||
|
||||
it 'tests simple query with column name and search term with quotes' do
|
||||
@result = @evaluator.tokenize('name:"name with space"')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[2][1]).to eq '"name with space"'
|
||||
end
|
||||
|
||||
it 'tests search term with quotes containing non alphanumerical characters' do
|
||||
@result = @evaluator.tokenize('"|*|/\()#-!=<>&$"')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"|*|/\()#-!=<>&$"'
|
||||
end
|
||||
|
||||
it 'tests simple query in brackets' do
|
||||
@result = @evaluator.tokenize('(name:"name with space")')
|
||||
|
||||
expect(@result.length).to eq 5
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'name'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"name with space"'
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests multiple query wtih brackets' do
|
||||
@result = @evaluator.tokenize('(name:"name with space") or (tag:mta)')
|
||||
|
||||
expect(@result.length).to eq 11
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'name'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"name with space"'
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_OR
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[7][1]).to eq 'tag'
|
||||
expect(@result[8][0]).to eq :COLON
|
||||
expect(@result[9][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[9][1]).to eq 'mta'
|
||||
expect(@result[10][0]).to eq :R_BRACKET
|
||||
|
||||
end
|
||||
|
||||
it 'tests complex query' do
|
||||
@result = @evaluator.tokenize('(device-id:"with space" tag:mta no-quotes-id-123)'\
|
||||
'or "id with quotes-5" and ( ("id with q 10" or "id with q 20")'\
|
||||
'and ("id with Q 30" "id with Q 40") and not id-without-Q-50)')
|
||||
|
||||
expect(@result.length).to eq 27
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'device-id'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"with space"'
|
||||
expect(@result[4][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[4][1]).to eq 'tag'
|
||||
expect(@result[5][0]).to eq :COLON
|
||||
expect(@result[6][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[6][1]).to eq 'mta'
|
||||
expect(@result[7][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[7][1]).to eq 'no-quotes-id-123'
|
||||
expect(@result[8][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[9][0]).to eq :OPERATOR_OR
|
||||
expect(@result[10][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[10][1]).to eq '"id with quotes-5"'
|
||||
expect(@result[11][0]).to eq :OPERATOR_AND
|
||||
|
||||
expect(@result[12][0]).to eq :L_BRACKET
|
||||
expect(@result[13][0]).to eq :L_BRACKET
|
||||
expect(@result[14][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[14][1]).to eq '"id with q 10"'
|
||||
expect(@result[15][0]).to eq :OPERATOR_OR
|
||||
expect(@result[16][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[16][1]).to eq '"id with q 20"'
|
||||
expect(@result[17][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[18][0]).to eq :OPERATOR_AND
|
||||
expect(@result[19][0]).to eq :L_BRACKET
|
||||
expect(@result[20][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[20][1]).to eq '"id with Q 30"'
|
||||
expect(@result[21][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[21][1]).to eq '"id with Q 40"'
|
||||
expect(@result[22][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[23][0]).to eq :OPERATOR_AND
|
||||
expect(@result[24][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[25][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[25][1]).to eq 'id-without-Q-50'
|
||||
expect(@result[26][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests query with -or-, -and- and -not- words inside quoted expression' do
|
||||
@result = @evaluator.tokenize('tag:"tag with or and not inside"')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'tag'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[2][1]).to eq '"tag with or and not inside"'
|
||||
end
|
||||
end
|
||||
end
|
||||
164
spec/lib/query_parser_spec.rb
Normal file
@@ -0,0 +1,164 @@
|
||||
require './parser'
|
||||
|
||||
class QueryParserTester
|
||||
describe 'Testing the Parser' do
|
||||
before do
|
||||
@evaluator = Query.new
|
||||
end
|
||||
|
||||
it 'tests query with only one search term without quotes and without column name' do
|
||||
@result = @evaluator.parse('-123')
|
||||
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '-123'
|
||||
end
|
||||
|
||||
it 'tests query with only one search term with quotes and without column name' do
|
||||
@result = @evaluator.parse('"OR 128"')
|
||||
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '"OR 128"'
|
||||
end
|
||||
|
||||
it 'tests query with one column and search term without quotes' do
|
||||
@result = @evaluator.parse('tag:mta')
|
||||
|
||||
expect(@result['tag']).to eq 'mta'
|
||||
end
|
||||
|
||||
it 'tests query with one column and search term with quotes' do
|
||||
@result = @evaluator.parse('tag:"tag 120"')
|
||||
|
||||
expect(@result['tag']).to eq '"tag 120"'
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with OR and search terms without quotes' do
|
||||
@result = @evaluator.parse('tag:mta OR tag:12')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '12' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with OR and search terms with quotes' do
|
||||
@result = @evaluator.parse('tag:mta OR tag:"tag 12"')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '"tag 12"' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with AND and search terms without quotes' do
|
||||
@result = @evaluator.parse('tag:mta AND tag:12')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '12' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with AND and search terms with quotes' do
|
||||
@result = @evaluator.parse('tag:mta and tag:"tag 12"')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '"tag 12"' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests simple query with brackets' do
|
||||
@result = @evaluator.parse('(123)')
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '123'
|
||||
end
|
||||
|
||||
it 'tests simple query with brackets and with a column name' do
|
||||
@result = @evaluator.parse('(name:JF)')
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result['name']).to eq 'JF'
|
||||
end
|
||||
|
||||
it 'tests query with OR operator in brackets' do
|
||||
@result = @evaluator.parse('(name:JF or tag:mta)')
|
||||
|
||||
@expected_array = [
|
||||
{ 'name' => 'JF' },
|
||||
{ 'tag' => 'mta' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two simple brackets expressions' do
|
||||
@result = @evaluator.parse('(name:JF) and (-456)')
|
||||
|
||||
@expected_array = [
|
||||
{ 'name' => 'JF' },
|
||||
{ :DEFAULT_COLUMN => '-456' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two brackets expressions' do
|
||||
@result = @evaluator.parse('(name:JF or tag:"tag 0") and (-456)')
|
||||
|
||||
@expected_array_part_1 = [
|
||||
{ 'name' => 'JF' },
|
||||
{ 'tag' => '"tag 0"' }
|
||||
]
|
||||
|
||||
@expected_array_total = [
|
||||
{:OPERATOR_OR => @expected_array_part_1},
|
||||
{:DEFAULT_COLUMN => '-456'}
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array_total
|
||||
end
|
||||
|
||||
it 'tests operator precedence' do
|
||||
@result1 = @evaluator.parse('tag:mta or name:JF and 12_4')
|
||||
@result2 = @evaluator.parse('tag:mta or (name:JF and 12_4)')
|
||||
|
||||
expect(@result1).to eq @result2
|
||||
|
||||
expect(@result1.length).to eq 1
|
||||
|
||||
@expected_array_part_2 = [
|
||||
{'name' => 'JF'},
|
||||
{:DEFAULT_COLUMN => '12_4'}
|
||||
]
|
||||
|
||||
@expected_array_total = [
|
||||
{'tag' => 'mta'},
|
||||
{:OPERATOR_AND => @expected_array_part_2}
|
||||
]
|
||||
|
||||
expect(@result1[:OPERATOR_OR]).to eq @expected_array_total
|
||||
|
||||
end
|
||||
|
||||
# Tests to write :
|
||||
# * query with multiple column names and search terms without logical operators
|
||||
# * AND NOT, OR NOT tests
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
22
spec/lib/text_to_regex_query_spec.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe TextToRegexQuery do
|
||||
include_examples 'pg_search', SimpleVectorModel
|
||||
describe '.new' do
|
||||
# just default
|
||||
it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR 1<>1', '%some-default-value%']) }
|
||||
|
||||
# default and named
|
||||
it { expect(described_class.new('name:hamo id:1', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR players.name like ? OR 1<>1', '%id:1%', '%hamo%']) }
|
||||
|
||||
# escape characters
|
||||
it { expect(described_class.new('name:hamo id:1',[:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR players.name like ? OR 1<>1', '%id:1%', '%hamo%']) }
|
||||
|
||||
# default and explicit with underscore
|
||||
it { expect(described_class.new('device_id:-123', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq([" players.device_id like ? OR 1<>1", "%-123%"]) }
|
||||
|
||||
# default and explicit with underscore with multiple columns
|
||||
it { expect(described_class.new('name:bla device_id:-123', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq([" players.name like ? OR players.device_id like ? OR 1<>1", "%bla%", "%-123%"]) }
|
||||
|
||||
end
|
||||
end
|
||||
80
spec/lib/text_to_tsquery_spec.rb
Normal file
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe TextToTsquery do
|
||||
describe '.new' do
|
||||
# partial match
|
||||
it { expect(described_class.new('A').tsquery).to eq('A:*') }
|
||||
it { expect(described_class.new('A', wildcard: false).tsquery).to eq('A:') }
|
||||
it { expect(described_class.new(' A ').tsquery).to eq('A:*') }
|
||||
|
||||
# AND operations
|
||||
it { expect(described_class.new('A B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A B C').tsquery).to eq('A:*&B:*&C:*') }
|
||||
it { expect(described_class.new('A and B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A AND B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A & B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A && B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A & B && C and D AND E F').tsquery).to eq('A:*&B:*&C:*&D:*&E:*&F:*') }
|
||||
|
||||
# OR operations
|
||||
it { expect(described_class.new('A or B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A or B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A OR B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A OR B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A | B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A | B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A || B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A || B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A or or B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A or or B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A | B || C or D OR E').tsquery).to eq('A:*|B:*|C:*|D:*|E:*') }
|
||||
it { expect(described_class.new('A | B || C or D OR E', wildcard: false).tsquery).to eq('A:|B:|C:|D:|E:') }
|
||||
|
||||
# () Precedence
|
||||
it { expect(described_class.new('(A)').tsquery).to eq('(A:*)') }
|
||||
it { expect(described_class.new('(A)', wildcard: false).tsquery).to eq('(A:)') }
|
||||
it { expect(described_class.new('(A B)').tsquery).to eq('(A:*&B:*)') }
|
||||
it { expect(described_class.new('(A B)', wildcard: false).tsquery).to eq('(A:&B:)') }
|
||||
it { expect(described_class.new('A (B !C)').tsquery).to eq('A:*&(B:*&!C)') }
|
||||
it { expect(described_class.new('A (B !C)', wildcard: false).tsquery).to eq('A:&(B:&!C)') }
|
||||
it { expect(described_class.new('(A AND B) OR C').tsquery).to eq('(A:*&B:*)|C:*') }
|
||||
it { expect(described_class.new('(A AND B) OR C', wildcard: false).tsquery).to eq('(A:&B:)|C:') }
|
||||
it { expect(described_class.new('A AND (B OR C)').tsquery).to eq('A:*&(B:*|C:*)') }
|
||||
it { expect(described_class.new('A AND (B OR C)', wildcard: false).tsquery).to eq('A:&(B:|C:)') }
|
||||
it { expect(described_class.new('(A & B) || C').tsquery).to eq('(A:*&B:*)|C:*') }
|
||||
it { expect(described_class.new('(A & B) || C', wildcard: false).tsquery).to eq('(A:&B:)|C:') }
|
||||
it { expect(described_class.new('A && (B | C)').tsquery).to eq('A:*&(B:*|C:*)') }
|
||||
it { expect(described_class.new('A && (B | C)', wildcard: false).tsquery).to eq('A:&(B:|C:)') }
|
||||
it { expect(described_class.new('A && !D (B | C | !E)').tsquery).to eq('A:*&!D&(B:*|C:*|!E)') }
|
||||
it { expect(described_class.new('A && !D (B | C | !E)', wildcard: false).tsquery).to eq('A:&!D&(B:|C:|!E)') }
|
||||
|
||||
# Exact Matches
|
||||
it { expect(described_class.new('"A"').tsquery).to eq("'A'") }
|
||||
it { expect(described_class.new('"A B"').tsquery).to eq("'A B'") }
|
||||
it { expect(described_class.new('"A&B"').tsquery).to eq("'A&B'") }
|
||||
it { expect(described_class.new('"-A|B"').tsquery).to eq("'-A|B'") }
|
||||
it { expect(described_class.new('"A-B"').tsquery).to eq("'A-B'") }
|
||||
it { expect(described_class.new('"A" B').tsquery).to eq("'A'&B:*") }
|
||||
it { expect(described_class.new('"A" B', wildcard: false).tsquery).to eq("'A'&B:") }
|
||||
it { expect(described_class.new('"A B" C').tsquery).to eq("'A B'&C:*") }
|
||||
it { expect(described_class.new('"A B" C', wildcard: false).tsquery).to eq("'A B'&C:") }
|
||||
it { expect(described_class.new('("A B" or C) and D').tsquery).to eq("('A B'|C:*)&D:*") }
|
||||
it { expect(described_class.new('("A B" or C) and D', wildcard: false).tsquery).to eq("('A B'|C:)&D:") }
|
||||
|
||||
describe 'validations' do
|
||||
it { expect { described_class.new('(') }.to raise_error(ArgumentError, /parenthesis/) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.valid_search_parenthesis?' do
|
||||
it { expect(described_class.valid_search_parenthesis?('')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('()')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('()()')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('(()())')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('((())())')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('(')).to eq false }
|
||||
it { expect(described_class.valid_search_parenthesis?(')(')).to eq false }
|
||||
it { expect(described_class.valid_search_parenthesis?('())')).to eq false }
|
||||
it { expect(described_class.valid_search_parenthesis?('((()())')).to eq false }
|
||||
end
|
||||
end
|
||||
45
spec/schema.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ActiveRecord::Schema.define do
|
||||
create_table :vector_models, force: true do |t|
|
||||
t.string :name
|
||||
t.string :value
|
||||
t.tsvector :search_cache
|
||||
t.timestamps null: false
|
||||
end
|
||||
add_index :vector_models, :search_cache, using: :gin
|
||||
|
||||
create_table :vector_with_custom_primary_key_models, id: false, force: true do |t|
|
||||
t.uuid :uuid, null: false
|
||||
t.string :name
|
||||
t.string :value
|
||||
t.tsvector :search_vector
|
||||
t.timestamps null: false
|
||||
end
|
||||
add_index :vector_with_custom_primary_key_models, :uuid, using: :btree
|
||||
add_index :vector_with_custom_primary_key_models, :search_vector, using: :gin
|
||||
|
||||
create_table :dynamic_models, force: true do |t|
|
||||
t.string :name
|
||||
t.string :value
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
create_table :tags, force: true do |t|
|
||||
t.string :value
|
||||
t.references :category, index: true
|
||||
t.references :taggable, polymorphic: true, index: true
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
create_table :categories, force: true do |t|
|
||||
t.string :name
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
create_table :sections, force: true do |t|
|
||||
t.references :tag
|
||||
t.string :name
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
52
spec/spec_helper.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler'
|
||||
Bundler.require
|
||||
|
||||
require 'simplecov'
|
||||
SimpleCov.start do
|
||||
add_filter '/spec/'
|
||||
end
|
||||
|
||||
require 'active_record'
|
||||
require 'database_cleaner'
|
||||
require 'dotenv/load'
|
||||
require 'pg_searchable_regex'
|
||||
require 'pry'
|
||||
|
||||
# requiring support files
|
||||
Dir[__dir__ + '/support/**/*.rb'].each { |f| require f }
|
||||
|
||||
RSpec.configure do |config|
|
||||
env = Dotenv.parse('.env.test', '.env.test.local')
|
||||
db_config = {
|
||||
adapter: 'postgresql',
|
||||
host: ENV['DATABASE_HOST'] || env['DATABASE_HOST'],
|
||||
username: ENV['DATABASE_USERNAME'] || env['DATABASE_USERNAME'],
|
||||
password: ENV['DATABASE_PASSWORD'] || env['DATABASE_PASSWORD'],
|
||||
database: ENV['DATABASE_NAME'] || env['DATABASE_NAME'],
|
||||
pool: ENV['DATABASE_POOL'] || env['DATABASE_POOL'],
|
||||
port: ENV['DATABASE_PORT'] || env['DATABASE_PORT']
|
||||
}
|
||||
db_config_admin = db_config.merge(database: 'postgres', schema_search_path: 'public')
|
||||
|
||||
config.before(:suite) do
|
||||
ActiveRecord::Base.establish_connection(db_config_admin)
|
||||
ActiveRecord::Base.connection.create_database(db_config[:database])
|
||||
ActiveRecord::Base.establish_connection(db_config)
|
||||
ActiveRecord::Tasks::DatabaseTasks.load_schema_for(db_config, :ruby, "#{__dir__}/schema.rb")
|
||||
DatabaseCleaner.strategy = :transaction
|
||||
DatabaseCleaner.clean_with(:truncation)
|
||||
end
|
||||
|
||||
config.around do |example|
|
||||
DatabaseCleaner.cleaning do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
config.after(:suite) do
|
||||
ActiveRecord::Base.establish_connection(db_config_admin)
|
||||
ActiveRecord::Base.connection.drop_database(db_config[:database])
|
||||
end
|
||||
end
|
||||
103
spec/support/models.rb
Executable file
@@ -0,0 +1,103 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class VectorModel < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache
|
||||
end
|
||||
|
||||
class VectorModelWithoutCallback < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, skip_callback: true
|
||||
end
|
||||
|
||||
class VectorWithCustomCallback < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, skip_callback: true, external_cache_data: :colors
|
||||
after_save :update_pg_search_cache
|
||||
|
||||
def colors
|
||||
%w[orange blue]
|
||||
end
|
||||
end
|
||||
|
||||
class VectorWithCustomPrimaryKeyModel < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[uuid name value]
|
||||
self.primary_key = :uuid
|
||||
before_save { self.uuid = SecureRandom.uuid }
|
||||
end
|
||||
|
||||
class SimpleVectorModel < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, language: :simple
|
||||
end
|
||||
|
||||
class VectorWithoutWildcardModel < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, wildcard: false
|
||||
end
|
||||
|
||||
class VectorModelWithCustomSearchScope < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, scope: 'fulltext'
|
||||
end
|
||||
|
||||
class VectorModelWithTagValues < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, external_cache_data: :tag_values
|
||||
has_many :tags, as: :taggable
|
||||
|
||||
def tag_values
|
||||
tags.pluck(:value)
|
||||
end
|
||||
end
|
||||
|
||||
class DynamicModel < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value]
|
||||
end
|
||||
|
||||
class DynamicModelWithTagValues < ActiveRecord::Base
|
||||
self.table_name = :dynamic_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value tags.value], joins: [:tags]
|
||||
has_many :tags, as: :taggable
|
||||
end
|
||||
|
||||
class DynamicModelWithCategory < ActiveRecord::Base
|
||||
self.table_name = :dynamic_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value tags.value categories.name],
|
||||
joins: { tags: :category }
|
||||
has_many :tags, as: :taggable
|
||||
end
|
||||
|
||||
class DynamicModelWithSectionsTrhough < ActiveRecord::Base
|
||||
self.table_name = :dynamic_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value tags.value sections.name],
|
||||
joins: [{ tags: :category }, :sections]
|
||||
has_many :tags, as: :taggable
|
||||
has_many :sections, through: :tags
|
||||
end
|
||||
|
||||
class Tag < ActiveRecord::Base
|
||||
belongs_to :category
|
||||
has_many :sections
|
||||
belongs_to :taggable, polymorphic: true
|
||||
after_save { taggable.update_pg_search_cache if taggable.class.ts_cache_field.present? }
|
||||
end
|
||||
|
||||
class Section < ActiveRecord::Base
|
||||
belongs_to :tag
|
||||
end
|
||||
|
||||
class Category < ActiveRecord::Base
|
||||
has_many :tags
|
||||
end
|
||||
68
spec/support/ts_search_scope_shared_examples.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
shared_examples 'pg_search' do |mock_class, scope_method = 'scope_search'|
|
||||
describe 'searchable fields' do
|
||||
let!(:record) { mock_class.create name: 'something', value: 'amazing' }
|
||||
|
||||
it { expect(mock_class.send(scope_method, record.id)).to include(record) }
|
||||
it { expect(mock_class.send(scope_method, 'something')).to include(record) }
|
||||
it { expect(mock_class.send(scope_method, 'amazing')).to include(record) }
|
||||
it { expect(mock_class.send(scope_method, 'candy')).not_to include(record) }
|
||||
end
|
||||
|
||||
describe 'operators' do
|
||||
before do
|
||||
mock_class.create(name: 'uno', value: 'one')
|
||||
mock_class.create(name: 'dos', value: 'two')
|
||||
mock_class.create(name: 'tres', value: 'three')
|
||||
mock_class.create(name: 'cuatro', value: 'four')
|
||||
mock_class.create(name: 'cuatro', value: 'five')
|
||||
mock_class.create(name: 'cinco', value: 'one-two-three')
|
||||
end
|
||||
|
||||
it 'returns all records if search query is blank' do
|
||||
expect(mock_class.send(scope_method, ' ').first).not_to be_blank
|
||||
end
|
||||
|
||||
describe 'AND searches' do
|
||||
it { expect(mock_class.send(scope_method, 'uno one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno&one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno&&one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno & one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno and one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'dos AND two').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno dos').count).to eq(0) }
|
||||
end
|
||||
|
||||
describe 'OR searches' do
|
||||
it { expect(mock_class.send(scope_method, 'uno or dos').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, 'uno or dos').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, 'uno or or dos').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, 'uno or or dos').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, 'one OR two').count).to eq(3) }
|
||||
it { expect(mock_class.send(scope_method, 'uno or two').count).to eq(3) }
|
||||
end
|
||||
|
||||
describe 'AND and OR searches' do
|
||||
it { expect(mock_class.send(scope_method, 'uno or dos two').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, 'uno or dos one').count).to eq(1) }
|
||||
end
|
||||
|
||||
describe 'NOT searches' do
|
||||
it { expect(mock_class.send(scope_method, '!cuatro').count).to eq(4) }
|
||||
it { expect(mock_class.send(scope_method, 'cuatro !four').count).to eq(1) }
|
||||
end
|
||||
|
||||
describe 'exact match' do
|
||||
it { expect(mock_class.send(scope_method, '"cinco"').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, '"one-two-three"').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, '"one two"').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, '"one&two"').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, '"one|two"').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, '"one-two-three" or two').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, '"one-two"').count).to eq(0) }
|
||||
it { expect(mock_class.send(scope_method, '"cinco one"').count).to eq(1) }
|
||||
end
|
||||
end
|
||||
end
|
||||
11
version.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Version
|
||||
MAJOR = 1
|
||||
MINOR = 0
|
||||
PATCH = 13
|
||||
|
||||
def self.to_s
|
||||
[MAJOR, MINOR, PATCH].compact.join('.')
|
||||
end
|
||||
end
|
||||