attr_searchable 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ gemfiles/*.lock
data/.travis.yml ADDED
@@ -0,0 +1,34 @@
1
+
2
+ addons:
3
+ postgresql: "9.3"
4
+
5
+ before_script:
6
+ - mysql -e 'create database attr_searchable;'
7
+ - psql -c 'create database attr_searchable;' -U postgres
8
+
9
+ rvm:
10
+ - 1.9.3
11
+ - 2.0.0
12
+ - 2.1.1
13
+ - rbx-2
14
+ - jruby
15
+ env:
16
+ - DATABASE=sqlite
17
+ - DATABASE=mysql
18
+ - DATABASE=postgres
19
+ matrix:
20
+ allow_failures:
21
+ - rvm: rbx-2
22
+ - rvm: jruby
23
+ fast_finish: true
24
+
25
+ gemfile:
26
+ - gemfiles/3.2.gemfile
27
+ - gemfiles/4.0.gemfile
28
+ - gemfiles/4.1.gemfile
29
+
30
+ install:
31
+ - "travis_retry bundle install"
32
+
33
+ script: "bundle exec rake test"
34
+
data/Appraisals ADDED
@@ -0,0 +1,14 @@
1
+ appraise "3.2" do
2
+ gem "activerecord", "~> 3.2.18"
3
+ gem "attr_searchable", :path => "../"
4
+ end
5
+
6
+ appraise "4.0" do
7
+ gem "activerecord", "~> 4.0.0"
8
+ gem "attr_searchable", :path => "../"
9
+ end
10
+
11
+ appraise "4.1" do
12
+ gem "activerecord", "~> 4.1.0.beta"
13
+ gem "attr_searchable", :path => "../"
14
+ end
data/Gemfile ADDED
@@ -0,0 +1,23 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in attr_searchable.gemspec
4
+ gemspec
5
+
6
+ platforms :jruby do
7
+ gem 'activerecord-jdbcmysql-adapter'
8
+ gem 'activerecord-jdbcsqlite3-adapter'
9
+ gem 'activerecord-jdbcpostgresql-adapter'
10
+ end
11
+
12
+ platforms :ruby do
13
+ gem 'sqlite3'
14
+ gem 'mysql2'
15
+ gem 'pg'
16
+ end
17
+
18
+ platforms :rbx do
19
+ gem 'racc'
20
+ gem 'rubysl', '~> 2.0'
21
+ gem 'psych'
22
+ end
23
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Benjamin Vetter
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,339 @@
1
+ # AttrSearchable
2
+
3
+ [![Build Status](https://secure.travis-ci.org/mrkamel/attr_searchable.png?branch=master)](http://travis-ci.org/mrkamel/attr_searchable)
4
+ [![Code Climate](https://codeclimate.com/github/mrkamel/attr_searchable.png)](https://codeclimate.com/github/mrkamel/attr_searchable)
5
+ [![Dependency Status](https://gemnasium.com/mrkamel/attr_searchable.png?travis)](https://gemnasium.com/mrkamel/attr_searchable)
6
+
7
+ AttrSearchable extends your ActiveRecord models to support fulltext search
8
+ engine like queries via simple query strings and hash-based queries. Assume you
9
+ have a `Book` model having various attributes like `title`, `author`, `stock`,
10
+ `price`, `available`. Using AttrSearchable you can perform:
11
+
12
+ ```ruby
13
+ Book.search("Joanne Rowling Harry Potter")
14
+ Book.search("author: Rowling title:'Harry Potter'")
15
+ Book.search("price > 10 AND price < 20 -stock:0 (Potter OR Rowling)")
16
+ # ...
17
+ ```
18
+
19
+ Thus, you can hand out a search query string to your models and you, your app's
20
+ admins and/or users will get powerful query features without the need for
21
+ integrating additional third party search servers, since AttrSearchable can use
22
+ fulltext index capabilities of your RDBMS in a database agnostic way (currently
23
+ MySQL and PostgreSQL fulltext indices are supported) and optimizes the queries
24
+ to make optimal use of them. Read more below.
25
+
26
+ Complex hash-based queries are supported as well:
27
+
28
+ ```ruby
29
+ Book.search(:author => "Rowling", :title => "Harry Potter")
30
+ Book.search(:or => [{:author => "Rowling"}, {:author => "Tolkien"}])
31
+ Book.search(:and => [{:price => {:gt => 10}}, {:not => {:stock => 0}}, :or => [{:title => "Potter"}, {:author => "Rowling"}]])
32
+ Book.search(:or => [{:query => "Rowling -Potter"}, {:query => "Tolkien -Rings"}])
33
+ # ...
34
+ ```
35
+
36
+ ## Installation
37
+
38
+ Add this line to your application's Gemfile:
39
+
40
+ gem 'attr_searchable'
41
+
42
+ And then execute:
43
+
44
+ $ bundle
45
+
46
+ Or install it yourself as:
47
+
48
+ $ gem install attr_searchable
49
+
50
+ ## Usage
51
+
52
+ To enable AttrSearchable for a model, `include AttrSearchable` and specify the
53
+ attributes you want to expose to search queries:
54
+
55
+ ```ruby
56
+ class Book < ActiveRecord::Base
57
+ include AttrSearchable
58
+
59
+ attr_searchable :title, :description, :stock, :price, :created_at, :available
60
+ attr_searchable :comment => ["comments.title", "comments.message"]
61
+ attr_searchable :author => "author.name"
62
+ # ...
63
+
64
+ has_many :comments
65
+ belongs_to :author
66
+ end
67
+ ```
68
+
69
+ ## How does it work
70
+
71
+ AttrSearchable parses the query and maps it to an SQL Query using Arel.
72
+ Thus, AttrSearchable is not bound to a specific RDBMS.
73
+
74
+ ```ruby
75
+ Book.search("stock > 0")
76
+ # ... WHERE books.stock > 0
77
+
78
+ Book.search("price > 10 stock > 0")
79
+ # ... WHERE books.price > 10 AND books.stock > 0
80
+
81
+ Book.search("Harry Potter")
82
+ # ... WHERE (books.title LIKE '%Harry%' OR books.description LIKE '%Harry%' OR ...) AND (books.title LIKE '%Potter%' OR books.description LIKE '%Potter%' ...)
83
+
84
+ Book.search("available:yes OR created_at:2014")
85
+ # ... WHERE books.available = 1 OR (books.created_at >= '2014-01-01 00:00:00' and books.created_at <= '2014-12-31 00:00:00')
86
+ ```
87
+
88
+ Of course, these `LIKE '%...%'` queries won't achieve optimal performance, but
89
+ check out the section below on AttrSearchable's fulltext capabilities to
90
+ understand how the resulting queries can be optimized.
91
+
92
+ As `Book.search(...)` returns an `ActiveRecord::Relation`, you are free to pre-
93
+ or post-process the search results in every possible way:
94
+
95
+ ```ruby
96
+ Book.where(:available => true).search("Harry Potter").order("books.id desc").paginate(:page => params[:page])
97
+ ```
98
+
99
+ ## Fulltext index capabilities
100
+
101
+ By default, i.e. if you don't tell AttrSearchable about your fulltext indices,
102
+ AttrSearchable will use `LIKE '%...%'` queries. Unfortunately, unless you
103
+ create a [trigram index](http://www.postgresql.org/docs/9.1/static/pgtrgm.html)
104
+ (postgres only), theses queries can not use SQL indices, such that every row
105
+ needs to be scanned by your RDBMS when you search for `Book.search("Harry
106
+ Potter")` or similar, which is btw. usually ok for small data sets and a small
107
+ amount of regular queries. Contrary, when you search for
108
+ `Book.search("title=Potter")` indices can and will be used. Moreover, other
109
+ indices (on price, stock, etc) will of course be used by your RDBMS when you
110
+ search for `Book.search("stock > 0")`, etc.
111
+
112
+ Regarding the `LIKE` penalty, the easiest way to make them use indices is
113
+ to remove the left wildcard. AttrSearchble supports this via:
114
+
115
+ ```ruby
116
+ class Book < ActiveRecord::Base
117
+ # ...
118
+
119
+ attr_searchable_options, :title, :left_wildcard => false
120
+
121
+ # ...
122
+ end
123
+ ```
124
+
125
+ However, this is often not desirable. Therefore, AttrSearchable can exploit the
126
+ fulltext index capabilities of MySQL and PostgreSQL. To use already existing
127
+ fulltext indices, simply tell AttrSearchable to use them via:
128
+
129
+ ```ruby
130
+ class Book < ActiveRecord::Base
131
+ # ...
132
+
133
+ attr_searchable_options :title, :type => :fulltext
134
+ attr_searchable_options :author, :type => :fulltext
135
+
136
+ # ...
137
+ end
138
+ ```
139
+
140
+ AttrSearchable will then transparently change its SQL queries for the
141
+ attributes having fulltext indices to:
142
+
143
+ ```ruby
144
+ Book.search("Harry Potter")
145
+ # MySQL: ... WHERE MATCH(books.title) AGAINST('+Harry +Potter' IN BOOLEAN MODE) OR MATCH(books.author) AGAINST('+Harry +Potter' IN BOOLEAN MODE)
146
+ # PostgreSQL: ... WHERE to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Harry & Potter') OR to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Harry & Potter')
147
+ ```
148
+
149
+ Obviously, theses queries won't always return the same results as wildcard
150
+ `LIKE` queries, because we search for words instead of substrings. However,
151
+ fulltext indices will usually of course provide better performance.
152
+
153
+ Moreover, the query above is not yet perfect. To improve it even more,
154
+ AttrSearchable tries to optimize the queries to make optimal use of fulltext
155
+ indices while still allowing to mix them with non-fulltext attributes. To
156
+ improve queries, the first thing you want to do is to specify a default field
157
+ to search in, such that AttrSearchable must no longer search within all fields:
158
+
159
+ ```ruby
160
+ attr_searchable :all => [:author, :title]
161
+ attr_searchable_options :all, :type => :fulltext, :default => true
162
+ ```
163
+
164
+ Now AttrSearchable can optimize the following, not yet optimal query:
165
+
166
+ ```ruby
167
+ BookSearch("Rowling OR Tolkien stock > 1")
168
+ # MySQL: ... WHERE (MATCH(books.author) AGAINST('+Rowling' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Tolkien' IN BOOLEAN MODE)) AND books.stock > 1
169
+ # PostgreSQL: ... WHERE (to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Rowling') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Tolkien')) AND books.stock > 1
170
+ ```
171
+
172
+ to the following, more performant query:
173
+
174
+ ```ruby
175
+ BookSearch("Rowling OR Tolkien stock > 1")
176
+ # MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('Rowling Tolkien' IN BOOLEAN MODE) AND books.stock > 1
177
+ # PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', 'Rowling | Tokien') and books.stock > 1
178
+ ```
179
+
180
+ Other queries will be optimized in a similar way, such that AttrSearchable
181
+ tries to minimize the fultext constraints within a query, namely `MATCH()
182
+ AGAINST()` for MySQL and `to_tsvector() @@ to_tsquery()` for PostgreSQL.
183
+
184
+ ```ruby
185
+ BookSearch("(Rowling -Potter) OR Tolkien")
186
+ # MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('(+Rowling -Potter) Tolkien' IN BOOLEAN MODE)
187
+ # PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', '(Rowling & !Potter) | Tolkien')
188
+ ```
189
+
190
+ To create a fulltext index on `books.title` in MySQL, simply use:
191
+
192
+ ```ruby
193
+ add_index :books, :title, :type => :fulltext
194
+ ```
195
+
196
+ Regarding compound indices, which will e.g. be used for the default field `all`
197
+ we already specified above, use:
198
+
199
+ ```ruby
200
+ add_index :books, [:author, :title], :type => :fulltext
201
+ ```
202
+
203
+ Please note that MySQL supports fulltext indices for MyISAM and, as of MySQL
204
+ version 5.6+, for InnoDB as well. For more details about MySQL fulltext indices
205
+ visit
206
+ [http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html](http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html)
207
+
208
+ Regarding PostgreSQL there are more ways to create a fulltext index. However,
209
+ one of the easiest ways is:
210
+
211
+ ```ruby
212
+ ActiveRecord::Base.connection.execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', title))"
213
+ ```
214
+
215
+ Regarding compound indices for PostgreSQL, use:
216
+
217
+ ```ruby
218
+ ActiveRecord::Base.connection.execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', author || ' ' || title))"
219
+ ```
220
+
221
+ To use another PostgreSQL dictionary than `simple`, you have to create the
222
+ index accordingly and you need tell AttrSearchable about it, e.g.:
223
+
224
+ ```ruby
225
+ attr_searchable_options :title, :dictionary => "english"
226
+ ```
227
+
228
+ For more details about PostgreSQL fulltext indices visit
229
+ [http://www.postgresql.org/docs/9.3/static/textsearch.html](http://www.postgresql.org/docs/9.3/static/textsearch.html)
230
+
231
+ ## Associations
232
+
233
+ If you specify searchable attributes from another model, like
234
+
235
+ ```ruby
236
+ class Book < ActiveRecord::Base
237
+ # ...
238
+
239
+ attr_searchable :author => "author.name"
240
+
241
+ # ...
242
+ end
243
+ ```
244
+
245
+ AttrSearchable will by default `eager_load` these associations, when you
246
+ perform `Book.search(...)`. If you don't want that or need to perform special
247
+ operations, define a `search_scope` within your model:
248
+
249
+ ```ruby
250
+ class Book < ActiveRecord::Base
251
+ # ...
252
+
253
+ scope :search_scope, lambda { joins(:author).eager_load(:comments) } # etc.
254
+
255
+ # ...
256
+ end
257
+ ```
258
+
259
+ AttrSearchable will then skip any association auto loading and will use
260
+ the `search_scope` instead.
261
+
262
+ ## Supported operators
263
+
264
+ Query string queries support `AND/and`, `OR/or`, `:`, `=`, `!=`, `<`, `<=`,
265
+ `>`, `>=`, `NOT/not/-`, `()`, `"..."` and `'...'`. Default operators are `AND`
266
+ and `matches`, `OR` has precedence over `AND`. `NOT` can only be used as infix
267
+ operator regarding a single attribute.
268
+
269
+ Hash based queries support `:and => [...]` and `:or => [...]`, which take an array
270
+ of `:not => {...}`, `:matches => {...}`, `:eq => {...}`, `:not_eq => {...}`,
271
+ `:lt => {...}`, `:lteq => {...}`, `gt => {...}`, `:gteq => {...}` and `:query => "..."`
272
+ arguments. Moreover, `:query => "..."` makes it possible to create sub-queries.
273
+ The other rules for query string queries apply to hash based queries as well.
274
+
275
+ ## Mapping
276
+
277
+ When searching in boolean, datetime, timestamp, etc. fields, AttrSearchable
278
+ performs some mapping. The following queries are equivalent:
279
+
280
+ ```ruby
281
+ Book.search("available:true")
282
+ Book.search("available:1")
283
+ Book.search("available:yes")
284
+ ```
285
+
286
+ as well as
287
+
288
+ ```ruby
289
+ Book.search("available:false")
290
+ Book.search("available:0")
291
+ Book.search("available:no")
292
+ ```
293
+
294
+ For datetime and timestamp fields, AttrSearchable expands certain values to
295
+ ranges:
296
+
297
+ ```ruby
298
+ Book.search("created_at:2014")
299
+ # ... WHERE created_at >= '2014-01-01 00:00:00' AND created_at <= '2014-12-31 23:59:59'
300
+
301
+ Book.search("created_at:2014-06")
302
+ # ... WHERE created_at >= '2014-06-01 00:00:00' AND created_at <= '2014-06-30 23:59:59'
303
+
304
+ Book.search("created_at:2014-06-15")
305
+ # ... WHERE created_at >= '2014-06-15 00:00:00' AND created_at <= '2014-06-15 23:59:59'
306
+ ```
307
+
308
+ ## Chaining
309
+
310
+ Chaining of searches is possible. However, chaining does currently not allow
311
+ AttrSearchable to optimize the individual queries for fulltext indices.
312
+
313
+ ```ruby
314
+ Book.search("Harry").search("Potter")
315
+ ```
316
+
317
+ will generate
318
+
319
+ ```ruby
320
+ # MySQL: ... WHERE MATCH(...) AGAINST('+Harry' IN BOOLEAN MODE) AND MATCH(...) AGAINST('+Potter' IN BOOLEAN MODE)
321
+ # PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry') AND to_tsvector(...) @@ to_tsquery('simple', 'Potter')
322
+ ```
323
+
324
+ instead of
325
+
326
+ ```ruby
327
+ # MySQL: ... WHERE MATCH(...) AGAINST('+Harry +Potter' IN BOOLEAN MODE)
328
+ # PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry & Potter')
329
+ ```
330
+
331
+ Thus, if you use fulltext indices, you better avoid chaining.
332
+
333
+ ## Contributing
334
+
335
+ 1. Fork it
336
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
337
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
338
+ 4. Push to the branch (`git push origin my-new-feature`)
339
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "lib"
6
+ t.pattern = "test/**/*_test.rb"
7
+ t.verbose = true
8
+ end
9
+
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'attr_searchable/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "attr_searchable"
8
+ spec.version = AttrSearchable::VERSION
9
+ spec.authors = ["Benjamin Vetter"]
10
+ spec.email = ["vetter@flakks.com"]
11
+ spec.description = %q{Complex search-engine like query support for activerecord}
12
+ spec.summary = %q{Easily perform complex search-engine like queries on your activerecord models}
13
+ spec.homepage = "https://github.com/mrkamel/attr_searchable"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "treetop"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "activerecord", ">= 3.0.0"
26
+ spec.add_development_dependency "factory_girl"
27
+ spec.add_development_dependency "appraisal"
28
+ spec.add_development_dependency "minitest"
29
+ end
@@ -0,0 +1,26 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 3.2.18"
6
+ gem "attr_searchable", :path => "../"
7
+
8
+ platforms :jruby do
9
+ gem "activerecord-jdbcmysql-adapter"
10
+ gem "activerecord-jdbcsqlite3-adapter"
11
+ gem "activerecord-jdbcpostgresql-adapter"
12
+ end
13
+
14
+ platforms :ruby do
15
+ gem "sqlite3"
16
+ gem "mysql2"
17
+ gem "pg"
18
+ end
19
+
20
+ platforms :rbx do
21
+ gem "racc"
22
+ gem "rubysl", "~> 2.0"
23
+ gem "psych"
24
+ end
25
+
26
+ gemspec :path => "../"
@@ -0,0 +1,26 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.0.0"
6
+ gem "attr_searchable", :path => "../"
7
+
8
+ platforms :jruby do
9
+ gem "activerecord-jdbcmysql-adapter"
10
+ gem "activerecord-jdbcsqlite3-adapter"
11
+ gem "activerecord-jdbcpostgresql-adapter"
12
+ end
13
+
14
+ platforms :ruby do
15
+ gem "sqlite3"
16
+ gem "mysql2"
17
+ gem "pg"
18
+ end
19
+
20
+ platforms :rbx do
21
+ gem "racc"
22
+ gem "rubysl", "~> 2.0"
23
+ gem "psych"
24
+ end
25
+
26
+ gemspec :path => "../"
@@ -0,0 +1,26 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.1.0.beta"
6
+ gem "attr_searchable", :path => "../"
7
+
8
+ platforms :jruby do
9
+ gem "activerecord-jdbcmysql-adapter"
10
+ gem "activerecord-jdbcsqlite3-adapter"
11
+ gem "activerecord-jdbcpostgresql-adapter"
12
+ end
13
+
14
+ platforms :ruby do
15
+ gem "sqlite3"
16
+ gem "mysql2"
17
+ gem "pg"
18
+ end
19
+
20
+ platforms :rbx do
21
+ gem "racc"
22
+ gem "rubysl", "~> 2.0"
23
+ gem "psych"
24
+ end
25
+
26
+ gemspec :path => "../"