diff --git a/README.md b/README.md index da693b6..f72a44f 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,49 @@ RSpec.describe MyResourceClass, resource_kit: true do end ``` +## Default Connection Factory + +ResourceKit also allows you to specify a default connection factory. This is useful for when you want to just initialize resources, or enable class methods. + +```ruby +class BaseResource < ResourceKit::Resource + resources do + default_handler { |resp| raise "Unexpected status code: #{resp.code}" } + connection { Faraday.new(url: 'http://example.com') } + end +end + +class UsersResource < BaseResource + resources do + get "/users/info" => :info + end +end + +resource = UsersResource.new +user_info = resource.info +``` + +## Singleton Methods + +In most cases, you don't care about instantiating a class to use it. You just want the class to respond to a singleton method. + +ResourceKit allows you to do this by mixing in `ResourceKit::SingletonRequests` into your resource classes. This makes calling methods a little simpler, but requires you setup a default connection in order to work. + +```ruby +class UsersResource < ResourceKit::Resource + include ResourceKit::SingletonRequests + + resources do + default_handler { |resp| raise "Unexpected status code: #{resp.code}" } + connection { Faraday.new(url: 'http://example.com') } + + get '/api/users/:id' => :find + end +end + +users_info = UsersResource.find(id: 123) +``` + ### Nice to have's Things we've thought about but just haven't implemented are: diff --git a/lib/resource_kit.rb b/lib/resource_kit.rb index b0e1fc7..432f7f2 100644 --- a/lib/resource_kit.rb +++ b/lib/resource_kit.rb @@ -15,4 +15,5 @@ module ResourceKit autoload :StatusCodeMapper, 'resource_kit/status_code_mapper' autoload :EndpointResolver, 'resource_kit/endpoint_resolver' + autoload :SingletonRequests, 'resource_kit/singleton_requests' end diff --git a/lib/resource_kit/method_factory.rb b/lib/resource_kit/method_factory.rb index 79bd275..43bddae 100644 --- a/lib/resource_kit/method_factory.rb +++ b/lib/resource_kit/method_factory.rb @@ -2,11 +2,7 @@ module ResourceKit class MethodFactory def self.construct(object, resource_collection, invoker = ActionInvoker) resource_collection.each do |action| - if object.method_defined?(action.name) - raise ArgumentError, "Action '#{action.name}' is already defined on `#{object}`" - end method_block = method_for_action(action, invoker) - object.send(:define_method, action.name, &method_block) end end diff --git a/lib/resource_kit/resource.rb b/lib/resource_kit/resource.rb index d532e8e..b7dcd53 100644 --- a/lib/resource_kit/resource.rb +++ b/lib/resource_kit/resource.rb @@ -8,7 +8,7 @@ class Resource attr_reader :connection, :scope def initialize(connection: nil, scope: nil) - @connection = connection + @connection = connection || _resources.default_connection.call @scope = scope end @@ -23,6 +23,10 @@ def self.resources(&block) self._resources end + def self.inherited(base) + base._resources = resources.dup + end + def action(name) _resources.find_action(name) end diff --git a/lib/resource_kit/resource_collection.rb b/lib/resource_kit/resource_collection.rb index e66f40c..4fc0701 100644 --- a/lib/resource_kit/resource_collection.rb +++ b/lib/resource_kit/resource_collection.rb @@ -42,6 +42,25 @@ def find_action(name) end end + def default_connection(&connection_block) + @default_connection = connection_block if block_given? + @default_connection || Proc.new { } + end + + def dup + collection = ResourceCollection.new + + # Copy all actions into the collection + each do |action| + collection << action + end + + # Copy all of the default handlers in + collection.default_handlers.merge!(default_handlers) + + collection + end + private def parse_verb_and_path(verb_and_path) diff --git a/lib/resource_kit/singleton_requests.rb b/lib/resource_kit/singleton_requests.rb new file mode 100644 index 0000000..4f885f4 --- /dev/null +++ b/lib/resource_kit/singleton_requests.rb @@ -0,0 +1,29 @@ +module ResourceKit + module SingletonRequests + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def method_missing(action_name, *args, &block) + if action = resources.find_action(action_name) + define_method_for_action(action_name) + send(action_name, *args, &block) + else + super + end + end + + private + + def define_method_for_action(action_name) + class_eval <<-RUBY + def self.#{action_name}(*args, &block) + resource_instance = new + resource_instance.send(:#{action_name}, *args, &block) + end + RUBY + end + end + end +end \ No newline at end of file diff --git a/spec/integration/inheritence_spec.rb b/spec/integration/inheritence_spec.rb new file mode 100644 index 0000000..cb8eda0 --- /dev/null +++ b/spec/integration/inheritence_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +class DummyInheritenceResource < ResourceKit::Resource + resources do + default_handler(:ok) { |resp| "Hello" } + get "/dummy" => :dummy + end +end + +class DummyChildResource < DummyInheritenceResource + resources do + get "/inherited" => :inherited + end +end + +RSpec.describe 'Resource Inheritence' do + it 'inherits the superclasses actions' do + action = DummyChildResource.resources.find_action(:dummy) + expect(action).to_not be_nil + expect(action.name).to eq(:dummy) + end + + it 'inherits default handlers' do + expect(DummyChildResource.resources.default_handlers[200]).to_not be_nil + end + + it 'does not modify the parent resource class' do + expect(DummyInheritenceResource.resources.find_action(:inherited)).to be_nil + end +end \ No newline at end of file diff --git a/spec/lib/resource_kit/method_factory_spec.rb b/spec/lib/resource_kit/method_factory_spec.rb index 3778139..d729fe5 100644 --- a/spec/lib/resource_kit/method_factory_spec.rb +++ b/spec/lib/resource_kit/method_factory_spec.rb @@ -16,14 +16,6 @@ expect(instance).to respond_to(:find, :all) end - it 'bails when the method is already defined' do - collection.action :all - - expect { - ResourceKit::MethodFactory.construct(klass, collection) - }.to raise_exception(ArgumentError).with_message("Action 'all' is already defined on `#{klass}`") - end - it 'adds the correct interface for the action' do ResourceKit::MethodFactory.construct(klass, collection) method_sig = klass.instance_method(:all).parameters diff --git a/spec/lib/resource_kit/resource_collection_spec.rb b/spec/lib/resource_kit/resource_collection_spec.rb index 72b003a..1397d92 100644 --- a/spec/lib/resource_kit/resource_collection_spec.rb +++ b/spec/lib/resource_kit/resource_collection_spec.rb @@ -64,4 +64,17 @@ expect(retrieved_action.name).to eq(:all) end end + + describe '#default_connection' do + it 'sets the default connection when passed a block' do + proc = Proc.new { } + collection.default_connection(&proc) + + expect(collection.default_connection).to eq(proc) + end + + it 'returns a proc that returns nil when nothing has been set' do + expect(collection.default_connection.call).to be_nil + end + end end diff --git a/spec/lib/resource_kit/resource_spec.rb b/spec/lib/resource_kit/resource_spec.rb index 02671b3..63cef30 100644 --- a/spec/lib/resource_kit/resource_spec.rb +++ b/spec/lib/resource_kit/resource_spec.rb @@ -46,6 +46,19 @@ class DropletResource < described_class expect(instance.connection).to be(connection) expect(instance.scope).to be(scope) end + + context 'with a defaulted connection' do + it 'uses the default connection' do + klass = Class.new(ResourceKit::Resource) do + resources do + default_connection { :default_connection } + end + end + + instance = klass.new + expect(instance.connection).to be(:default_connection) + end + end end describe '#action' do diff --git a/spec/lib/resource_kit/singleton_requests_spec.rb b/spec/lib/resource_kit/singleton_requests_spec.rb new file mode 100644 index 0000000..231c751 --- /dev/null +++ b/spec/lib/resource_kit/singleton_requests_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +RSpec.describe ResourceKit::SingletonRequests do + let(:klass) do + Class.new(ResourceKit::Resource) do + include ResourceKit::SingletonRequests + + resources do + get '/hello' => :hello + end + end + end + + describe 'Inclusions' do + let(:connection) { Faraday.new { |b| b.adapter :test, stubs } } + let(:stubs) do + Faraday::Adapter::Test::Stubs.new do |stub| + stub.get('/hello') { |env| [200, {}, 'world'] } + end + end + + before do + # Set the default connection for the resource to our stubbed one + klass.resources.default_connection { connection } + end + + it 'allows calling actions as class methods' do + value = klass.hello + expect(value).to eq('world') + end + + it 'defines the method on the class when called' do + expect { klass.hello }.to change { klass.methods.include?(:hello) }.to(true).from(false) + end + end +end \ No newline at end of file