Skip to content
This repository

HTTPClient::IncludeClient #95

Merged
merged 1 commit into from about 2 years ago

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
nahi commented

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 nahi closed this
Hiroshi Nakamura
Owner
nahi commented

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

Showing 1 unique commit by 1 author.

May 17, 2012
Jonathan Rochkind jrochkind HTTPClient::IncludeClient
a mix-in for easily adding a thread-safe lazily initialized class-level
HTTPClient object to your class.
fc1c0ff
This page is out of date. Refresh to see the latest.
83 lib/httpclient/include_client.rb
... ... @@ -0,0 +1,83 @@
  1 +# It is useful to re-use a HTTPClient instance for multiple requests, to
  2 +# re-use HTTP 1.1 persistent connections.
  3 +#
  4 +# To do that, you sometimes want to store an HTTPClient instance in a global/
  5 +# class variable location, so it can be accessed and re-used.
  6 +#
  7 +# This mix-in makes it easy to create class-level access to one or more
  8 +# HTTPClient instances. The HTTPClient instances are lazily initialized
  9 +# on first use (to, for instance, avoid interfering with WebMock/VCR),
  10 +# and are initialized in a thread-safe manner. Note that a
  11 +# HTTPClient, once initialized, is safe for use in multiple threads.
  12 +#
  13 +# Note that you `extend` HTTPClient::IncludeClient, not `include.
  14 +#
  15 +# require 'httpclient/include_client'
  16 +# class Widget
  17 +# extend HTTPClient::IncludeClient
  18 +#
  19 +# include_http_client
  20 +# # and/or, specify more stuff
  21 +# include_http_client('http://myproxy:8080', :method_name => :my_client) do |client|
  22 +# # any init you want
  23 +# client.set_cookie_store nil
  24 +# client.
  25 +# end
  26 +# end
  27 +#
  28 +# That creates two HTTPClient instances available at the class level.
  29 +# The first will be available from Widget.http_client (default method
  30 +# name for `include_http_client`), with default initialization.
  31 +#
  32 +# The second will be available at Widget.my_client, with the init arguments
  33 +# provided, further initialized by the block provided.
  34 +#
  35 +# In addition to a class-level method, for convenience instance-level methods
  36 +# are also provided. Widget.http_client is identical to Widget.new.http_client
  37 +#
  38 +#
  39 +class HTTPClient
  40 + module IncludeClient
  41 +
  42 +
  43 + def include_http_client(*args, &block)
  44 + # We're going to dynamically define a class
  45 + # to hold our state, namespaced, as well as possibly dynamic
  46 + # name of cover method.
  47 + method_name = (args.last.delete(:method_name) if args.last.kind_of? Hash) || :http_client
  48 + args.pop if args.last == {} # if last arg was named methods now empty, remove it.
  49 +
  50 + # By the amazingness of closures, we can create these things
  51 + # in local vars here and use em in our method, we don't even
  52 + # need iVars for state.
  53 + client_instance = nil
  54 + client_mutex = Mutex.new
  55 + client_args = args
  56 + client_block = block
  57 +
  58 + # to define a _class method_ on the specific class that's currently
  59 + # `self`, we have to use this bit of metaprogramming, sorry.
  60 + (class << self; self ; end).instance_eval do
  61 + define_method(method_name) do
  62 + # implementation copied from ruby stdlib singleton
  63 + # to create this global obj thread-safely.
  64 + return client_instance if client_instance
  65 + client_mutex.synchronize do
  66 + return client_instance if client_instance
  67 + # init HTTPClient with specified args/block
  68 + client_instance = HTTPClient.new(*client_args)
  69 + client_block.call(client_instance) if client_block
  70 + end
  71 + return client_instance
  72 + end
  73 + end
  74 +
  75 + # And for convenience, an _instance method_ on the class that just
  76 + # delegates to the class method.
  77 + define_method(method_name) do
  78 + self.class.send(method_name)
  79 + end
  80 +
  81 + end
  82 + end
  83 +end
52 test/test_include_client.rb
... ... @@ -0,0 +1,52 @@
  1 +# -*- encoding: utf-8 -*-
  2 +require File.expand_path('helper', File.dirname(__FILE__))
  3 +
  4 +require 'httpclient/include_client'
  5 +class TestIncludeClient < Test::Unit::TestCase
  6 + class Widget
  7 + extend HTTPClient::IncludeClient
  8 +
  9 + include_http_client("http://example.com") do |client|
  10 + client.cookie_manager = nil
  11 + client.agent_name = "iMonkey 4k"
  12 + end
  13 + end
  14 +
  15 + class OtherWidget
  16 + extend HTTPClient::IncludeClient
  17 +
  18 + include_http_client
  19 + include_http_client(:method_name => :other_http_client)
  20 + end
  21 +
  22 + class UnrelatedBlankClass ; end
  23 +
  24 + def test_client_class_level_singleton
  25 + assert_equal Widget.http_client.object_id, Widget.http_client.object_id
  26 +
  27 + assert_equal Widget.http_client.object_id, Widget.new.http_client.object_id
  28 +
  29 + assert_not_equal Widget.http_client.object_id, OtherWidget.http_client.object_id
  30 + end
  31 +
  32 + def test_configured
  33 + assert_equal Widget.http_client.agent_name, "iMonkey 4k"
  34 + assert_nil Widget.http_client.cookie_manager
  35 + assert_equal Widget.http_client.proxy.to_s, "http://example.com"
  36 + end
  37 +
  38 + def test_two_includes
  39 + assert_not_equal OtherWidget.http_client.object_id, OtherWidget.other_http_client.object_id
  40 +
  41 + assert_equal OtherWidget.other_http_client.object_id, OtherWidget.new.other_http_client.object_id
  42 + end
  43 +
  44 + # meta-programming gone wrong sometimes accidentally
  45 + # adds the class method to _everyone_, a mistake we've made before.
  46 + def test_not_infected_class_hieararchy
  47 + assert ! Class.respond_to?(:http_client)
  48 + assert ! UnrelatedBlankClass.respond_to?(:http_client)
  49 + end
  50 +
  51 +
  52 +end

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.