├── .gitignore
├── Appraisals
├── Gemfile
├── gemfiles
└── fluentd_v0.14.gemfile
├── test
├── helper.rb
└── plugin
│ ├── test_out_twitter.rb
│ └── test_in_twitter.rb
├── .travis.yml
├── Rakefile
├── LICENSE.txt
├── fluent-plugin-twitter.gemspec
├── lib
└── fluent
│ └── plugin
│ ├── out_twitter.rb
│ └── in_twitter.rb
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 | vendor/*
6 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "fluentd v0.14" do
2 | gem "fluentd", "~> 0.14.0"
3 | end
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in fluent-plugin-twitter.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/gemfiles/fluentd_v0.14.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "fluentd", "~> 0.14.0"
6 |
7 | gemspec :path => "../"
8 |
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | require 'test/unit'
3 |
4 | $LOAD_PATH.unshift(File.join(__dir__, '..', 'lib'))
5 | $LOAD_PATH.unshift(__dir__)
6 | require 'fluent/test'
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: ruby
3 |
4 | rvm:
5 | - 2.4.1
6 | - 2.3.4
7 | - 2.2.5
8 | - 2.1.10
9 |
10 | before_install:
11 | - gem update bundler
12 |
13 | gemfile:
14 | - Gemfile
15 | - gemfiles/fluentd_v0.14.gemfile
16 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 | Rake::TestTask.new(:test) do |test|
4 | test.libs << 'lib' << 'test'
5 | test.pattern = 'test/**/test_*.rb'
6 | test.verbose = true
7 | end
8 |
9 | task :default => :test
10 |
11 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012- Kentaro Yoshida
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
15 |
--------------------------------------------------------------------------------
/fluent-plugin-twitter.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 |
3 | Gem::Specification.new do |s|
4 | s.name = "fluent-plugin-twitter"
5 | s.version = "0.6.1"
6 | s.authors = ["Kentaro Yoshida"]
7 | s.email = ["y.ken.studio@gmail.com"]
8 | s.homepage = "https://github.com/y-ken/fluent-plugin-twitter"
9 | s.summary = %q{Fluentd Input/Output plugin to collect/process tweets with Twitter Streaming API.}
10 | s.license = "Apache-2.0"
11 |
12 | s.files = `git ls-files`.split("\n")
13 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15 | s.require_paths = ["lib"]
16 |
17 | s.required_ruby_version = "> 2.1"
18 |
19 | s.add_development_dependency "rake"
20 | s.add_development_dependency "test-unit", ">= 3.1.0"
21 | s.add_development_dependency "appraisal"
22 |
23 | s.add_runtime_dependency "fluentd", [">= 0.14.0", "< 2"]
24 | s.add_runtime_dependency "twitter", "~> 6.0"
25 | end
26 |
--------------------------------------------------------------------------------
/lib/fluent/plugin/out_twitter.rb:
--------------------------------------------------------------------------------
1 | require "twitter"
2 | require "fluent/plugin/output"
3 |
4 | class Fluent::Plugin::TwitterOutput < Fluent::Plugin::Output
5 | Fluent::Plugin.register_output('twitter', self)
6 |
7 | config_param :consumer_key, :string, secret: true
8 | config_param :consumer_secret, :string, secret: true
9 | config_param :access_token, :string, secret: true
10 | config_param :access_token_secret, :string, secret: true
11 |
12 | config_section :proxy, multi: false do
13 | config_param :host, :string
14 | config_param :port, :string
15 | config_param :username, :string, default: nil
16 | config_param :password, :string, default: nil, secret: true
17 | end
18 |
19 | def initialize
20 | super
21 | end
22 |
23 | def configure(conf)
24 | super
25 |
26 | @twitter = Twitter::REST::Client.new do |config|
27 | config.consumer_key = @consumer_key
28 | config.consumer_secret = @consumer_secret
29 | config.access_token = @access_token
30 | config.access_token_secret = @access_token_secret
31 | config.proxy = @proxy.to_h if @proxy
32 | end
33 | end
34 |
35 | def process(tag, es)
36 | es.each do |_time, record|
37 | tweet(record['message'])
38 | end
39 | end
40 |
41 | def tweet(message)
42 | begin
43 | @twitter.update(message)
44 | rescue Twitter::Error => e
45 | log.error("Twitter Error: #{e.message}")
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/test/plugin/test_out_twitter.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'fluent/test/driver/output'
3 | require 'fluent/plugin/out_twitter'
4 |
5 | class TwitterOutputTest < Test::Unit::TestCase
6 | def setup
7 | Fluent::Test.setup
8 | end
9 |
10 | CONFIG = %[
11 | consumer_key CONSUMER_KEY
12 | consumer_secret CONSUMER_SECRET
13 | access_token ACCESS_TOKEN
14 | access_token_secret ACCESS_TOKEN_SECRET
15 | ]
16 |
17 | def create_driver(conf = CONFIG)
18 | Fluent::Test::Driver::Output.new(Fluent::Plugin::TwitterOutput).configure(conf)
19 | end
20 |
21 | sub_test_case "configure" do
22 | def test_empty
23 | assert_raise(Fluent::ConfigError) {
24 | create_driver('')
25 | }
26 | end
27 |
28 | def test_configure
29 | d = create_driver %[
30 | consumer_key CONSUMER_KEY
31 | consumer_secret CONSUMER_SECRET
32 | access_token ACCESS_TOKEN
33 | access_token_secret ACCESS_TOKEN_SECRET
34 | ]
35 | assert_equal 'CONSUMER_KEY', d.instance.consumer_key
36 | assert_equal 'CONSUMER_SECRET', d.instance.consumer_secret
37 | assert_equal 'ACCESS_TOKEN', d.instance.access_token
38 | assert_equal 'ACCESS_TOKEN_SECRET', d.instance.access_token_secret
39 | end
40 |
41 | def test_proxy
42 | conf = %[
43 | consumer_key CONSUMER_KEY
44 | consumer_secret CONSUMER_SECRET
45 | access_token ACCESS_TOKEN
46 | access_token_secret ACCESS_TOKEN_SECRET
47 |
48 | host proxy.example.com
49 | port 8080
50 | username proxyuser
51 | password proxypass
52 |
53 | ]
54 | d = create_driver(conf)
55 | expected = {
56 | host: "proxy.example.com",
57 | port: "8080",
58 | username: "proxyuser",
59 | password: "proxypass"
60 | }
61 | assert_equal(expected, d.instance.proxy.to_h)
62 | end
63 |
64 | def test_multi_proxy
65 | conf = %[
66 | consumer_key CONSUMER_KEY
67 | consumer_secret CONSUMER_SECRET
68 | access_token ACCESS_TOKEN
69 | access_token_secret ACCESS_TOKEN_SECRET
70 |
71 | host proxy.example.com
72 | port 8080
73 | username proxyuser
74 | password proxypass
75 |
76 |
77 | host proxy.example.com
78 | port 8081
79 | username proxyuser
80 | password proxypass
81 |
82 | ]
83 | assert_raise(Fluent::ConfigError) do
84 | create_driver(conf)
85 | end
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/test/plugin/test_in_twitter.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'fluent/plugin/in_twitter'
3 | require 'fluent/test/driver/input'
4 |
5 | class TwitterInputTest < Test::Unit::TestCase
6 | def setup
7 | Fluent::Test.setup
8 | end
9 |
10 | CONFIG = %[
11 | consumer_key CONSUMER_KEY
12 | consumer_secret CONSUMER_SECRET
13 | access_token ACCESS_TOKEN
14 | access_token_secret ACCESS_TOKEN_SECRET
15 | tag input.twitter
16 | timeline sampling
17 | ]
18 |
19 | def create_driver(conf = CONFIG, syntax: :v1)
20 | Fluent::Test::Driver::Input.new(Fluent::Plugin::TwitterInput).configure(conf, syntax: syntax)
21 | end
22 |
23 | sub_test_case "v0 syntax" do
24 | def test_empty
25 | assert_raise(Fluent::ConfigError) {
26 | create_driver('', syntax: :v0)
27 | }
28 | end
29 |
30 | def test_configure
31 | d = create_driver %[
32 | consumer_key CONSUMER_KEY
33 | consumer_secret CONSUMER_SECRET
34 | access_token ACCESS_TOKEN
35 | access_token_secret ACCESS_TOKEN_SECRET
36 | tag input.twitter
37 | timeline tracking
38 | keyword ${hashtag}fluentd,fluentd lang:ja
39 | ]
40 | assert_equal 'CONSUMER_KEY', d.instance.consumer_key
41 | assert_equal 'CONSUMER_SECRET', d.instance.consumer_secret
42 | assert_equal 'ACCESS_TOKEN', d.instance.access_token
43 | assert_equal 'ACCESS_TOKEN_SECRET', d.instance.access_token_secret
44 | assert_equal '#fluentd,fluentd lang:ja', d.instance.keyword
45 | end
46 | end
47 |
48 | sub_test_case "v1 syntax" do
49 | def test_empty
50 | assert_raise(Fluent::ConfigError) {
51 | create_driver('')
52 | }
53 | end
54 |
55 | def test_multi_keyword
56 | d = create_driver(%[
57 | consumer_key CONSUMER_KEY
58 | consumer_secret CONSUMER_SECRET
59 | access_token ACCESS_TOKEN
60 | access_token_secret ACCESS_TOKEN_SECRET
61 | tag input.twitter
62 | timeline tracking
63 | keyword 'treasuredata,treasure data,#treasuredata,fluentd,#fluentd'
64 | ])
65 | assert_equal 'CONSUMER_KEY', d.instance.consumer_key
66 | assert_equal 'CONSUMER_SECRET', d.instance.consumer_secret
67 | assert_equal 'ACCESS_TOKEN', d.instance.access_token
68 | assert_equal 'ACCESS_TOKEN_SECRET', d.instance.access_token_secret
69 | assert_equal 'treasuredata,treasure data,#treasuredata,fluentd,#fluentd', d.instance.keyword
70 | end
71 | end
72 |
73 | sub_test_case "proxy" do
74 | test "simple" do
75 | conf = %[
76 | consumer_key CONSUMER_KEY
77 | consumer_secret CONSUMER_SECRET
78 | access_token ACCESS_TOKEN
79 | access_token_secret ACCESS_TOKEN_SECRET
80 | tag input.twitter
81 | timeline tracking
82 | keyword 'treasuredata,treasure data,#treasuredata,fluentd,#fluentd'
83 |
84 | host proxy.example.com
85 | port 8080
86 | username proxyuser
87 | password proxypass
88 |
89 | ]
90 | d = create_driver(conf)
91 | expected = {
92 | host: "proxy.example.com",
93 | port: "8080",
94 | username: "proxyuser",
95 | password: "proxypass"
96 | }
97 | assert_equal(expected, d.instance.proxy.to_h)
98 | end
99 |
100 | test "multi proxy is not supported" do
101 | conf = %[
102 | consumer_key CONSUMER_KEY
103 | consumer_secret CONSUMER_SECRET
104 | access_token ACCESS_TOKEN
105 | access_token_secret ACCESS_TOKEN_SECRET
106 | tag input.twitter
107 | timeline tracking
108 | keyword 'treasuredata,treasure data,#treasuredata,fluentd,#fluentd'
109 |
110 | host proxy.example.com
111 | port 8080
112 | username proxyuser
113 | password proxypass
114 |
115 |
116 | host proxy.example.com
117 | port 8081
118 | username proxyuser
119 | password proxypass
120 |
121 | ]
122 | assert_raise(Fluent::ConfigError) do
123 | create_driver(conf)
124 | end
125 | end
126 | end
127 | end
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | fluent-plugin-twitter [](https://travis-ci.org/y-ken/fluent-plugin-twitter)
2 | =====================
3 |
4 | ## Component
5 | Fluentd Input/Output plugin to process tweets with Twitter Streaming API.
6 |
7 | ## Dependency
8 |
9 | before use, install dependent library as:
10 |
11 | ```
12 | # for RHEL/CentOS (eventmachine requires build dependency)
13 | $ sudo yum -y install gcc gcc-c++ openssl-devel libcurl libcurl-devel
14 |
15 | # for Ubuntu/Debian (eventmachine requires build dependency)
16 | $ sudo apt-get install build-essential libssl-dev
17 | ```
18 |
19 | ## Requirements
20 |
21 | | fluent-plugin-twitter | fluentd | ruby |
22 | |--------------------|------------|--------|
23 | | 0.6.1 | v0.14.x | >= 2.1 |
24 | | 0.5.4 | v0.12.x | >= 1.9 |
25 |
26 | ## Installation
27 |
28 | install with `gem` or `td-agent-gem` command as:
29 |
30 | ```
31 | # for fluentd
32 | $ gem install eventmachine
33 | $ gem install fluent-plugin-twitter
34 |
35 | # for td-agent2
36 | $ sudo td-agent-gem install eventmachine
37 | $ sudo td-agent-gem install fluent-plugin-twitter -v 0.5.4
38 | ```
39 |
40 | ## Input Configuration
41 |
42 | ### Input Sample
43 |
44 | It require td-agent2 (fluentd v0.12) to use keyword with hashtag.
45 |
46 | `````
47 |
48 | @type twitter
49 | consumer_key YOUR_CONSUMER_KEY # Required
50 | consumer_secret YOUR_CONSUMER_SECRET # Required
51 | access_token YOUR_ACCESS_TOKEN # Required
52 | access_token_secret YOUR_ACCESS_TOKEN_SECRET # Required
53 | tag input.twitter.sampling # Required
54 | timeline tracking # Required (tracking or sampling or location or userstream)
55 | keyword 'Ruby,Python,#fleuntd' # Optional (keyword has priority than follow_ids)
56 | follow_ids 14252,53235 # Optional (integers, not screen names)
57 | locations 31.110283, 129.431631, 45.619283, 145.510175 # Optional (bounding boxes; first pair specifies longitude/latitude of southwest corner)
58 | lang ja,en # Optional
59 | output_format nest # Optional (nest or flat or simple[default])
60 | flatten_separator _ # Optional
61 |
62 |
63 |
64 | @type stdout
65 |
66 | `````
67 |
68 | ### Proxy support
69 |
70 | ```
71 |
72 | @type twitter
73 | consumer_key YOUR_CONSUMER_KEY # Required
74 | consumer_secret YOUR_CONSUMER_SECRET # Required
75 | access_token YOUR_ACCESS_TOKEN # Required
76 | access_token_secret YOUR_ACCESS_TOKEN_SECRET # Required
77 | tag input.twitter.sampling # Required
78 | timeline tracking # Required (tracking or sampling or location or userstream)
79 |
80 | host proxy.example.com # Required
81 | port 8080 # Required
82 | username proxyuser # Optional
83 | password proxypass # Optional
84 |
85 |
86 | ```
87 |
88 | ### Testing
89 |
90 | `````
91 | $ tail -f /var/log/td-agent/td-agent.log
92 | `````
93 |
94 | ## Output Configuration
95 |
96 | ### Output Sample
97 | `````
98 |
99 | @type http
100 | port 8888
101 |
102 |
103 |
104 | @type twitter
105 | consumer_key YOUR_CONSUMER_KEY
106 | consumer_secret YOUR_CONSUMER_SECRET
107 | access_token YOUR_ACCESS_TOKEN
108 | access_token_secret YOUR_ACCESS_TOKEN_SECRET
109 |
110 | `````
111 |
112 | ### Proxy support
113 |
114 | ```
115 |
116 | @type twitter
117 | consumer_key YOUR_CONSUMER_KEY
118 | consumer_secret YOUR_CONSUMER_SECRET
119 | access_token YOUR_ACCESS_TOKEN
120 | access_token_secret YOUR_ACCESS_TOKEN_SECRET
121 |
122 | host proxy.example.com
123 | port 8080
124 | username proxyuser
125 | password proxypass
126 |
127 |
128 | ```
129 |
130 | ### Testing
131 |
132 | `````
133 | $ curl http://localhost:8888/notify.twitter -F 'json={"message":"foo"}'
134 | `````
135 |
136 | ## Reference
137 |
138 | ### Twitter OAuth Guide
139 | http://pocketstudio.jp/log3/2012/02/12/how_to_get_twitter_apikey_and_token/
140 |
141 | ### Quick Tour to count a tweet with fluent-plugin-twitter and fluent-plugin-datacounter
142 | http://qiita.com/items/fe4258b394190f23fece
143 |
144 | ## TODO
145 |
146 | patches welcome!
147 |
148 | ## Copyright
149 |
150 | Copyright © 2012- Kentaro Yoshida (@yoshi_ken)
151 |
152 | ## License
153 |
154 | Apache License, Version 2.0
155 |
--------------------------------------------------------------------------------
/lib/fluent/plugin/in_twitter.rb:
--------------------------------------------------------------------------------
1 | require "fluent/plugin/input"
2 |
3 | require 'twitter'
4 | require 'nkf'
5 |
6 | module Fluent::Plugin
7 | class TwitterInput < Fluent::Plugin::Input
8 | Fluent::Plugin.register_input('twitter', self)
9 |
10 | helpers :thread
11 |
12 | TIMELINE_TYPE = %i(userstream sampling location tracking)
13 | OUTPUT_FORMAT_TYPE = %i(nest flat simple)
14 |
15 | config_param :consumer_key, :string, secret: true
16 | config_param :consumer_secret, :string, secret: true
17 | config_param :access_token, :string, secret: true
18 | config_param :access_token_secret, :string, secret: true
19 | config_param :tag, :string
20 | config_param :timeline, :enum, list: TIMELINE_TYPE
21 | config_param :keyword, :string, default: nil
22 | config_param :follow_ids, :string, default: nil
23 | config_param :locations, :string, default: nil
24 | config_param :lang, :string, default: nil
25 | config_param :output_format, :enum, list: OUTPUT_FORMAT_TYPE, default: :simple
26 | config_param :flatten_separator, :string, default: '_'
27 |
28 | config_section :proxy, multi: false do
29 | config_param :host, :string
30 | config_param :port, :string
31 | config_param :username, :string, default: nil
32 | config_param :password, :string, default: nil, secret: true
33 | end
34 |
35 | def initialize
36 | super
37 | @running = false
38 | end
39 |
40 | def configure(conf)
41 | super
42 |
43 | @keyword = @keyword.gsub('${hashtag}', '#') unless @keyword.nil?
44 |
45 | @client = Twitter::Streaming::Client.new do |config|
46 | config.consumer_key = @consumer_key
47 | config.consumer_secret = @consumer_secret
48 | config.access_token = @access_token
49 | config.access_token_secret = @access_token_secret
50 | config.proxy = @proxy.to_h if @proxy
51 | end
52 | end
53 |
54 | def start
55 | @running = true
56 | thread_create(:in_twitter) do
57 | run
58 | end
59 | end
60 |
61 | def shutdown
62 | @running = false
63 | @client.close if @client.respond_to?(:close)
64 | super
65 | end
66 |
67 | def run
68 | notice = "twitter: starting Twitter Streaming API for #{@timeline}."
69 | notice << " tag:#{@tag}"
70 | notice << " lang:#{@lang}" unless @lang.nil?
71 | notice << " keyword:#{@keyword}" unless @keyword.nil?
72 | notice << " follow:#{@follow_ids}" unless @follow_ids.nil? && !@keyword.nil?
73 | log.info notice
74 |
75 | if [:sampling, :tracking].include?(@timeline) && @keyword
76 | @client.filter(track: @keyword, &method(:handle_object))
77 | elsif @timeline == :tracking && @follow_ids
78 | @client.filter(follow: @follow_ids, &method(:handle_object))
79 | elsif @timeline == :sampling && @keyword.nil? && @follow_ids.nil?
80 | @client.sample(&method(:handle_object))
81 | elsif @timeline == :userstream
82 | @client.user(&method(:handle_object))
83 | end
84 | end
85 |
86 | def handle_object(object)
87 | return unless @running
88 | if is_message?(object)
89 | get_message(object)
90 | end
91 | end
92 |
93 | def is_message?(tweet)
94 | return false if !tweet.is_a?(Twitter::Tweet)
95 | return false if (!@lang.nil? && @lang != '') && !@lang.include?(tweet.user.lang)
96 | if @timeline == :userstream && (!@keyword.nil? && @keyword != '')
97 | pattern = NKF::nkf('-WwZ1', @keyword).gsub(/,\s?/, '|')
98 | tweet = NKF::nkf('-WwZ1', tweet.text)
99 | return false if !Regexp.new(pattern, Regexp::IGNORECASE).match(tweet)
100 | end
101 | return true
102 | end
103 |
104 | def get_message(tweet)
105 | case @output_format
106 | when :nest
107 | record = hash_key_to_s(tweet.to_h)
108 | when :flat
109 | record = hash_flatten(tweet.to_h)
110 | when :simple
111 | record = {}
112 | record['message'] = tweet.text.scrub('')
113 | record['geo'] = tweet.geo
114 | record['place'] = tweet.place
115 | record['created_at'] = tweet.created_at
116 | record['user_name'] = tweet.user.name
117 | record['user_screen_name'] = tweet.user.screen_name
118 | record['user_profile_image_url'] = tweet.user.profile_image_url
119 | record['user_time_zone'] = tweet.user.time_zone
120 | record['user_lang'] = tweet.user.lang
121 | end
122 | router.emit(@tag, Fluent::Engine.now, record)
123 | end
124 |
125 | def hash_flatten(record, prefix = nil)
126 | record.inject({}) do |d, (k, v)|
127 | k = prefix.to_s + k.to_s
128 | if v.instance_of?(Hash)
129 | d.merge(hash_flatten(v, k + @flatten_separator))
130 | elsif v.instance_of?(String)
131 | d.merge(k => v.scrub(""))
132 | else
133 | d.merge(k => v)
134 | end
135 | end
136 | end
137 |
138 | def hash_key_to_s(hash)
139 | newhash = {}
140 | hash.each do |k, v|
141 | if v.instance_of?(Hash)
142 | newhash[k.to_s] = hash_key_to_s(v)
143 | elsif v.instance_of?(Array)
144 | newhash[k.to_s] = array_key_to_s(v)
145 | elsif v.instance_of?(String)
146 | newhash[k.to_s] = v.scrub('')
147 | else
148 | newhash[k.to_s] = v
149 | end
150 | end
151 | newhash
152 | end
153 |
154 | def array_key_to_s(array)
155 | array.map do |v|
156 | if v.instance_of?(Hash)
157 | hash_key_to_s(v)
158 | elsif v.instance_of?(Array)
159 | array_key_to_s(v)
160 | elsif v.instance_of?(String)
161 | v.scrub('')
162 | else
163 | v
164 | end
165 | end
166 | end
167 | end
168 | end
169 |
--------------------------------------------------------------------------------