httparty 0.13.0 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +18 -0
  3. data/.github/workflows/ci.yml +26 -0
  4. data/.gitignore +4 -0
  5. data/.rubocop.yml +92 -0
  6. data/.rubocop_todo.yml +124 -0
  7. data/CONTRIBUTING.md +23 -0
  8. data/Changelog.md +557 -0
  9. data/Gemfile +15 -3
  10. data/Guardfile +4 -3
  11. data/README.md +24 -25
  12. data/Rakefile +5 -7
  13. data/bin/httparty +20 -14
  14. data/docs/README.md +191 -0
  15. data/examples/README.md +89 -0
  16. data/examples/aaws.rb +10 -6
  17. data/examples/basic.rb +6 -10
  18. data/examples/body_stream.rb +14 -0
  19. data/examples/crack.rb +2 -2
  20. data/examples/custom_parsers.rb +6 -5
  21. data/examples/delicious.rb +8 -8
  22. data/examples/google.rb +2 -2
  23. data/examples/headers_and_user_agents.rb +7 -3
  24. data/examples/idn.rb +10 -0
  25. data/examples/logging.rb +36 -0
  26. data/examples/microsoft_graph.rb +52 -0
  27. data/examples/multipart.rb +22 -0
  28. data/examples/nokogiri_html_parser.rb +0 -3
  29. data/examples/peer_cert.rb +9 -0
  30. data/examples/rescue_json.rb +17 -0
  31. data/examples/rubyurl.rb +3 -3
  32. data/examples/stackexchange.rb +24 -0
  33. data/examples/stream_download.rb +26 -0
  34. data/examples/tripit_sign_in.rb +20 -9
  35. data/examples/twitter.rb +7 -7
  36. data/examples/whoismyrep.rb +1 -1
  37. data/httparty.gemspec +13 -9
  38. data/lib/httparty/connection_adapter.rb +105 -25
  39. data/lib/httparty/cookie_hash.rb +10 -9
  40. data/lib/httparty/decompressor.rb +102 -0
  41. data/lib/httparty/exceptions.rb +8 -2
  42. data/lib/httparty/hash_conversions.rb +39 -19
  43. data/lib/httparty/headers_processor.rb +32 -0
  44. data/lib/httparty/logger/apache_formatter.rb +47 -0
  45. data/lib/httparty/logger/curl_formatter.rb +93 -0
  46. data/lib/httparty/logger/logger.rb +22 -10
  47. data/lib/httparty/logger/logstash_formatter.rb +61 -0
  48. data/lib/httparty/module_inheritable_attributes.rb +6 -4
  49. data/lib/httparty/net_digest_auth.rb +76 -25
  50. data/lib/httparty/parser.rb +28 -15
  51. data/lib/httparty/request/body.rb +105 -0
  52. data/lib/httparty/request/multipart_boundary.rb +13 -0
  53. data/lib/httparty/request.rb +218 -130
  54. data/lib/httparty/response/headers.rb +23 -19
  55. data/lib/httparty/response.rb +99 -15
  56. data/lib/httparty/response_fragment.rb +21 -0
  57. data/lib/httparty/text_encoder.rb +72 -0
  58. data/lib/httparty/utils.rb +13 -0
  59. data/lib/httparty/version.rb +3 -1
  60. data/lib/httparty.rb +191 -83
  61. data/website/css/common.css +1 -1
  62. data/website/index.html +3 -3
  63. metadata +50 -120
  64. data/.travis.yml +0 -7
  65. data/History +0 -303
  66. data/features/basic_authentication.feature +0 -20
  67. data/features/command_line.feature +0 -7
  68. data/features/deals_with_http_error_codes.feature +0 -26
  69. data/features/digest_authentication.feature +0 -20
  70. data/features/handles_compressed_responses.feature +0 -27
  71. data/features/handles_multiple_formats.feature +0 -57
  72. data/features/steps/env.rb +0 -22
  73. data/features/steps/httparty_response_steps.rb +0 -52
  74. data/features/steps/httparty_steps.rb +0 -35
  75. data/features/steps/mongrel_helper.rb +0 -94
  76. data/features/steps/remote_service_steps.rb +0 -74
  77. data/features/supports_redirection.feature +0 -22
  78. data/features/supports_timeout_option.feature +0 -13
  79. data/lib/httparty/core_extensions.rb +0 -32
  80. data/lib/httparty/logger/apache_logger.rb +0 -22
  81. data/lib/httparty/logger/curl_logger.rb +0 -48
  82. data/spec/fixtures/delicious.xml +0 -23
  83. data/spec/fixtures/empty.xml +0 -0
  84. data/spec/fixtures/google.html +0 -3
  85. data/spec/fixtures/ssl/generate.sh +0 -29
  86. data/spec/fixtures/ssl/generated/1fe462c2.0 +0 -16
  87. data/spec/fixtures/ssl/generated/bogushost.crt +0 -13
  88. data/spec/fixtures/ssl/generated/ca.crt +0 -16
  89. data/spec/fixtures/ssl/generated/ca.key +0 -15
  90. data/spec/fixtures/ssl/generated/selfsigned.crt +0 -14
  91. data/spec/fixtures/ssl/generated/server.crt +0 -13
  92. data/spec/fixtures/ssl/generated/server.key +0 -15
  93. data/spec/fixtures/ssl/openssl-exts.cnf +0 -9
  94. data/spec/fixtures/twitter.csv +0 -2
  95. data/spec/fixtures/twitter.json +0 -1
  96. data/spec/fixtures/twitter.xml +0 -403
  97. data/spec/fixtures/undefined_method_add_node_for_nil.xml +0 -2
  98. data/spec/httparty/connection_adapter_spec.rb +0 -298
  99. data/spec/httparty/cookie_hash_spec.rb +0 -83
  100. data/spec/httparty/exception_spec.rb +0 -23
  101. data/spec/httparty/logger/apache_logger_spec.rb +0 -26
  102. data/spec/httparty/logger/curl_logger_spec.rb +0 -18
  103. data/spec/httparty/logger/logger_spec.rb +0 -22
  104. data/spec/httparty/net_digest_auth_spec.rb +0 -152
  105. data/spec/httparty/parser_spec.rb +0 -165
  106. data/spec/httparty/request_spec.rb +0 -631
  107. data/spec/httparty/response_spec.rb +0 -221
  108. data/spec/httparty/ssl_spec.rb +0 -74
  109. data/spec/httparty_spec.rb +0 -764
  110. data/spec/spec.opts +0 -2
  111. data/spec/spec_helper.rb +0 -37
  112. data/spec/support/ssl_test_helper.rb +0 -47
  113. data/spec/support/ssl_test_server.rb +0 -80
  114. data/spec/support/stub_response.rb +0 -43
@@ -1,37 +1,37 @@
1
1
  dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  require File.join(dir, 'httparty')
3
3
  require 'pp'
4
- config = YAML::load(File.read(File.join(ENV['HOME'], '.delicious')))
4
+ config = YAML.load(File.read(File.join(ENV['HOME'], '.delicious')))
5
5
 
6
6
  class Delicious
7
7
  include HTTParty
8
8
  base_uri 'https://api.del.icio.us/v1'
9
9
 
10
10
  def initialize(u, p)
11
- @auth = {:username => u, :password => p}
11
+ @auth = { username: u, password: p }
12
12
  end
13
13
 
14
14
  # query params that filter the posts are:
15
15
  # tag (optional). Filter by this tag.
16
16
  # dt (optional). Filter by this date (CCYY-MM-DDThh:mm:ssZ).
17
17
  # url (optional). Filter by this url.
18
- # ie: posts(:query => {:tag => 'ruby'})
19
- def posts(options={})
20
- options.merge!({:basic_auth => @auth})
18
+ # ie: posts(query: {tag: 'ruby'})
19
+ def posts(options = {})
20
+ options.merge!({ basic_auth: @auth })
21
21
  self.class.get('/posts/get', options)
22
22
  end
23
23
 
24
24
  # query params that filter the posts are:
25
25
  # tag (optional). Filter by this tag.
26
26
  # count (optional). Number of items to retrieve (Default:15, Maximum:100).
27
- def recent(options={})
28
- options.merge!({:basic_auth => @auth})
27
+ def recent(options = {})
28
+ options.merge!({ basic_auth: @auth })
29
29
  self.class.get('/posts/recent', options)
30
30
  end
31
31
  end
32
32
 
33
33
  delicious = Delicious.new(config['username'], config['password'])
34
- pp delicious.posts(:query => {:tag => 'ruby'})
34
+ pp delicious.posts(query: { tag: 'ruby' })
35
35
  pp delicious.recent
36
36
 
37
37
  delicious.recent['posts']['post'].each { |post| puts post['href'] }
data/examples/google.rb CHANGED
@@ -10,7 +10,7 @@ end
10
10
  # google.com redirects to www.google.com so this is live test for redirection
11
11
  pp Google.get('http://google.com')
12
12
 
13
- puts '', '*'*70, ''
13
+ puts '', '*' * 70, ''
14
14
 
15
15
  # check that ssl is requesting right
16
- pp Google.get('https://www.google.com')
16
+ pp Google.get('https://www.google.com')
@@ -1,6 +1,10 @@
1
1
  # To send custom user agents to identify your application to a web service (or mask as a specific browser for testing), send "User-Agent" as a hash to headers as shown below.
2
2
 
3
- require 'httparty'
3
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require File.join(dir, 'httparty')
5
+ require 'pp'
4
6
 
5
- APPLICATION_NAME = "Httparty"
6
- response = HTTParty.get('http://example.com', :headers => {"User-Agent" => APPLICATION_NAME})
7
+ response = HTTParty.get('http://example.com', {
8
+ headers: {"User-Agent" => "Httparty"},
9
+ debug_output: STDOUT, # To show that User-Agent is Httparty
10
+ })
data/examples/idn.rb ADDED
@@ -0,0 +1,10 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'httparty')
3
+ require 'pp'
4
+
5
+ class Idn
6
+ include HTTParty
7
+ uri_adapter Addressable::URI
8
+ end
9
+
10
+ pp Idn.get("https://i❤️.ws/emojidomain/💎?format=json")
@@ -0,0 +1,36 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'httparty')
3
+ require 'logger'
4
+ require 'pp'
5
+
6
+ my_logger = Logger.new STDOUT
7
+
8
+ my_logger.info "Logging can be used on the main HTTParty class. It logs redirects too."
9
+ HTTParty.get "http://google.com", logger: my_logger
10
+
11
+ my_logger.info '*' * 70
12
+
13
+ my_logger.info "It can be used also on a custom class."
14
+
15
+ class Google
16
+ include HTTParty
17
+ logger ::Logger.new STDOUT
18
+ end
19
+
20
+ Google.get "http://google.com"
21
+
22
+ my_logger.info '*' * 70
23
+
24
+ my_logger.info "The default formatter is :apache. The :curl formatter can also be used."
25
+ my_logger.info "You can tell which method to call on the logger too. It is info by default."
26
+ HTTParty.get "http://google.com", logger: my_logger, log_level: :debug, log_format: :curl
27
+
28
+ my_logger.info '*' * 70
29
+
30
+ my_logger.info "These configs are also available on custom classes."
31
+ class Google
32
+ include HTTParty
33
+ logger ::Logger.new(STDOUT), :debug, :curl
34
+ end
35
+
36
+ Google.get "http://google.com"
@@ -0,0 +1,52 @@
1
+ require 'httparty'
2
+
3
+ class MicrosoftGraph
4
+ MS_BASE_URL = "https://login.microsoftonline.com".freeze
5
+ TOKEN_REQUEST_PATH = "oauth2/v2.0/token".freeze
6
+
7
+ def initialize(tenant_id)
8
+ @tenant_id = tenant_id
9
+ end
10
+
11
+ # Make a request to the Microsoft Graph API, for instance https://graph.microsoft.com/v1.0/users
12
+ def request(url)
13
+ return false unless (token = bearer_token)
14
+
15
+ response = HTTParty.get(
16
+ url,
17
+ headers: {
18
+ Authorization: "Bearer #{token}"
19
+ }
20
+ )
21
+
22
+ return false unless response.code == 200
23
+
24
+ return JSON.parse(response.body)
25
+ end
26
+
27
+ private
28
+
29
+ # A post to the Microsoft Graph to get a bearer token for the specified tenant. In this example
30
+ # our Rails application has already been given permission to request these tokens by the admin of
31
+ # the specified tenant_id.
32
+ #
33
+ # See here for more information https://developer.microsoft.com/en-us/graph/docs/concepts/auth_v2_service
34
+ #
35
+ # This request also makes use of the multipart/form-data post body.
36
+ def bearer_token
37
+ response = HTTParty.post(
38
+ "#{MS_BASE_URL}/#{@tenant_id}/#{TOKEN_REQUEST_PATH}",
39
+ multipart: true,
40
+ body: {
41
+ client_id: Rails.application.credentials[Rails.env.to_sym][:microsoft_client_id],
42
+ client_secret: Rails.application.credentials[Rails.env.to_sym][:microsoft_client_secret],
43
+ scope: 'https://graph.microsoft.com/.default',
44
+ grant_type: 'client_credentials'
45
+ }
46
+ )
47
+
48
+ return false unless response.code == 200
49
+
50
+ JSON.parse(response.body)['access_token']
51
+ end
52
+ end
@@ -0,0 +1,22 @@
1
+ # If you are uploading file in params, multipart will used as content-type automatically
2
+
3
+ HTTParty.post(
4
+ 'http://localhost:3000/user',
5
+ body: {
6
+ name: 'Foo Bar',
7
+ email: 'example@email.com',
8
+ avatar: File.open('/full/path/to/avatar.jpg')
9
+ }
10
+ )
11
+
12
+
13
+ # However, you can force it yourself
14
+
15
+ HTTParty.post(
16
+ 'http://localhost:3000/user',
17
+ multipart: true,
18
+ body: {
19
+ name: 'Foo Bar',
20
+ email: 'example@email.com'
21
+ }
22
+ )
@@ -1,14 +1,11 @@
1
1
  require 'rubygems'
2
2
  require 'nokogiri'
3
3
 
4
-
5
4
  dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
6
5
  require File.join(dir, 'httparty')
7
6
  require 'pp'
8
7
 
9
8
  class HtmlParserIncluded < HTTParty::Parser
10
- SupportedFormats.merge!('text/html' => :html)
11
-
12
9
  def html
13
10
  Nokogiri::HTML(body)
14
11
  end
@@ -0,0 +1,9 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'httparty')
3
+
4
+ peer_cert = nil
5
+ HTTParty.get("https://www.example.com") do |fragment|
6
+ peer_cert ||= fragment.connection.peer_cert
7
+ end
8
+
9
+ puts "The server's certificate expires #{peer_cert.not_after}"
@@ -0,0 +1,17 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'httparty')
3
+
4
+ # Take note of the "; 1" at the end of the following line. It's required only if
5
+ # running this in IRB, because IRB will try to inspect the variable named
6
+ # "request", triggering the exception.
7
+ request = HTTParty.get 'https://rubygems.org/api/v1/versions/doesnotexist.json' ; 1
8
+
9
+ # Check an exception due to parsing the response
10
+ # because HTTParty evaluate the response lazily
11
+ begin
12
+ request.inspect
13
+ # This would also suffice by forcing the request to be parsed:
14
+ # request.parsed_response
15
+ rescue => e
16
+ puts "Rescued #{e.inspect}"
17
+ end
data/examples/rubyurl.rb CHANGED
@@ -6,9 +6,9 @@ class Rubyurl
6
6
  include HTTParty
7
7
  base_uri 'rubyurl.com'
8
8
 
9
- def self.shorten( website_url )
10
- post( '/api/links.json', :query => { :link => { :website_url => website_url } } )
9
+ def self.shorten(website_url)
10
+ post('/api/links.json', query: { link: { website_url: website_url } })
11
11
  end
12
12
  end
13
13
 
14
- pp Rubyurl.shorten( 'http://istwitterdown.com/')
14
+ pp Rubyurl.shorten('http://istwitterdown.com/')
@@ -0,0 +1,24 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'httparty')
3
+ require 'pp'
4
+
5
+ class StackExchange
6
+ include HTTParty
7
+ base_uri 'api.stackexchange.com'
8
+
9
+ def initialize(service, page)
10
+ @options = { query: { site: service, page: page } }
11
+ end
12
+
13
+ def questions
14
+ self.class.get("/2.2/questions", @options)
15
+ end
16
+
17
+ def users
18
+ self.class.get("/2.2/users", @options)
19
+ end
20
+ end
21
+
22
+ stack_exchange = StackExchange.new("stackoverflow", 1)
23
+ pp stack_exchange.questions
24
+ pp stack_exchange.users
@@ -0,0 +1,26 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'httparty')
3
+ require 'pp'
4
+
5
+ # download file linux-4.6.4.tar.xz without using the memory
6
+ response = nil
7
+ filename = "linux-4.6.4.tar.xz"
8
+ url = "https://cdn.kernel.org/pub/linux/kernel/v4.x/#{filename}"
9
+
10
+ File.open(filename, "w") do |file|
11
+ response = HTTParty.get(url, stream_body: true) do |fragment|
12
+ if [301, 302].include?(fragment.code)
13
+ print "skip writing for redirect"
14
+ elsif fragment.code == 200
15
+ print "."
16
+ file.write(fragment)
17
+ else
18
+ raise StandardError, "Non-success status code while streaming #{fragment.code}"
19
+ end
20
+ end
21
+ end
22
+ puts
23
+
24
+ pp "Success: #{response.success?}"
25
+ pp File.stat(filename).inspect
26
+ File.unlink(filename)
@@ -3,30 +3,41 @@ require File.join(dir, 'httparty')
3
3
 
4
4
  class TripIt
5
5
  include HTTParty
6
- base_uri 'http://www.tripit.com'
6
+ base_uri 'https://www.tripit.com'
7
7
  debug_output
8
8
 
9
9
  def initialize(email, password)
10
10
  @email = email
11
- response = self.class.get('/account/login')
12
- response = self.class.post(
11
+ get_response = self.class.get('/account/login')
12
+ get_response_cookie = parse_cookie(get_response.headers['Set-Cookie'])
13
+
14
+ post_response = self.class.post(
13
15
  '/account/login',
14
- :body => {
15
- :login_email_address => email,
16
- :login_password => password
16
+ body: {
17
+ login_email_address: email,
18
+ login_password: password
17
19
  },
18
- :headers => {'Cookie' => response.headers['Set-Cookie']}
20
+ headers: {'Cookie' => get_response_cookie.to_cookie_string }
19
21
  )
20
- @cookie = response.request.options[:headers]['Cookie']
22
+
23
+ @cookie = parse_cookie(post_response.headers['Set-Cookie'])
21
24
  end
22
25
 
23
26
  def account_settings
24
- self.class.get('/account/edit', :headers => {'Cookie' => @cookie})
27
+ self.class.get('/account/edit', headers: { 'Cookie' => @cookie.to_cookie_string })
25
28
  end
26
29
 
27
30
  def logged_in?
28
31
  account_settings.include? "You're logged in as #{@email}"
29
32
  end
33
+
34
+ private
35
+
36
+ def parse_cookie(resp)
37
+ cookie_hash = CookieHash.new
38
+ resp.get_fields('Set-Cookie').each { |c| cookie_hash.add_cookies(c) }
39
+ cookie_hash
40
+ end
30
41
  end
31
42
 
32
43
  tripit = TripIt.new('email', 'password')
data/examples/twitter.rb CHANGED
@@ -1,31 +1,31 @@
1
1
  dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  require File.join(dir, 'httparty')
3
3
  require 'pp'
4
- config = YAML::load(File.read(File.join(ENV['HOME'], '.twitter')))
4
+ config = YAML.load(File.read(File.join(ENV['HOME'], '.twitter')))
5
5
 
6
6
  class Twitter
7
7
  include HTTParty
8
8
  base_uri 'twitter.com'
9
9
 
10
10
  def initialize(u, p)
11
- @auth = {:username => u, :password => p}
11
+ @auth = {username: u, password: p}
12
12
  end
13
13
 
14
14
  # which can be :friends, :user or :public
15
15
  # options[:query] can be things like since, since_id, count, etc.
16
- def timeline(which=:friends, options={})
17
- options.merge!({:basic_auth => @auth})
16
+ def timeline(which = :friends, options = {})
17
+ options.merge!({ basic_auth: @auth })
18
18
  self.class.get("/statuses/#{which}_timeline.json", options)
19
19
  end
20
20
 
21
21
  def post(text)
22
- options = { :query => {:status => text}, :basic_auth => @auth }
22
+ options = { query: { status: text }, basic_auth: @auth }
23
23
  self.class.post('/statuses/update.json', options)
24
24
  end
25
25
  end
26
26
 
27
27
  twitter = Twitter.new(config['email'], config['password'])
28
28
  pp twitter.timeline
29
- # pp twitter.timeline(:friends, :query => {:since_id => 868482746})
30
- # pp twitter.timeline(:friends, :query => 'since_id=868482746')
29
+ # pp twitter.timeline(:friends, query: {since_id: 868482746})
30
+ # pp twitter.timeline(:friends, query: 'since_id=868482746')
31
31
  # pp twitter.post('this is a test of 0.2.0')
@@ -7,4 +7,4 @@ class Rep
7
7
  end
8
8
 
9
9
  pp Rep.get('http://whoismyrepresentative.com/getall_mems.php?zip=46544')
10
- pp Rep.get('http://whoismyrepresentative.com/getall_mems.php', :query => {:zip => 46544})
10
+ pp Rep.get('http://whoismyrepresentative.com/getall_mems.php', query: { zip: 46544 })
data/httparty.gemspec CHANGED
@@ -1,26 +1,30 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
3
3
  require "httparty/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "httparty"
7
7
  s.version = HTTParty::VERSION
8
8
  s.platform = Gem::Platform::RUBY
9
+ s.licenses = ['MIT']
9
10
  s.authors = ["John Nunemaker", "Sandro Turriate"]
10
11
  s.email = ["nunemaker@gmail.com"]
11
- s.homepage = "http://jnunemaker.github.com/httparty"
12
- s.summary = %q{Makes http fun! Also, makes consuming restful web services dead easy.}
13
- s.description = %q{Makes http fun! Also, makes consuming restful web services dead easy.}
12
+ s.homepage = "https://github.com/jnunemaker/httparty"
13
+ s.summary = 'Makes http fun! Also, makes consuming restful web services dead easy.'
14
+ s.description = 'Makes http fun! Also, makes consuming restful web services dead easy.'
14
15
 
15
- s.required_ruby_version = '>= 1.9.3'
16
+ s.required_ruby_version = '>= 2.3.0'
16
17
 
17
- s.add_dependency 'json', "~> 1.8"
18
18
  s.add_dependency 'multi_xml', ">= 0.5.2"
19
+ s.add_dependency 'mini_mime', ">= 1.0.0"
19
20
 
21
+ # If this line is removed, all hard partying will cease.
20
22
  s.post_install_message = "When you HTTParty, you must party hard!"
21
23
 
22
- s.files = `git ls-files`.split("\n")
23
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ all_files = `git ls-files`.split("\n")
25
+ test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26
+
27
+ s.files = all_files - test_files
28
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
25
29
  s.require_paths = ["lib"]
26
30
  end