stretchy 0.3.6 → 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,10 +2,31 @@ require 'stretchy/clauses/boost_clause'
2
2
 
3
3
  module Stretchy
4
4
  module Clauses
5
+ #
6
+ # Boost documents that match a free-text query. Most
7
+ # options will be passed into {#initialize}, but you
8
+ # can also chain `.not` onto it. Calling `.where` or
9
+ # `.match` from here will apply filters (*not boosts*)
10
+ # and return to the base state
11
+ #
12
+ # @author [atevans]
13
+ #
5
14
  class BoostMatchClause < BoostClause
6
15
 
7
16
  delegate [:range, :geo] => :where
8
17
 
18
+ #
19
+ # Adds a match query to the boost functions.
20
+ #
21
+ # @overload initialize(base, opts_or_string)
22
+ # @param base [Base] Base query to copy data from
23
+ # @param opts_or_string [String] String to do a free-text match across the document
24
+ #
25
+ # @overload initialize(base, opts_or_string)
26
+ # @param base [Base] Base query to copy data from
27
+ # @param options = {} [Hash] Fields and values to match via full-text search
28
+ #
29
+ # @return [BoostMatchClause] Boost clause in match context, with queries applied
9
30
  def initialize(base, opts_or_string = {}, options = {})
10
31
  super(base)
11
32
  if opts_or_string.is_a?(Hash)
@@ -17,14 +38,52 @@ module Stretchy
17
38
  end
18
39
  end
19
40
 
41
+ #
42
+ # Switches to inverse context, and applies filters as inverse
43
+ # options (ie, documents that *do not* match the query will
44
+ # be boosted)
45
+ #
46
+ # @overload not(opts_or_string)
47
+ # @param [String] String that must not match anywhere in the document
48
+ #
49
+ # @overload not(opts_or_string)
50
+ # @param opts_or_string [Hash] Fields and values that should not match in the document
51
+ #
52
+ # @return [BoostMatchClause] Query with inverse matching boost function applied
20
53
  def not(opts_or_string = {}, options = {})
21
54
  self.class.new(self, opts_or_string, options.merge(inverse: !inverse?))
22
55
  end
23
56
 
57
+ #
58
+ # Returns to the base context; filters passed here
59
+ # will be used to filter documents.
60
+ #
61
+ # @example Returning to base context
62
+ # query.boost.match('string').where(other_field: 64)
63
+ #
64
+ # @example Staying in boost context
65
+ # query.boost.match('string').boost.where(other_field: 99)
66
+ #
67
+ # @see {WhereClause#initialize}
68
+ #
69
+ # @return [WhereClause] Query with where clause applied
24
70
  def where(*args)
25
71
  WhereClause.new(self, *args)
26
72
  end
27
73
 
74
+ #
75
+ # Returns to the base context. Queries passed here
76
+ # will be used to filter documents.
77
+ #
78
+ # @example Returning to base context
79
+ # query.boost.match(message: 'curse word').match('username')
80
+ #
81
+ # @example Staying in boost context
82
+ # query.boost.match(message: 'happy word').boost.match('love')
83
+ #
84
+ # @see {MatchClause#initialize}
85
+ #
86
+ # @return [MatchClause] Base context with match queries applied
28
87
  def match(*args)
29
88
  MatchClause.new(self, *args)
30
89
  end
@@ -2,27 +2,95 @@ require 'stretchy/clauses/boost_clause'
2
2
 
3
3
  module Stretchy
4
4
  module Clauses
5
+ #
6
+ # Boosts documents that match certain filters. Most filters will
7
+ # be passed into {#initialize}, but you can also use `.range` and
8
+ # `.geo` .
9
+ #
10
+ # @author [atevans]
11
+ #
5
12
  class BoostWhereClause < BoostClause
6
13
 
14
+ #
15
+ # Generates a boost that matches a set of filters.
16
+ #
17
+ # @param base [Base] Query to copy data from.
18
+ # @param options = {} [Hash] Fields and values to filter on.
19
+ #
20
+ # @see {WhereClause#initialize}
21
+ #
22
+ # @return [BoostWhereClause] Query with filter boosts applied
7
23
  def initialize(base, options = {})
8
24
  super(base, options)
9
25
  where_function(:init, options)
10
26
  self
11
27
  end
12
28
 
29
+ #
30
+ # Returns to the base context; filters passed here
31
+ # will be used to filter documents.
32
+ #
33
+ # @example Returning to base context
34
+ # query.boost.where(number_field: 33).where(other_field: 64)
35
+ #
36
+ # @example Staying in boost context
37
+ # query.boost.where(number_field: 33).boost.where(other_field: 99)
38
+ #
39
+ # @see {WhereClause#initialize}
40
+ #
41
+ # @return [WhereClause] Query with where clause applied
13
42
  def where(*args)
14
43
  WhereClause.new(self, *args)
15
44
  end
16
45
 
46
+ #
47
+ # Returns to the base context. Queries passed here
48
+ # will be used to filter documents.
49
+ #
50
+ # @example Returning to base context
51
+ # query.boost.where(number_field: 89).match('username')
52
+ #
53
+ # @example Staying in boost context
54
+ # query.boost.where(number_field: 89).boost.match('love')
55
+ #
56
+ # @see {MatchClause#initialize}
57
+ #
58
+ # @return [MatchClause] Base context with match queries applied
17
59
  def match(*args)
18
60
  MatchClause.new(self, *args)
19
61
  end
20
62
 
63
+ #
64
+ # Applies a range filter with a min or max
65
+ # as a boost.
66
+ #
67
+ # @see {WhereClause#range}
68
+ #
69
+ # @see {Filters::RangeFilter}
70
+ #
71
+ # @see http://www.elastic.co/guide/en/elasticsearch/guide/master/_ranges.html Elastic Guides - Ranges
72
+ #
73
+ # @return [Base] Query in base context with range boost applied
21
74
  def range(*args)
22
75
  where_function(:range, *args)
23
76
  Base.new(self)
24
77
  end
25
78
 
79
+ #
80
+ # Boosts a document if it matches a geo filter.
81
+ # This is different than {BoostClause#near} -
82
+ # while `.near` applies a decay function that boosts
83
+ # based on how close a field is to a geo point,
84
+ # `.geo` applies a filter that either boosts or doesn't
85
+ # boost the document.
86
+ #
87
+ # @see {WhereFunction#geo}
88
+ #
89
+ # @see {Filters::GeoFilter}
90
+ #
91
+ # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-distance-filter.html Elastic Docs - Geo Distance Filter
92
+ #
93
+ # @return [Base] Query in base context with geo filter boost applied
26
94
  def geo(*args)
27
95
  where_function(:geo, *args)
28
96
  Base.new(self)
@@ -2,12 +2,49 @@ require 'stretchy/clauses/base'
2
2
 
3
3
  module Stretchy
4
4
  module Clauses
5
+ #
6
+ # A Match clause inherits the same state as any clause.
7
+ # There aren't any more specific methods to chain, as
8
+ # this clause only handles basic full-text searches.
9
+ #
10
+ # @author [atevans]
11
+ #
5
12
  class MatchClause < Base
6
13
 
14
+ #
15
+ # Creates a temporary MatchClause outside the main
16
+ # query scope by using a new {Base}. Primarily
17
+ # used in {BoostClause} for boosting on full-text
18
+ # matches.
19
+ #
20
+ # @param options = {} [Hash] Options to pass to the full-text match
21
+ #
22
+ # @return [MatchClause] Temporary clause outside current state
7
23
  def self.tmp(options = {})
8
24
  self.new(Base.new, options)
9
25
  end
10
26
 
27
+ #
28
+ # Creates a new state with a match query applied.
29
+ #
30
+ # @overload initialize(base, opts_or_string)
31
+ # @param [Base] Base clause to copy data from
32
+ # @param [String] Performs a full-text query for this string on all fields in the document.
33
+ #
34
+ # @overload initialize(base, opts_or_string)
35
+ # @param [Base] Base clause to copy data from
36
+ # @param [Hash] A hash of fields and values to perform full-text matches with
37
+ #
38
+ # @example A basic full-text match
39
+ # query.match("anywhere in document")
40
+ #
41
+ # @example A full-text search on specific fields
42
+ # query.match(
43
+ # my_field: "match in my_field",
44
+ # other_field: "match in other_field"
45
+ # )
46
+ #
47
+ # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html Elastic Docs - Match Query
11
48
  def initialize(base, opts_or_str = {}, options = {})
12
49
  super(base)
13
50
  if opts_or_str.is_a?(Hash)
@@ -21,14 +58,73 @@ module Stretchy
21
58
  end
22
59
  end
23
60
 
61
+ #
62
+ # Switches to inverted context. Matches applied here work the same way as
63
+ # {#initialize}, but returned documents must **not** match these filters.
64
+ #
65
+ # @overload not(opts_or_str)
66
+ # @param [String] A string that must not be matched anywhere in the document
67
+ # @overload not(opts_or_str)
68
+ # @param [Hash] A hash of fields and strings that must not be matched in those fields
69
+ #
70
+ # @return [MatchClause] inverted query state with match filters applied
71
+ #
72
+ # @example Inverted full-text
73
+ # query.match.not("hello")
74
+ #
75
+ # @example Inverted full-text matching for specific fields
76
+ # query.match.not(
77
+ # my_field: "not_match_1",
78
+ # other_field: "not_match_2"
79
+ # )
24
80
  def not(opts_or_str = {}, options = {})
25
81
  self.class.new(self, opts_or_str, options.merge(inverse: true, should: should?))
26
82
  end
27
83
 
84
+ #
85
+ # Switches to `should` context. Applies full-text matches
86
+ # that are not required, but boost the relevance score for
87
+ # matching documents.
88
+ #
89
+ # Can be chained with {#not}
90
+ #
91
+ # @overload not(opts_or_str)
92
+ # @param [String] A string that should be matched anywhere in the document
93
+ # @overload not(opts_or_str)
94
+ # @param [Hash] A hash of fields and strings that should be matched in those fields
95
+ #
96
+ # @param opts_or_str = {} [type] [description]
97
+ # @param options = {} [type] [description]
98
+ #
99
+ # @return [MatchClause] query state with should filters added
100
+ #
101
+ # @example Should match with full-text
102
+ # query.match.should("anywhere")
103
+ #
104
+ # @example Should match specific fields
105
+ # query.match.should(
106
+ # field_one: "one",
107
+ # field_two: "two"
108
+ # )
109
+ #
110
+ # @example Should not match
111
+ # query.match.should.not(
112
+ # field_one: "one",
113
+ # field_two: "two"
114
+ # )
115
+ #
116
+ # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html Elastic Docs - Bool Query
28
117
  def should(opts_or_str = {}, options = {})
29
118
  self.class.new(self, opts_or_str, options.merge(should: true))
30
119
  end
31
120
 
121
+ #
122
+ # Converts this match context to a set of boosts
123
+ # to use in a {Stretchy::Queries::FunctionScoreQuery}
124
+ #
125
+ # @param weight = nil [Numeric] Weight of generated boost
126
+ #
127
+ # @return [Stretchy::Boosts::FilterBoost] boost containing these match parameters
32
128
  def to_boost(weight = nil)
33
129
  weight ||= Stretchy::Boosts::FilterBoost::DEFAULT_WEIGHT
34
130
  Stretchy::Boosts::FilterBoost.new(
@@ -39,6 +135,10 @@ module Stretchy
39
135
  )
40
136
  end
41
137
 
138
+ #
139
+ # Accessor for `@should`
140
+ #
141
+ # @return [true, false] `@should`
42
142
  def should?
43
143
  !!@should
44
144
  end
@@ -2,12 +2,62 @@ require 'stretchy/clauses/base'
2
2
 
3
3
  module Stretchy
4
4
  module Clauses
5
+ #
6
+ # A Where clause inherits the same state as any clause,
7
+ # but has a few possible states to transition to. You
8
+ # can call {#range} and {#geo} to add their respective
9
+ # filters, or you can transition to the inverted state
10
+ # via {#not}, or the `should` state via {#should}
11
+ #
12
+ # ### STATES:
13
+ # * **inverted:** any filters added in this state will be
14
+ # inverted, ie the document must **NOT** match said
15
+ # filters.
16
+ # * **should:** any filters added in this state will be
17
+ # applied to a `should` block. Documents which do
18
+ # not match these filters will be returned, but
19
+ # documents which do match will have a higher
20
+ # relevance score.
21
+ #
22
+ # @author [atevans]
23
+ #
5
24
  class WhereClause < Base
6
25
 
26
+ #
27
+ # Creates a temporary context by initializing a new Base object.
28
+ # Used primarily in {BoostWhereClause}
29
+ #
30
+ # @param options = {} [Hash] Options to filter on
31
+ #
32
+ # @return [WhereClause] A clause outside the main query context
7
33
  def self.tmp(options = {})
8
34
  self.new(Base.new, options)
9
35
  end
10
36
 
37
+ #
38
+ # Options passed to the initializer will be interpreted as filters
39
+ # to be added to the query. This is similar to ActiveRecord's `where`
40
+ # method.
41
+ #
42
+ # @param base [Base] Used to intialize the new state from the previous clause
43
+ # @param options = {} [Hash] filters to be applied to the new state
44
+ # @option options [true, false] :inverted (nil) Whether the new state is inverted
45
+ # @option options [true, false] :should (nil) Whether the new state is should
46
+ #
47
+ # @example Apply ActiveRecord-like filters
48
+ # query.where(
49
+ # string_field: "string",
50
+ # must_not_exist: nil,
51
+ # in_range: 27..33,
52
+ # included_in: [47, 23, 86]
53
+ # )
54
+ #
55
+ # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-filter.html Elastic Docs - Terms Filter
56
+ #
57
+ # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-filter.html Elastic Docs - Exists Filter
58
+ #
59
+ # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-filter.html Elastic Docs - Range Filter
60
+ #
11
61
  def initialize(base, options = {})
12
62
  super(base)
13
63
  @inverse = options.delete(:inverse)
@@ -15,15 +65,62 @@ module Stretchy
15
65
  add_params(options)
16
66
  end
17
67
 
68
+ #
69
+ # Accessor for `@should`
70
+ #
71
+ # @return [true, false] `@should`
18
72
  def should?
19
73
  !!@should
20
74
  end
21
75
 
76
+ #
77
+ # Add a range filter to the current context. While
78
+ # you can pass a `Range` object to {#where}, this
79
+ # allows you to specify an open-ended range, such
80
+ # as only specifying the minimum or maximum value.
81
+ #
82
+ # @param field [String, Symbol] The field to filter with this range
83
+ # @param options = {} [Hash] Options for the range
84
+ # @option options [Numeric] :min (nil) Minimum. Ranges are _inclusive_ by default
85
+ # @option options [Numeric] :max (nil) Maximum. Ranges are _inclusive_ by default
86
+ # @option options [true, false] :exclusive (nil) Overrides default and makes the range exclusive -
87
+ # equivalent to passing both `:exclusive_min` and `:exclusive_max`
88
+ # @option options [true, false] :exclusive_min (nil) Overrides default and makes the minimum exclusive
89
+ # @option options [true, false] :exclusive_max (nil) Overrides default and makes the maximum exclusive
90
+ #
91
+ # @return [self] query state with range filter applied
92
+ #
93
+ # @example Adding a range filter
94
+ # query.where.range(:my_range_field,
95
+ # min: 33,
96
+ # exclusive: true
97
+ # )
22
98
  def range(field, options = {})
23
99
  get_storage(:ranges)[field] = Stretchy::Types::Range.new(options)
24
100
  self
25
101
  end
26
102
 
103
+ #
104
+ # Adds a geo distance filter to the current context.
105
+ # Documents must have a `geo_point` field that is within
106
+ # the specified distance of the passed parameters.
107
+ #
108
+ # @param field [String, Symbol] The field this filter will be applied to.
109
+ # @param options = {} [Hash] Options for the geo distance filter
110
+ # @option options [String] :distance (nil) The maximum distance from the specified origin.
111
+ # Use an Elastic distance format such as `'21mi'` or `'37km'`
112
+ # @option options [Float] :lat (nil) The latitude of the origin point. Can also be specified as `:latitude`
113
+ # @option options [Float] :lng (nil) The longitude of the origin point.
114
+ # Can also be specified as `:lon` or `:longitude`
115
+ #
116
+ # @return [self] query state with geo distance filter applied
117
+ #
118
+ # @example Searching by distance from a point
119
+ # query.where.geo(:coords,
120
+ # distance: '27km',
121
+ # lat: 33.3,
122
+ # lng: 29.2
123
+ # )
27
124
  def geo(field, options = {})
28
125
  get_storage(:geos)[field] = {
29
126
  distance: options[:distance],
@@ -32,14 +129,65 @@ module Stretchy
32
129
  self
33
130
  end
34
131
 
132
+ #
133
+ # Switches current state to inverted. Options passed
134
+ # here are equivalent to those passed to {#initialize},
135
+ # except documents *must not* match these filters.
136
+ #
137
+ # Can be chained with {#should} to produce inverted should queries
138
+ #
139
+ # @param options = {} [Hash] Options to filter on
140
+ #
141
+ # @return [WhereClause] inverted query state with not filters applied.
142
+ #
143
+ # @example Inverting filters
144
+ # query.where.not(
145
+ # must_exist: nil,
146
+ # not_matching: "this string",
147
+ # not_in: [45, 67, 99],
148
+ # not_in_range: 89..23
149
+ # )
150
+ #
151
+ # @example Inverted should filters
152
+ # query.should.not(
153
+ # match_field: [:these, "options"]
154
+ # )
35
155
  def not(options = {})
36
156
  self.class.new(self, options.merge(inverse: true, should: should?))
37
157
  end
38
158
 
159
+ #
160
+ # Switches the current state to `should`. Options passed
161
+ # here are equivalent to those passed to {#initialize},
162
+ # except documents which do not match are still returned
163
+ # with a lower score than documents which do match.
164
+ #
165
+ # Can be chained with {#not} to produce inverted should queries
166
+ #
167
+ # @param options = {} [Hash] Options to filter on
168
+ #
169
+ # @return [WhereClause] should query state with should filters applied
170
+ #
171
+ # @example Specifying should options
172
+ # query.should(
173
+ # field: [99, 27]
174
+ # )
175
+ #
176
+ # @example Inverted should options
177
+ # query.should.not(
178
+ # exists_field: nil
179
+ # )
39
180
  def should(options = {})
40
181
  self.class.new(self, options.merge(should: true))
41
182
  end
42
183
 
184
+ #
185
+ # Converts the current context into a boost to
186
+ # be passed into a {FunctionScoreQuery}.
187
+ #
188
+ # @param weight = nil [Numeric] A weight for the {FunctionScoreQuery}
189
+ #
190
+ # @return [Boosts::FilterBoost] A boost including all the current filters
43
191
  def to_boost(weight = nil)
44
192
  weight ||= Stretchy::Boosts::FilterBoost::DEFAULT_WEIGHT
45
193
 
@@ -26,7 +26,14 @@ module Stretchy
26
26
  end
27
27
 
28
28
  def response
29
- @response ||= Stretchy.search(type: type, body: request, from: offset, size: limit)
29
+ params = {
30
+ type: type,
31
+ body: request,
32
+ from: offset,
33
+ size: limit
34
+ }
35
+ params[:explain] = true if clause.get_explain
36
+ @response ||= Stretchy.search(params)
30
37
  end
31
38
 
32
39
  def ids
@@ -41,6 +48,18 @@ module Stretchy
41
48
  end
42
49
  alias :results :hits
43
50
 
51
+ def scores
52
+ @scores ||= Hash[response['hits']['hits'].map do |hit|
53
+ [hit['_id'], hit['_score']]
54
+ end]
55
+ end
56
+
57
+ def explanations
58
+ @scores ||= Hash[response['hits']['hits'].map do |hit|
59
+ [hit['_id'], hit['_explanation']]
60
+ end]
61
+ end
62
+
44
63
  def took
45
64
  @took ||= response['took']
46
65
  end
@@ -8,12 +8,14 @@ module Stretchy
8
8
  end
9
9
  end
10
10
 
11
- # used for ensuring a concistent index in specs
11
+ # used for ensuring a consistent index in specs
12
12
  def refresh
13
+ Stretchy.log("Refreshing index: #{index_name}")
13
14
  client.indices.refresh index: index_name
14
15
  end
15
16
 
16
17
  def count
18
+ Stretchy.log("Counting all documents in index: #{index_name}")
17
19
  client.cat.count(index: index_name).split(' ')[2].to_i
18
20
  end
19
21
 
@@ -30,7 +32,10 @@ module Stretchy
30
32
  params[field] = options[field] if options[field]
31
33
  end
32
34
 
33
- client.search(params)
35
+ Stretchy.log("Querying Elastic:", params)
36
+ response = client.search(params)
37
+ Stretchy.log("Received response:", response)
38
+ response
34
39
  end
35
40
 
36
41
  def index(options = {})
@@ -38,7 +43,16 @@ module Stretchy
38
43
  type = options[:type]
39
44
  body = options[:body]
40
45
  id = options[:id] || options['id'] || body['id'] || body['_id'] || body[:id] || body[:_id]
41
- client.index(index: index, type: type, id: id, body: body)
46
+ params = {
47
+ index: index,
48
+ type: type,
49
+ id: id,
50
+ body: body
51
+ }
52
+ Stretchy.log("Indexing document:", params)
53
+ response = client.index(params)
54
+ Stretchy.log("Received response:", response)
55
+ response
42
56
  end
43
57
 
44
58
  def bulk(options = {})
@@ -51,23 +65,29 @@ module Stretchy
51
65
  document
52
66
  ]
53
67
  end
54
- client.bulk body: requests
68
+ Stretchy.log("Bulk indexing documents:", {body: requests})
69
+ response = client.bulk body: requests
70
+ Stretchy.log("Received response:", response)
55
71
  end
56
72
 
57
73
  def exists(_index_name = index_name)
74
+ Stretchy.log("Checking index existence for: #{_index_name}")
58
75
  client.indices.exists(index: _index_name)
59
76
  end
60
77
  alias :exists? :exists
61
78
 
62
79
  def delete(_index_name = index_name)
80
+ Stretchy.log("Deleting index: #{_index_name}")
63
81
  client.indices.delete(index: _index_name) if exists?(_index_name)
64
82
  end
65
83
 
66
84
  def create(_index_name = index_name)
85
+ Stretchy.log("Creating index: #{_index_name}")
67
86
  client.indices.create(index: _index_name) unless exists?(_index_name)
68
87
  end
69
88
 
70
89
  def mapping(_index_name, _type, _body)
90
+ Stretchy.log("Putting mapping:", {index_name: _index_name, type: _type, body: _body})
71
91
  client.indices.put_mapping(index: _index_name, type: _type, body: _body)
72
92
  end
73
93
 
@@ -0,0 +1,71 @@
1
+ module Stretchy
2
+ module Utils
3
+ class Colorize
4
+ COLORS = {
5
+ "default" => "38",
6
+ "black" => "30",
7
+ "red" => "31",
8
+ "green" => "32",
9
+ "brown" => "33",
10
+ "blue" => "34",
11
+ "purple" => "35",
12
+ "cyan" => "36",
13
+ "gray" => "37",
14
+ "dark gray" => "1;30",
15
+ "light red" => "1;31",
16
+ "light green" => "1;32",
17
+ "yellow" => "1;33",
18
+ "light blue" => "1;34",
19
+ "light purple" => "1;35",
20
+ "light cyan" => "1;36",
21
+ "white" => "1;37"
22
+ }.freeze
23
+
24
+ BG_COLORS = {
25
+ "default" => "0",
26
+ "black" => "40",
27
+ "red" => "41",
28
+ "green" => "42",
29
+ "brown" => "43",
30
+ "blue" => "44",
31
+ "purple" => "45",
32
+ "cyan" => "46",
33
+ "gray" => "47",
34
+ "dark gray" => "100",
35
+ "light red" => "101",
36
+ "light green" => "102",
37
+ "yellow" => "103",
38
+ "light blue" => "104",
39
+ "light purple" => "105",
40
+ "light cyan" => "106",
41
+ "white" => "107"
42
+ }.freeze
43
+
44
+ def colorize(string, color = "default", bg = "default")
45
+ color_code = COLORS[color]
46
+ bg_code = BG_COLORS[bg]
47
+ return "\033[#{bg_code};#{color_code}m#{string}\033[0m"
48
+ end
49
+
50
+ module ClassMethods
51
+ def colors
52
+ Colorize::COLORS
53
+ end
54
+
55
+ def bgs
56
+ Colorize::BG_COLORS
57
+ end
58
+
59
+ Colorize::COLORS.keys.each do |color|
60
+ define_method color do |string|
61
+ color_code = colors[color]
62
+ bg_code = bgs["default"]
63
+ string.split("\n").map{|s| "\033[#{bg_code};#{color_code}m#{s}\033[0m" }.join("\n")
64
+ end
65
+ end
66
+ end
67
+
68
+ extend ClassMethods
69
+ end
70
+ end
71
+ end