bloodhound 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 +1 -0
- data/README.markdown +118 -0
- data/bloodhound.gemspec +27 -0
- data/lib/bloodhound.rb +63 -0
- data/lib/bloodhound/active_record.rb +43 -0
- data/spec/bloodhound/active_record_spec.rb +44 -0
- data/spec/bloodhound_spec.rb +107 -0
- data/spec/spec_helper.rb +18 -0
- metadata +62 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
dist
|
data/README.markdown
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
Bloodhound
|
2
|
+
==========
|
3
|
+
|
4
|
+
Simple key:value string conversion to hashes, with type casting.
|
5
|
+
|
6
|
+
require "bloodhound"
|
7
|
+
|
8
|
+
hound = Bloodhound.new
|
9
|
+
hound.add_search_field(:name, :string)
|
10
|
+
hound.add_search_field(:age, :integer)
|
11
|
+
hound.add_search_field(:active, :boolean)
|
12
|
+
|
13
|
+
attributes = hound.attributes_from('name:"John Doe" age:22 active:yes')
|
14
|
+
attributes #=> { "name" => "John Doe", "age" => 22, "active" => true }
|
15
|
+
|
16
|
+
The available types are `:string` (the default), `:integer`, `:float`, `:date`,
|
17
|
+
`:time`, and `:boolean`. Any other type is treated as a string.
|
18
|
+
|
19
|
+
It matches several values as boolean values. 'yes', 'y', and 'true' are all
|
20
|
+
mapped to `true`, while 'no', 'n', and 'false' are mapped to `false`.
|
21
|
+
|
22
|
+
You can customize the hash key returned:
|
23
|
+
|
24
|
+
hound.add_search_field(:user, :string, "users.name")
|
25
|
+
|
26
|
+
attributes = hound.attributes_from('user:"John Doe"')
|
27
|
+
attributes #=> { "users.name" => "John Doe" }
|
28
|
+
|
29
|
+
It can match dates and times, using [Chronic](http://github.com/mojombo/chronic),
|
30
|
+
so this is valid:
|
31
|
+
|
32
|
+
hound.add_search_field(:added, :date, "added_on")
|
33
|
+
|
34
|
+
attributes = hound.attributes_from("added:today")
|
35
|
+
attributes #=> { "added_on" => Date.today }
|
36
|
+
|
37
|
+
Finally, you can also provide processing rules for the parsed value, by passing
|
38
|
+
a block:
|
39
|
+
|
40
|
+
hound.add_search_field(:inactive, :boolean, "active") {|value| not value }
|
41
|
+
|
42
|
+
attributes = hound.attributes_from("inactive:true")
|
43
|
+
attributes #=> { "active" => false }
|
44
|
+
|
45
|
+
ActiveRecord integration
|
46
|
+
------------------------
|
47
|
+
|
48
|
+
require "bloodhound/active_record"
|
49
|
+
|
50
|
+
class Video < ActiveRecord::Base
|
51
|
+
extend Bloodhound::Searchable
|
52
|
+
named_scope :search, lambda {|query| bloodhound.attributes_from(query) }
|
53
|
+
end
|
54
|
+
|
55
|
+
The ActiveRecord implementation will automatically define search fields for all
|
56
|
+
non-id, non-timestamp columns (ie, all except for `id` and `foo_id`).
|
57
|
+
|
58
|
+
You can, of course, add those in manually if you need them.
|
59
|
+
|
60
|
+
The syntax for defining attributes is a bit more rails-esque:
|
61
|
+
|
62
|
+
class Video < ActiveRecord::Base
|
63
|
+
extend Bloodhound::Searchable
|
64
|
+
search_field :user, :type => :string, :attribute => "users.login"
|
65
|
+
end
|
66
|
+
|
67
|
+
The return value of `ActiveRecord::Bloodhound#attributes_from` changes a bit,
|
68
|
+
and returns a hash directly compatible with ActiveRecord:
|
69
|
+
|
70
|
+
attributes = Video.bloodhound.attributes_from("user:foca")
|
71
|
+
attributes #=> { :conditions => { "users.login" => "foca" } }
|
72
|
+
|
73
|
+
Any extra options you pass to `search_field` are added into the finder options:
|
74
|
+
|
75
|
+
class Video < ActiveRecord::Base
|
76
|
+
extend Bloodhound::Searchable
|
77
|
+
search_field :user, :attribute => "users.login", :joins => :user
|
78
|
+
|
79
|
+
belongs_to :user
|
80
|
+
end
|
81
|
+
|
82
|
+
attributes = Video.bloodhound.attributes_from("user:foca")
|
83
|
+
attributes #=> { :joins => :user,
|
84
|
+
:conditions => { "users.login" => "foca" } }
|
85
|
+
|
86
|
+
Known problems
|
87
|
+
--------------
|
88
|
+
|
89
|
+
* Chronic is a bit… weird matching some stuff, specially regarding time zones.
|
90
|
+
* The ActiveRecord 'extra options' won't merge. So if you define two search
|
91
|
+
fields with ':joins => :an_association', only the latter will remain. This
|
92
|
+
will be fixed in a future release.
|
93
|
+
|
94
|
+
License
|
95
|
+
-------
|
96
|
+
|
97
|
+
(The MIT License)
|
98
|
+
|
99
|
+
Copyright (c) 2010 Nicolas Sanguinetti, http://nicolassanguinetti.info
|
100
|
+
|
101
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
102
|
+
a copy of this software and associated documentation files (the
|
103
|
+
'Software'), to deal in the Software without restriction, including
|
104
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
105
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
106
|
+
permit persons to whom the Software is furnished to do so, subject to
|
107
|
+
the following conditions:
|
108
|
+
|
109
|
+
The above copyright notice and this permission notice shall be
|
110
|
+
included in all copies or substantial portions of the Software.
|
111
|
+
|
112
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
113
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
114
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
115
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
116
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
117
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
118
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/bloodhound.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "bloodhound"
|
3
|
+
s.version = "0.1"
|
4
|
+
s.date = "2010-01-15"
|
5
|
+
|
6
|
+
s.description = "Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }"
|
7
|
+
s.summary = "Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }"
|
8
|
+
s.homepage = "http://github.com/foca/bloodhound"
|
9
|
+
|
10
|
+
s.authors = ["Nicolás Sanguinetti"]
|
11
|
+
s.email = "contacto@nicolassanguinetti.info"
|
12
|
+
|
13
|
+
s.require_paths = ["lib"]
|
14
|
+
s.has_rdoc = false
|
15
|
+
|
16
|
+
s.files = %w[
|
17
|
+
.gitignore
|
18
|
+
README.markdown
|
19
|
+
bloodhound.gemspec
|
20
|
+
lib/bloodhound.rb
|
21
|
+
lib/bloodhound/active_record.rb
|
22
|
+
spec/spec_helper.rb
|
23
|
+
spec/bloodhound_spec.rb
|
24
|
+
spec/bloodhound/active_record_spec.rb
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
data/lib/bloodhound.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require "chronic"
|
2
|
+
|
3
|
+
class Bloodhound
|
4
|
+
VERSION = "0.1"
|
5
|
+
ATTRIBUTE_RE = /\s*(\S+):(?:"([^"]*)"|'([^']*)'|(\S+))\s*/.freeze
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@mappings = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_search_field(name, type=:string, attribute=name, &mapping)
|
12
|
+
@mappings[name.to_sym] = [attribute, type.to_sym, mapping]
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes_from(query)
|
16
|
+
parse_query(query).inject({}) do |conditions, (key,value)|
|
17
|
+
attribute, type, mapping = @mappings.fetch(key.to_sym, [])
|
18
|
+
|
19
|
+
if attribute && type
|
20
|
+
conditions[attribute.to_s] = (mapping || default_mapping).call(cast_value(value, type))
|
21
|
+
end
|
22
|
+
|
23
|
+
conditions
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def default_mapping
|
28
|
+
lambda {|value| value }
|
29
|
+
end
|
30
|
+
private :default_mapping
|
31
|
+
|
32
|
+
def cast_value(value, type)
|
33
|
+
case type
|
34
|
+
when :boolean
|
35
|
+
{ "true" => true,
|
36
|
+
"yes" => true,
|
37
|
+
"y" => true,
|
38
|
+
"false" => false,
|
39
|
+
"no" => false,
|
40
|
+
"n" => false }.fetch(value.downcase, true)
|
41
|
+
when :integer
|
42
|
+
# Kernel#Float is a bit more lax about parsing numbers, like
|
43
|
+
# Integer(0.0) fails, when we just want it interpreted as a zero
|
44
|
+
Float(value).to_i
|
45
|
+
when :float, :decimal
|
46
|
+
Float(value)
|
47
|
+
when :date
|
48
|
+
Date.parse(Chronic.parse(value).to_s)
|
49
|
+
when :time, :datetime
|
50
|
+
Chronic.parse(value)
|
51
|
+
else
|
52
|
+
value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
private :cast_value
|
56
|
+
|
57
|
+
def parse_query(query)
|
58
|
+
query.scan(ATTRIBUTE_RE).map do |(key,*value)|
|
59
|
+
[key, value.compact.first]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
private :parse_query
|
63
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "bloodhound"
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class Bloodhound < ::Bloodhound
|
5
|
+
def initialize
|
6
|
+
super()
|
7
|
+
@extra_options = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_search_field(name, type=:string, attribute=name, options={}, &mapping)
|
11
|
+
super(name, type, attribute, &mapping)
|
12
|
+
@extra_options[attribute.to_s] = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes_from(query)
|
16
|
+
super(query).inject({}) do |finder_options, (name, value)|
|
17
|
+
finder_options[:conditions] ||= {}
|
18
|
+
finder_options[:conditions].update(name => value)
|
19
|
+
finder_options.update(@extra_options.fetch(name, {}))
|
20
|
+
finder_options
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Bloodhound::Searchable
|
27
|
+
def bloodhound
|
28
|
+
@bloodhound ||= ActiveRecord::Bloodhound.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def search_field(name, options={}, &mapping)
|
32
|
+
attribute = options.delete(:attribute) || name
|
33
|
+
type = options.delete(:type) || :string
|
34
|
+
bloodhound.add_search_field(name, type, attribute, options, &mapping)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.extended(model)
|
38
|
+
model.columns.each do |column|
|
39
|
+
next if column.name.to_s =~ /_?id$/
|
40
|
+
model.search_field column.name, :type => column.type
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "bloodhound/active_record"
|
3
|
+
|
4
|
+
describe ActiveRecord::Bloodhound do
|
5
|
+
it "returns a hash with { :conditions => {keys matched} } instead of just the keys" do
|
6
|
+
subject.add_search_field(:foo)
|
7
|
+
subject.attributes_from("foo:bar").should == { :conditions => { "foo" => "bar" } }
|
8
|
+
end
|
9
|
+
|
10
|
+
it "allows for extra options, that get merged into the return hash" do
|
11
|
+
subject.add_search_field(:user, :string, "users.name", :joins => :users)
|
12
|
+
subject.attributes_from("user:'John Doe'").should == {
|
13
|
+
:joins => :users,
|
14
|
+
:conditions => { "users.name" => "John Doe" }
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe Bloodhound::Searchable do
|
20
|
+
Column = Struct.new(:name, :type)
|
21
|
+
|
22
|
+
class MockModel
|
23
|
+
def self.columns
|
24
|
+
[Column.new(:id, :integer), Column.new(:name, :string), Column.new(:user_id, :integer)]
|
25
|
+
end
|
26
|
+
|
27
|
+
extend Bloodhound::Searchable
|
28
|
+
end
|
29
|
+
|
30
|
+
it "allows accessing the bloodhound object in the model by calling Model.bloodhound" do
|
31
|
+
MockModel.bloodhound.should be_an(ActiveRecord::Bloodhound)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "adds search fields for the non-id fields in the model" do
|
35
|
+
conditions = MockModel.bloodhound.attributes_from("name:John id:3 user_id:5").fetch(:conditions)
|
36
|
+
conditions.should_not have_key("id")
|
37
|
+
conditions.should_not have_key("user_id")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "allows you to add search fields with a more ActiveRecord-ish syntax" do
|
41
|
+
MockModel.search_field :user, :type => :string, :attribute => "users.name"
|
42
|
+
MockModel.bloodhound.attributes_from("user:John").should == { :conditions => { "users.name" => "John" } }
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Bloodhound do
|
4
|
+
context "adding fields" do
|
5
|
+
it "can specify a type of :string" do
|
6
|
+
subject.add_search_field(:some_string, :string)
|
7
|
+
{ "some_string" => "falafel" }.should be_included_in_extracted_attributes_from("some_string:falafel")
|
8
|
+
end
|
9
|
+
|
10
|
+
it "can specify a type of :integer" do
|
11
|
+
subject.add_search_field(:some_number, :integer)
|
12
|
+
{ "some_number" => 1 }.should be_included_in_extracted_attributes_from("some_number:1")
|
13
|
+
end
|
14
|
+
|
15
|
+
it "can specify a type of :float" do
|
16
|
+
subject.add_search_field(:some_number, :float)
|
17
|
+
{ "some_number" => 1.5 }.should be_included_in_extracted_attributes_from("some_number:1.5")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can specify a type of :boolean" do
|
21
|
+
subject.add_search_field(:some_boolean, :boolean)
|
22
|
+
{ "some_boolean" => true }.should be_included_in_extracted_attributes_from("some_boolean:true")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "can specify a type of :date" do
|
26
|
+
subject.add_search_field(:some_date, :date)
|
27
|
+
{ "some_date" => Date.parse('2010-01-05') }.should be_included_in_extracted_attributes_from("some_date:2010-01-05")
|
28
|
+
end
|
29
|
+
|
30
|
+
it "can specifiy a type of :time" do
|
31
|
+
subject.add_search_field(:some_time, :time)
|
32
|
+
{ "some_time" => Time.mktime(2010, 1, 5, 12, 0, 0) }.should be_included_in_extracted_attributes_from("some_time:'2010-01-05 12:00:00'")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can specify a type of :text" do
|
36
|
+
subject.add_search_field(:some_text, :text)
|
37
|
+
{ "some_text" => "Hello world" }.should be_included_in_extracted_attributes_from("some_text:'Hello world'")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "defaults to a :string field" do
|
41
|
+
subject.add_search_field(:some_string)
|
42
|
+
{ "some_string" => "falafel" }.should be_included_in_extracted_attributes_from("some_string:falafel")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "matching quoted values" do
|
47
|
+
before { subject.add_search_field(:foo) }
|
48
|
+
|
49
|
+
it "matches unquoted values" do
|
50
|
+
{ "foo" => "bar" }.should be_included_in_extracted_attributes_from("foo:bar")
|
51
|
+
{ "foo" => "b'a'r" }.should be_included_in_extracted_attributes_from("foo:b'a'r")
|
52
|
+
{ "foo" => "ba'r" }.should be_included_in_extracted_attributes_from("foo:ba'r")
|
53
|
+
end
|
54
|
+
|
55
|
+
it "matches values within double quotes" do
|
56
|
+
{ "foo" => "this is awesome" }.should be_included_in_extracted_attributes_from(%Q(foo:"this is awesome"))
|
57
|
+
{ "foo" => "this is 'tricky'" }.should be_included_in_extracted_attributes_from(%Q(foo:"this is 'tricky'"))
|
58
|
+
{ "foo" => "'this is even trickier'" }.should be_included_in_extracted_attributes_from(%Q(foo:"'this is even trickier'"))
|
59
|
+
end
|
60
|
+
|
61
|
+
it "matches values within single quotes" do
|
62
|
+
{ "foo" => 'this is awesome' }.should be_included_in_extracted_attributes_from(%Q(foo:'this is awesome'))
|
63
|
+
{ "foo" => 'this is "tricky"' }.should be_included_in_extracted_attributes_from(%Q(foo:'this is "tricky"'))
|
64
|
+
{ "foo" => '"this is even trickier"' }.should be_included_in_extracted_attributes_from(%Q(foo:'"this is even trickier"'))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "casting values" do
|
69
|
+
it "matches 'yes', 'true', 'y', 'no', 'false', and 'n' as boolean values" do
|
70
|
+
subject.add_search_field(:foo, :boolean)
|
71
|
+
{ "foo" => true }.should be_included_in_extracted_attributes_from("foo:yes")
|
72
|
+
{ "foo" => true }.should be_included_in_extracted_attributes_from("foo:y")
|
73
|
+
{ "foo" => true }.should be_included_in_extracted_attributes_from("foo:true")
|
74
|
+
{ "foo" => false }.should be_included_in_extracted_attributes_from("foo:no")
|
75
|
+
{ "foo" => false }.should be_included_in_extracted_attributes_from("foo:n")
|
76
|
+
{ "foo" => false }.should be_included_in_extracted_attributes_from("foo:false")
|
77
|
+
end
|
78
|
+
|
79
|
+
it "parses integers as Integer objects" do
|
80
|
+
subject.add_search_field(:foo, :integer)
|
81
|
+
subject.add_search_field(:bar, :integer)
|
82
|
+
{ "foo" => 1, "bar" => 2 }.should be_included_in_extracted_attributes_from("foo:1 bar:2.8")
|
83
|
+
end
|
84
|
+
|
85
|
+
it "parses decimal values as Float objects" do
|
86
|
+
subject.add_search_field(:foo, :float)
|
87
|
+
{ "foo" => 1.5 }.should be_included_in_extracted_attributes_from("foo:1.5")
|
88
|
+
end
|
89
|
+
|
90
|
+
it "parses dates as instances of Date" do
|
91
|
+
subject.add_search_field(:foo, :date)
|
92
|
+
{ "foo" => Date.today }.should be_included_in_extracted_attributes_from("foo:today")
|
93
|
+
end
|
94
|
+
|
95
|
+
it "parses times as instances of Time" do
|
96
|
+
subject.add_search_field(:foo, :time)
|
97
|
+
{ "foo" => Time.mktime(2010, 1, 5, 12, 0, 0) }.should be_included_in_extracted_attributes_from("foo:2010-01-05 12:00:00")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "processing matched values with user-defined lambdas" do
|
102
|
+
it "lets you convert matched values according to your business rules" do
|
103
|
+
subject.add_search_field(:incomplete, :boolean, "complete") {|value| not value }
|
104
|
+
{ "complete" => false }.should be_included_in_extracted_attributes_from("incomplete:yes")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "spec"
|
2
|
+
require "ostruct"
|
3
|
+
require "bloodhound"
|
4
|
+
|
5
|
+
Spec::Runner.configure do |config|
|
6
|
+
module CustomMatchers
|
7
|
+
def be_included_in_extracted_attributes_from(attribute_string)
|
8
|
+
simple_matcher :other_set_of_attributes do |given|
|
9
|
+
expected = subject.attributes_from(attribute_string)
|
10
|
+
given.each do |key, value|
|
11
|
+
expected.fetch(key).should == value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
config.include CustomMatchers
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bloodhound
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Nicol\xC3\xA1s Sanguinetti"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-15 00:00:00 -02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }
|
17
|
+
email: contacto@nicolassanguinetti.info
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- .gitignore
|
26
|
+
- README.markdown
|
27
|
+
- bloodhound.gemspec
|
28
|
+
- lib/bloodhound.rb
|
29
|
+
- lib/bloodhound/active_record.rb
|
30
|
+
- spec/spec_helper.rb
|
31
|
+
- spec/bloodhound_spec.rb
|
32
|
+
- spec/bloodhound/active_record_spec.rb
|
33
|
+
has_rdoc: false
|
34
|
+
homepage: http://github.com/foca/bloodhound
|
35
|
+
licenses: []
|
36
|
+
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
version:
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.3.5
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }
|
61
|
+
test_files: []
|
62
|
+
|