magick_columns 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # MagickColumns
2
+
3
+ This gem extends ActiveRecord to provide queries built from *simple* strings
4
+
5
+ ## Instalation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'magick_columns'
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ You must declare `has_magick_columns` in your model:
16
+
17
+ ```ruby
18
+ class Person < ActiveRecord::Base
19
+ has_magick_columns name: :string, email: :email, birth: :date
20
+ end
21
+ ```
22
+
23
+ And now you can do something like this:
24
+
25
+ ```ruby
26
+ people = Person.magick_search('anakin or luke')
27
+ ```
28
+
29
+ The method returns a Relation, so you can apply any aditional method you want:
30
+
31
+ ```ruby
32
+ people.order('name').limit(5)
33
+ ```
34
+
35
+ And of course you can "spy" the query with:
36
+
37
+ ```ruby
38
+ people.to_sql
39
+ ```
40
+
41
+ ## Rules
42
+
43
+ There is also a set of rules, for tokenize and replace some types of "patterns".
44
+ For example, you can write:
45
+
46
+ ```ruby
47
+ people = Person.magick_search('from 01/01/2000')
48
+ ```
49
+
50
+ And you get the people born in the XXI century =)
51
+
52
+ ## Custom configuration
53
+
54
+ If you want to define your own rules, or replace some existing configuration add
55
+ in config/initializers one ruby file, for example magick_columns_config.rb
56
+
57
+ ```ruby
58
+ MagickColumns.setup do |config|
59
+ config.and_operators = ['and']
60
+ config.or_operators = ['or']
61
+ config.from_operators = ['from', 'since']
62
+ config.until_operators = ['to', 'until']
63
+ config.today_operators = ['today', 'now']
64
+ # Each replacement rule consists in a pattern and a replacement proc or lambda
65
+ config.replacement_rules[:new_replacement_rule] =
66
+ pattern: /today/,
67
+ replacement: ->(match) { Date.today.to_s(:db) }
68
+ }
69
+ # Each tokenizer rule consists in a pattern and a tokenizer proc or lambda.
70
+ # The proc must return a hash with a valid SQL operator and a term for the
71
+ # condition.
72
+ config.tokenize_rules[:new_tokenize_rule] = {
73
+ pattern: /(\A\s*|\s+)(from|since)\s+(\S+)/,
74
+ tokenizer: ->(match) { { operator: '>=', term: match[3] } }
75
+ }
76
+ end
77
+ ```
78
+
79
+ ## How to contribute
80
+
81
+ If you find what you might think is a bug:
82
+
83
+ 1. Check the GitHub issue tracker to see if anyone else has had the same issue.
84
+ https://github.com/francocatena/magick_columns/issues/
85
+ 2. If you do not see anything, create an issue with information on how to reproduce it.
86
+
87
+ If you want to contribute an enhancement or a fix:
88
+
89
+ 1. Fork the project on GitHub.
90
+ https://github.com/francocatena/magick_columns/
91
+ 2. Make your changes with tests.
92
+ 3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that are not related to your enhancement or fix
93
+ 4. Send a pull request.
@@ -1,6 +1,58 @@
1
- require 'magick_columns/magick_columns'
2
- require 'magick_columns/defaults'
1
+ module MagickColumns
2
+ autoload :DEFAULTS, 'magick_columns/defaults'
3
+ autoload :I18N_DEFAULTS, 'magick_columns/defaults'
4
+ autoload :TOKENIZE_RULES, 'magick_columns/rules'
5
+ autoload :REPLACEMENT_RULES, 'magick_columns/rules'
6
+ autoload :Tokenizer, 'magick_columns/tokenizer'
7
+
8
+ class << self
9
+ private
3
10
 
4
- autoload 'Timeliness', 'timeliness'
11
+ def _default_setup_for(config)
12
+ translation = I18n.t("magick_columns.#{config}", raise: true) rescue MagickColumns::I18N_DEFAULTS[config]
5
13
 
6
- ActiveRecord::Base.send :include, MagickColumns::Base
14
+ if translation.respond_to?(:map)
15
+ translation.map { |c| Regexp.quote(c) }.join('|')
16
+ else
17
+ translation
18
+ end
19
+ end
20
+ end
21
+
22
+ # Strings considered "and" spliters
23
+ mattr_accessor :and_operators
24
+ @@and_operators = _default_setup_for :and
25
+
26
+ # Strings considered "or" spliters
27
+ mattr_accessor :or_operators
28
+ @@or_operators = _default_setup_for :or
29
+
30
+ # Strings considered "from" terms (like "from 01/01/2012")
31
+ mattr_accessor :from_operators
32
+ @@from_operators = _default_setup_for :from
33
+
34
+ # Strings considered "until" terms (like "until 01/01/2012")
35
+ mattr_accessor :until_operators
36
+ @@until_operators = _default_setup_for :until
37
+
38
+ # Strings considered "today" strings (like "from today")
39
+ mattr_accessor :today_operators
40
+ @@today_operators = _default_setup_for :today
41
+
42
+ # Rules to replace text in the natural string
43
+ mattr_accessor :replacement_rules
44
+ @@replacement_rules = REPLACEMENT_RULES.dup
45
+
46
+ # Rules for tokenize the natural string
47
+ mattr_accessor :tokenize_rules
48
+ @@tokenize_rules = TOKENIZE_RULES.dup
49
+
50
+ # Setup method for plugin configuration
51
+ def self.setup
52
+ yield self
53
+ end
54
+ end
55
+
56
+ autoload :Timeliness, 'timeliness'
57
+
58
+ require 'magick_columns/railtie' if defined?(Rails)
@@ -0,0 +1,56 @@
1
+ module MagickColumns
2
+ module ActiveRecord
3
+ def has_magick_columns(options = {})
4
+ @@_magick_columns ||= {}
5
+ @@_magick_columns[name] ||= []
6
+
7
+ options.each do |field, type|
8
+ column_options = _magick_column_options(type)
9
+
10
+ @@_magick_columns[name] << { field: field }.merge(column_options)
11
+ end
12
+ end
13
+
14
+ def magick_search(query)
15
+ or_queries = []
16
+ terms = {}
17
+
18
+ MagickColumns::Tokenizer.new(query).extract_terms.each_with_index do |or_term, i|
19
+ and_queries = []
20
+
21
+ or_term.each_with_index do |and_term, j|
22
+ mini_query = []
23
+
24
+ @@_magick_columns[name].each_with_index do |column, k|
25
+ if column[:condition].call(and_term[:term])
26
+ operator = and_term[:operator] || _map_magick_column_operator(column[:operator])
27
+ terms[:"t_#{i}_#{j}_#{k}"] = column[:mask] % {t: and_term[:term]}
28
+
29
+ mini_query << "#{column[:field]} #{operator} :t_#{i}_#{j}_#{k}"
30
+ end
31
+ end
32
+
33
+ and_queries << mini_query.join(' OR ')
34
+ end
35
+
36
+ or_queries << and_queries.map { |a_q| "(#{a_q})" }.join(' AND ')
37
+ end
38
+
39
+ where(or_queries.map { |o_q| "(#{o_q})" }.join(' OR '), terms)
40
+ end
41
+
42
+ private
43
+
44
+ def _magick_column_options(type)
45
+ type.kind_of?(Hash) ? type : MagickColumns::DEFAULTS[type.to_sym]
46
+ end
47
+
48
+ def _map_magick_column_operator(operator, db = nil)
49
+ db ||= ::ActiveRecord::Base.connection.adapter_name
50
+
51
+ operator == :like ? (db == 'PostgreSQL' ? 'ILIKE' : 'LIKE') : operator
52
+ end
53
+ end
54
+ end
55
+
56
+ ActiveRecord::Base.extend MagickColumns::ActiveRecord
@@ -1,4 +1,4 @@
1
- module MagicColumns
1
+ module MagickColumns
2
2
  DEFAULTS = {
3
3
  string: {
4
4
  operator: :like,
@@ -28,4 +28,12 @@ module MagicColumns
28
28
  convert: ->(t) { ::Timeliness.parse(t.to_s) }
29
29
  }
30
30
  }
31
+
32
+ I18N_DEFAULTS = {
33
+ from: ['from', 'since'],
34
+ until: ['to', 'until'],
35
+ and: ['and'],
36
+ or: ['or'],
37
+ today: ['today', 'now']
38
+ }
31
39
  end
@@ -0,0 +1,15 @@
1
+ 'en':
2
+ magick_columns:
3
+ and:
4
+ - and
5
+ or:
6
+ - or
7
+ from:
8
+ - from
9
+ - since
10
+ until:
11
+ - to
12
+ - until
13
+ today:
14
+ - today
15
+ - now
@@ -0,0 +1,13 @@
1
+ 'es':
2
+ magick_columns:
3
+ and:
4
+ - y
5
+ or:
6
+ - o
7
+ from:
8
+ - desde
9
+ until:
10
+ - hasta
11
+ today:
12
+ - hoy
13
+ - ahora
@@ -0,0 +1,9 @@
1
+ module MagickColumns
2
+ class Railtie < Rails::Railtie
3
+ initializer 'magick_columns.active_record' do
4
+ ActiveSupport.on_load :active_record do
5
+ require 'magick_columns/active_record'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ # TODO: These rules should be added to a live array, not a constant one
2
+ module MagickColumns
3
+ TOKENIZE_RULES = {
4
+ from: {
5
+ pattern: /(\A\s*|\s+)(#{MagickColumns.from_operators})\s+(\S+)/,
6
+ tokenizer: ->(match) { { operator: '>=', term: match[3] } }
7
+ },
8
+ until:{
9
+ pattern: /(\A\s*|\s+)(#{MagickColumns.until_operators})\s+(\S+)/,
10
+ tokenizer: ->(match) { { operator: '<=', term: match[3] } }
11
+ }
12
+ }
13
+
14
+ REPLACEMENT_RULES = {
15
+ today: {
16
+ pattern: /#{MagickColumns.today_operators}/,
17
+ replacement: ->(match) { Date.today.to_s(:db) }
18
+ }
19
+ }
20
+ end
@@ -0,0 +1,56 @@
1
+ module MagickColumns
2
+ class Tokenizer
3
+ def initialize(query = '')
4
+ @query = query
5
+ end
6
+
7
+ def extract_terms
8
+ terms = []
9
+
10
+ clean_query.split(%r{\s+(#{MagickColumns.or_operators})\s+}).each do |o_t|
11
+ unless o_t =~ %r{\A(#{MagickColumns.or_operators})\z}
12
+ and_terms = []
13
+
14
+ o_t.split(%r{\s+(#{MagickColumns.and_operators})\s+}).each do |t|
15
+ unless t =~ %r{\A(#{MagickColumns.and_operators})\z}
16
+ and_terms.concat split_term_in_terms(t)
17
+ end
18
+ end
19
+
20
+ terms << and_terms unless and_terms.empty?
21
+ end
22
+ end
23
+
24
+ terms.reject(&:empty?)
25
+ end
26
+
27
+ def clean_query
28
+ @query.strip
29
+ .gsub(%r{\A(\s*(#{MagickColumns.and_operators})\s+)+}, '')
30
+ .gsub(%r{(\s+(#{MagickColumns.and_operators})\s*)+\z}, '')
31
+ .gsub(%r{\A(\s*(#{MagickColumns.or_operators})\s+)+}, '')
32
+ .gsub(%r{(\s+(#{MagickColumns.or_operators})\s*)+\z}, '')
33
+ end
34
+
35
+ def split_term_in_terms(term)
36
+ term_copy = term.dup
37
+ terms = []
38
+
39
+ MagickColumns.replacement_rules.each do |rule, options|
40
+ while(match = term_copy.match(options[:pattern]))
41
+ term_copy.sub!(options[:pattern], options[:replacement].call(match))
42
+ end
43
+ end
44
+
45
+ MagickColumns.tokenize_rules.each do |rule, options|
46
+ while(match = term_copy.match(options[:pattern]))
47
+ terms << options[:tokenizer].call(match)
48
+
49
+ term_copy.sub!(options[:pattern], '')
50
+ end
51
+ end
52
+
53
+ terms + term_copy.strip.split(/\s+/).map { |t| { term: t } }
54
+ end
55
+ end
56
+ end
@@ -1,3 +1,3 @@
1
1
  module MagickColumns
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
@@ -116,3 +116,96 @@ Migrating to CreateArticles (20120312175442)
116
116
   (0.5ms) SELECT version FROM "schema_migrations"
117
117
   (3.2ms) INSERT INTO "schema_migrations" (version) VALUES ('20120312175442')
118
118
   (10.9ms) INSERT INTO "schema_migrations" (version) VALUES ('20120312175303')
119
+  (0.6ms) SELECT "schema_migrations"."version" FROM "schema_migrations" 
120
+  (0.1ms) SET search_path TO public
121
+  (222.3ms) DROP DATABASE IF EXISTS "dummy_test"
122
+  (0.2ms) SET search_path TO public
123
+  (884.3ms) CREATE DATABASE "dummy_test" ENCODING = 'unicode'
124
+  (145.9ms) CREATE TABLE "articles" ("id" serial primary key, "name" character varying(255), "code" integer, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
125
+  (276.6ms) CREATE TABLE "people" ("id" serial primary key, "name" character varying(255), "email" character varying(255), "birth" date, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL) 
126
+  (100.6ms) CREATE TABLE "schema_migrations" ("version" character varying(255) NOT NULL)
127
+  (145.3ms)  SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
128
+ FROM pg_class t
129
+ INNER JOIN pg_index d ON t.oid = d.indrelid
130
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
131
+ WHERE i.relkind = 'i'
132
+ AND d.indisprimary = 'f'
133
+ AND t.relname = 'schema_migrations'
134
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
135
+ ORDER BY i.relname
136
+ 
137
+  (255.5ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
138
+  (0.7ms) SELECT version FROM "schema_migrations"
139
+  (74.6ms) INSERT INTO "schema_migrations" (version) VALUES ('20120312175442')
140
+  (52.4ms) INSERT INTO "schema_migrations" (version) VALUES ('20120312175303')
141
+  (42.1ms) ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE "people" DISABLE TRIGGER ALL;ALTER TABLE "articles" DISABLE TRIGGER ALL
142
+  (0.2ms) BEGIN
143
+ Fixture Delete (0.6ms) DELETE FROM "articles"
144
+ Fixture Insert (26.6ms) INSERT INTO "articles" ("name", "code", "created_at", "updated_at", "id") VALUES ('Lightsaber', 1, '2012-03-15 17:41:34', '2012-03-15 17:41:34', 980403528)
145
+ Fixture Insert (0.3ms) INSERT INTO "articles" ("name", "code", "created_at", "updated_at", "id") VALUES ('Ship', 2, '2012-03-15 17:41:34', '2012-03-15 17:41:34', 976284455)
146
+ PK and serial sequence (2.2ms) SELECT attr.attname, seq.relname
147
+ FROM pg_class seq,
148
+ pg_attribute attr,
149
+ pg_depend dep,
150
+ pg_namespace name,
151
+ pg_constraint cons
152
+ WHERE seq.oid = dep.objid
153
+ AND seq.relkind = 'S'
154
+ AND attr.attrelid = dep.refobjid
155
+ AND attr.attnum = dep.refobjsubid
156
+ AND attr.attrelid = cons.conrelid
157
+ AND attr.attnum = cons.conkey[1]
158
+ AND cons.contype = 'p'
159
+ AND dep.refobjid = '"articles"'::regclass
160
+
161
+ Reset sequence (181.0ms)  SELECT setval('"articles_id_seq"', (SELECT COALESCE(MAX("id")+(SELECT increment_by FROM "articles_id_seq"), (SELECT min_value FROM "articles_id_seq")) FROM "articles"), false)
162
+ 
163
+  (37.0ms) COMMIT
164
+  (0.3ms) ALTER TABLE "schema_migrations" ENABLE TRIGGER ALL;ALTER TABLE "people" ENABLE TRIGGER ALL;ALTER TABLE "articles" ENABLE TRIGGER ALL
165
+  (0.3ms) ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE "people" DISABLE TRIGGER ALL;ALTER TABLE "articles" DISABLE TRIGGER ALL
166
+  (0.2ms) BEGIN
167
+ Fixture Delete (0.4ms) DELETE FROM "people"
168
+ Fixture Insert (13.9ms) INSERT INTO "people" ("name", "email", "birth", "created_at", "updated_at", "id") VALUES ('Obi-Wan Kenobi', 'obi@sw.com', '3012-03-15', '2012-03-15 17:41:35', '2012-03-15 17:41:35', 632303495)
169
+ Fixture Insert (0.3ms) INSERT INTO "people" ("name", "email", "birth", "created_at", "updated_at", "id") VALUES ('Luke Skywalker', 'luke@sw.com', '3052-03-15', '2012-03-15 17:41:35', '2012-03-15 17:41:35', 962534057)
170
+ Fixture Insert (0.3ms) INSERT INTO "people" ("name", "email", "birth", "created_at", "updated_at", "id") VALUES ('Anakin Skywalker', 'anakin@sw.com', '3032-03-15', '2012-03-15 17:41:35', '2012-03-15 17:41:35', 222665832)
171
+ PK and serial sequence (2.1ms) SELECT attr.attname, seq.relname
172
+ FROM pg_class seq,
173
+ pg_attribute attr,
174
+ pg_depend dep,
175
+ pg_namespace name,
176
+ pg_constraint cons
177
+ WHERE seq.oid = dep.objid
178
+ AND seq.relkind = 'S'
179
+ AND attr.attrelid = dep.refobjid
180
+ AND attr.attnum = dep.refobjsubid
181
+ AND attr.attrelid = cons.conrelid
182
+ AND attr.attnum = cons.conkey[1]
183
+ AND cons.contype = 'p'
184
+ AND dep.refobjid = '"people"'::regclass
185
+
186
+ Reset sequence (12.5ms)  SELECT setval('"people_id_seq"', (SELECT COALESCE(MAX("id")+(SELECT increment_by FROM "people_id_seq"), (SELECT min_value FROM "people_id_seq")) FROM "people"), false)
187
+ 
188
+  (15.0ms) COMMIT
189
+  (0.2ms) ALTER TABLE "schema_migrations" ENABLE TRIGGER ALL;ALTER TABLE "people" ENABLE TRIGGER ALL;ALTER TABLE "articles" ENABLE TRIGGER ALL
190
+  (0.9ms) SELECT "schema_migrations"."version" FROM "schema_migrations" 
191
+  (0.2ms) SET search_path TO public
192
+  (537.3ms) DROP DATABASE IF EXISTS "dummy_test"
193
+  (0.2ms) SET search_path TO public
194
+  (2217.0ms) CREATE DATABASE "dummy_test" ENCODING = 'unicode'
195
+  (176.4ms) CREATE TABLE "articles" ("id" serial primary key, "name" character varying(255), "code" integer, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
196
+  (153.9ms) CREATE TABLE "people" ("id" serial primary key, "name" character varying(255), "email" character varying(255), "birth" date, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL) 
197
+  (10.3ms) CREATE TABLE "schema_migrations" ("version" character varying(255) NOT NULL)
198
+  (3.1ms)  SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
199
+ FROM pg_class t
200
+ INNER JOIN pg_index d ON t.oid = d.indrelid
201
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
202
+ WHERE i.relkind = 'i'
203
+ AND d.indisprimary = 'f'
204
+ AND t.relname = 'schema_migrations'
205
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
206
+ ORDER BY i.relname
207
+ 
208
+  (85.1ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
209
+  (0.5ms) SELECT version FROM "schema_migrations"
210
+  (9.3ms) INSERT INTO "schema_migrations" (version) VALUES ('20120312175442')
211
+  (10.8ms) INSERT INTO "schema_migrations" (version) VALUES ('20120312175303')