Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

HTTPClient::IncludeClient #95

Merged
merged 1 commit into from

3 participants

Jonathan Rochkind BuildHive Hiroshi Nakamura
Jonathan Rochkind

a mix-in for easily adding a thread-safe lazily initialized class-level HTTPClient object to your class.

The rdocs say why and how -- if you have more questions about motivations/use cases/why I think this is generally useful, I can explain more.

Feedback welcome. If you aren't interested in having this in HTTPClient, no problem, I'll make my own (tiny, tiny) gem instead. If you are interested, but have some critique, feedback, suggestions, or demands as to implementation, I'm happy to hear em.

Jonathan Rochkind jrochkind HTTPClient::IncludeClient
a mix-in for easily adding a thread-safe lazily initialized class-level
HTTPClient object to your class.
fc1c0ff
BuildHive

Hiroshi Nakamura » httpclient #2 SUCCESS
This pull request looks good
(what's this?)

Hiroshi Nakamura
Owner

Thanks @jrochkind. It looks fine! Before I merge it, can you show me an example that use it? Just a single example would be enough.

Jonathan Rochkind

There's a 'fake' example in the rdoc. But I am using it in a gem that is under-development, very not mature or not done yet, just beginning, not sure if that's what you're looking for:

https://github.com/jrochkind/bento_search/blob/b72ced67c50c4db6ab7b2d0330270bbbb2a998f0/app/models/bento_search/google_books_engine.rb

Hiroshi Nakamura nahi merged commit fff089c into from
Hiroshi Nakamura
Owner

Sorry for late reply, and thanks for providing usecase. To be honest I'm still not sure it's the best way to 'extend' a class with module to inherit features but I guess it's enough; you would use it, test exists and it doesn't affect other features. Let me know how it goes with your client.

Jonathan Rochkind

Yeah, it's totally up to you. Since this additional code is purely additional and doesn't change any existing code, it would be quite easy for me to distribute it as a seperate gem too, then you wouldn't have any implied maintenance responsibility for it. Your call, if you want it or not.

Do you have any suggestions for other ways to easily provide "global persistent" HTTPClient to re-use a client object (and thus re-use persistent connections it's maintaining)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 17, 2012
  1. Jonathan Rochkind

    HTTPClient::IncludeClient

    jrochkind authored
    a mix-in for easily adding a thread-safe lazily initialized class-level
    HTTPClient object to your class.
This page is out of date. Refresh to see the latest.
Showing with 135 additions and 0 deletions.
  1. +83 −0 lib/httpclient/include_client.rb
  2. +52 −0 test/test_include_client.rb
83 lib/httpclient/include_client.rb
View
@@ -0,0 +1,83 @@
+# It is useful to re-use a HTTPClient instance for multiple requests, to
+# re-use HTTP 1.1 persistent connections.
+#
+# To do that, you sometimes want to store an HTTPClient instance in a global/
+# class variable location, so it can be accessed and re-used.
+#
+# This mix-in makes it easy to create class-level access to one or more
+# HTTPClient instances. The HTTPClient instances are lazily initialized
+# on first use (to, for instance, avoid interfering with WebMock/VCR),
+# and are initialized in a thread-safe manner. Note that a
+# HTTPClient, once initialized, is safe for use in multiple threads.
+#
+# Note that you `extend` HTTPClient::IncludeClient, not `include.
+#
+# require 'httpclient/include_client'
+# class Widget
+# extend HTTPClient::IncludeClient
+#
+# include_http_client
+# # and/or, specify more stuff
+# include_http_client('http://myproxy:8080', :method_name => :my_client) do |client|
+# # any init you want
+# client.set_cookie_store nil
+# client.
+# end
+# end
+#
+# That creates two HTTPClient instances available at the class level.
+# The first will be available from Widget.http_client (default method
+# name for `include_http_client`), with default initialization.
+#
+# The second will be available at Widget.my_client, with the init arguments
+# provided, further initialized by the block provided.
+#
+# In addition to a class-level method, for convenience instance-level methods
+# are also provided. Widget.http_client is identical to Widget.new.http_client
+#
+#
+class HTTPClient
+ module IncludeClient
+
+
+ def include_http_client(*args, &block)
+ # We're going to dynamically define a class
+ # to hold our state, namespaced, as well as possibly dynamic
+ # name of cover method.
+ method_name = (args.last.delete(:method_name) if args.last.kind_of? Hash) || :http_client
+ args.pop if args.last == {} # if last arg was named methods now empty, remove it.
+
+ # By the amazingness of closures, we can create these things
+ # in local vars here and use em in our method, we don't even
+ # need iVars for state.
+ client_instance = nil
+ client_mutex = Mutex.new
+ client_args = args
+ client_block = block
+
+ # to define a _class method_ on the specific class that's currently
+ # `self`, we have to use this bit of metaprogramming, sorry.
+ (class << self; self ; end).instance_eval do
+ define_method(method_name) do
+ # implementation copied from ruby stdlib singleton
+ # to create this global obj thread-safely.
+ return client_instance if client_instance
+ client_mutex.synchronize do
+ return client_instance if client_instance
+ # init HTTPClient with specified args/block
+ client_instance = HTTPClient.new(*client_args)
+ client_block.call(client_instance) if client_block
+ end
+ return client_instance
+ end
+ end
+
+ # And for convenience, an _instance method_ on the class that just
+ # delegates to the class method.
+ define_method(method_name) do
+ self.class.send(method_name)
+ end
+
+ end
+ end
+end
52 test/test_include_client.rb
View
@@ -0,0 +1,52 @@
+# -*- encoding: utf-8 -*-
+require File.expand_path('helper', File.dirname(__FILE__))
+
+require 'httpclient/include_client'
+class TestIncludeClient < Test::Unit::TestCase
+ class Widget
+ extend HTTPClient::IncludeClient
+
+ include_http_client("http://example.com") do |client|
+ client.cookie_manager = nil
+ client.agent_name = "iMonkey 4k"
+ end
+ end
+
+ class OtherWidget
+ extend HTTPClient::IncludeClient
+
+ include_http_client
+ include_http_client(:method_name => :other_http_client)
+ end
+
+ class UnrelatedBlankClass ; end
+
+ def test_client_class_level_singleton
+ assert_equal Widget.http_client.object_id, Widget.http_client.object_id
+
+ assert_equal Widget.http_client.object_id, Widget.new.http_client.object_id
+
+ assert_not_equal Widget.http_client.object_id, OtherWidget.http_client.object_id
+ end
+
+ def test_configured
+ assert_equal Widget.http_client.agent_name, "iMonkey 4k"
+ assert_nil Widget.http_client.cookie_manager
+ assert_equal Widget.http_client.proxy.to_s, "http://example.com"
+ end
+
+ def test_two_includes
+ assert_not_equal OtherWidget.http_client.object_id, OtherWidget.other_http_client.object_id
+
+ assert_equal OtherWidget.other_http_client.object_id, OtherWidget.new.other_http_client.object_id
+ end
+
+ # meta-programming gone wrong sometimes accidentally
+ # adds the class method to _everyone_, a mistake we've made before.
+ def test_not_infected_class_hieararchy
+ assert ! Class.respond_to?(:http_client)
+ assert ! UnrelatedBlankClass.respond_to?(:http_client)
+ end
+
+
+end
Something went wrong with that request. Please try again.