party_resource 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Eden Development (UK) LTD
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = party_resource
2
+
3
+ Simple REST interface for ruby objects.
4
+
5
+ party_resource is a framework for building ruby objects which interact with a REST api. Built on top of HTTParty.
6
+
7
+ == Note on Patches/Pull Requests
8
+
9
+ * Fork the project.
10
+ * Make your feature addition or bug fix.
11
+ * Add tests for it. This is important so I don't break it in a
12
+ future version unintentionally.
13
+ * Commit, do not mess with rakefile, version, or history.
14
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
15
+ * Send me a pull request. Bonus points for topic branches.
16
+
17
+ == Copyright
18
+
19
+ Copyright (c) 2010 Eden Development (UK) LTD. See LICENSE for details.
@@ -0,0 +1,60 @@
1
+ module PartyResource
2
+ module Buildable
3
+
4
+ def builder
5
+ Builder.new(@options[:as])
6
+ end
7
+
8
+ class Builder
9
+ def initialize(build_options)
10
+ @build_options = build_options
11
+ end
12
+
13
+ def call(raw_result, context, included)
14
+ return nil if raw_result.nil?
15
+ return raw_result.map{ |value| build_result(value, context, included) } if map_result?(raw_result)
16
+ build_result(raw_result, context, included)
17
+ end
18
+
19
+ private
20
+ def builder
21
+ return lambda {|raw_result, context| raw_result} if wants_raw_result?
22
+ return lambda {|raw_result, context| return_type(context).send(return_method,raw_result) } if wants_object?
23
+ return lambda {|raw_result, context| @build_options.call(raw_result) }
24
+ end
25
+
26
+ def build_result(value, context, included)
27
+ if value.is_a? Hash
28
+ value.merge!(included)
29
+ end
30
+ builder.call(value, context)
31
+ end
32
+
33
+
34
+ def map_result?(result)
35
+ result.is_a?(Array) && !wants_raw_result?
36
+ end
37
+
38
+ def wants_raw_result?
39
+ return_type == :raw
40
+ end
41
+
42
+ def wants_object?
43
+ @build_options.is_a?(Array) || @build_options.is_a?(Class) || @build_options == :self
44
+ end
45
+
46
+ def return_type(context=nil)
47
+ return_type = @build_options.is_a?(Array) ? @build_options.first : @build_options
48
+ if return_type == :self
49
+ return_type = context.is_a?(Class) ? context : context.class
50
+ end
51
+ return_type
52
+ end
53
+
54
+ def return_method
55
+ @build_options.is_a?(Array) ? @build_options.last : :new
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,35 @@
1
+ require 'httparty'
2
+ module PartyResource
3
+ module Connector
4
+
5
+ class Base
6
+ attr :options
7
+
8
+ def initialize(name, options)
9
+ self.options = options
10
+ @name = name
11
+ end
12
+
13
+ def fetch(request)
14
+ response = HTTParty.send(request.verb, request.path, request.http_data(options))
15
+ unless (200..399).include? response.code
16
+ raise PartyResource::ConnectionError.build(response)
17
+ end
18
+ response
19
+ end
20
+
21
+ private
22
+
23
+ def options=(options)
24
+ @options = {}
25
+ @options[:base_uri] = HTTParty.normalize_base_uri(options[:base_uri]) if options.has_key?(:base_uri)
26
+
27
+ if options.has_key?(:username) || options.has_key?(:password)
28
+ @options[:basic_auth] = {:username => options[:username], :password => options[:password]}
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ require 'party_resource/connector/base'
2
+ module PartyResource
3
+ def self.Connector(name = nil)
4
+ Connector.lookup(name)
5
+ end
6
+
7
+ module Connector
8
+ def self.lookup(name)
9
+ name ||= repository.default
10
+ connector = repository.connectors[name]
11
+ raise NoConnector.new(name) if connector.nil?
12
+ connector
13
+ end
14
+
15
+ def self.default=(name)
16
+ repository.default = name
17
+ end
18
+
19
+ def self.add(name, options)
20
+ repository.new_connector(name, options)
21
+ end
22
+
23
+ def self.repository
24
+ @repository ||= Repository.new
25
+ end
26
+
27
+ class Repository
28
+ attr_accessor :default
29
+
30
+ def connectors
31
+ @connectors ||= {}
32
+ end
33
+
34
+ def new_connector(name, options)
35
+ connectors[name] = Base.new(name, options)
36
+ self.default = name if default.nil? || options[:default]
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,57 @@
1
+ module PartyResource
2
+
3
+ Error = Class.new(StandardError)
4
+
5
+ class MissingParameter < Error
6
+ def initialize(parameter, context)
7
+ @parameter = parameter
8
+ @context = context
9
+ end
10
+
11
+ def to_s
12
+ "No value for #{@parameter} is available in #{@context}"
13
+ end
14
+ end
15
+
16
+ class NoConnector < Error
17
+ def initialize(name)
18
+ @name = name
19
+ end
20
+
21
+ def to_s
22
+ "Connector '#{@name}' has not been defined"
23
+ end
24
+ end
25
+
26
+ class ConnectionError < Error;
27
+ attr_reader :data
28
+
29
+ def self.build(data=nil)
30
+ klass = case data.code
31
+ when 404: ResourceNotFound
32
+ when 422: ResourceInvalid
33
+ when 400..499: ClientError
34
+ when 500..599: ServerError
35
+ else self
36
+ end
37
+ klass.new(data)
38
+ end
39
+
40
+ def initialize(data=nil)
41
+ super()
42
+ @data = data
43
+ end
44
+
45
+ def to_s
46
+ code = ''
47
+ code = "#{data.code} " rescue nil
48
+ "A #{code}connection error occured"
49
+ end
50
+ end
51
+
52
+ ClientError = Class.new(ConnectionError) # 4xx
53
+ ServerError = Class.new(ConnectionError) # 5xx
54
+
55
+ ResourceNotFound = Class.new(ClientError) # 404
56
+ ResourceInvalid = Class.new(ClientError) # 422
57
+ end
@@ -0,0 +1,11 @@
1
+ module MethodDefine
2
+ private
3
+ def define_meta_method(name, &blk)
4
+ (class << self; self; end).instance_eval { define_method name, &blk }
5
+ end
6
+
7
+ def define_method_on(level, name, &block)
8
+ creator = level == :instance ? :define_method : :define_meta_method
9
+ send(creator, name, &block)
10
+ end
11
+ end
@@ -0,0 +1,91 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ Hash.send(:include, ActiveSupport::CoreExtensions::Hash::IndifferentAccess) unless Hash.method_defined?(:with_indifferent_access)
3
+
4
+ module PartyResource
5
+
6
+ module ClassMethods
7
+ include MethodDefine
8
+
9
+ def connect(name, options={})
10
+ level = options.delete(:on)
11
+ options = {:as => :self, :connector => @party_connector}.merge(options)
12
+ route = Route.new(options)
13
+
14
+ define_method_on(level, name) do |*args|
15
+ route.call(self, *args)
16
+ end
17
+ end
18
+
19
+ def property(*names)
20
+ options = names.pop if names.last.is_a?(Hash)
21
+ names.each do |name|
22
+ name = name.to_sym
23
+ define_method name do
24
+ get_property(name)
25
+ end
26
+ @property_list ||= []
27
+ @property_list << Property.new(name, options)
28
+ end
29
+ end
30
+
31
+ def party_connector(name)
32
+ @party_connector = name
33
+ end
34
+
35
+ private
36
+ def property_list
37
+ @property_list ||= []
38
+ if superclass.include?(PartyResource)
39
+ @property_list + superclass.send(:property_list)
40
+ else
41
+ @property_list
42
+ end
43
+ end
44
+ end
45
+
46
+ module ParameterValues
47
+ def parameter_values(list)
48
+ list.inject({}) do |out, var|
49
+ begin
50
+ out[var] = send(var)
51
+ rescue
52
+ raise MissingParameter.new(var, self)
53
+ end
54
+ out
55
+ end
56
+ end
57
+ end
58
+
59
+ def to_properties_hash
60
+ self.class.send(:property_list).inject({}) do |hash, property|
61
+ hash.merge(property.to_hash(self))
62
+ end
63
+ end
64
+
65
+ def properties_equal?(other)
66
+ begin
67
+ self.class.send(:property_list).all? {|property| self.send(property.name) == other.send(property.name) }
68
+ rescue NoMethodError
69
+ false
70
+ end
71
+ end
72
+
73
+
74
+ private
75
+ def populate_properties(hash)
76
+ hash = hash.with_indifferent_access
77
+ self.class.send(:property_list).each do |property|
78
+ instance_variable_set("@#{property.name}", property.value_from(hash, self)) if property.has_value_in?(hash)
79
+ end
80
+ end
81
+
82
+ def self.included(klass)
83
+ klass.extend(ClassMethods)
84
+ klass.extend(ParameterValues)
85
+ klass.send(:include, ParameterValues)
86
+ end
87
+
88
+ def get_property(name)
89
+ instance_variable_get("@#{name}")
90
+ end
91
+ end
@@ -0,0 +1,53 @@
1
+ require 'party_resource/buildable'
2
+ module PartyResource
3
+ class Property
4
+ include Buildable
5
+
6
+ attr :name
7
+
8
+ def initialize(name, options)
9
+ @name = name
10
+ @options = {:as => :raw}.merge(options || {})
11
+ end
12
+
13
+ def value_from(hash, context)
14
+ builder.call retrieve_value(hash), context, {}
15
+ end
16
+
17
+ def has_value_in?(hash)
18
+ input_hash(hash).has_key?(input_key) || hash.has_key?(name)
19
+ end
20
+
21
+ def to_hash(context)
22
+ value = context.send(name)
23
+ return {} if value.nil?
24
+ value = value.to_properties_hash if value.respond_to?(:to_properties_hash)
25
+ output_keys.reverse.inject(value) do |value, key|
26
+ {key => value}
27
+ end
28
+ end
29
+
30
+ private
31
+ def retrieve_value(hash)
32
+ input_hash(hash)[input_key] || hash[name]
33
+ end
34
+
35
+ def input_hash(hash)
36
+ input_keys[0..-2].inject(hash) do |value, name|
37
+ value[name] || {}
38
+ end
39
+ end
40
+
41
+ def input_keys
42
+ [@options[:from] || name].flatten
43
+ end
44
+
45
+ def output_keys
46
+ [@options[:to] || input_keys].flatten
47
+ end
48
+
49
+ def input_key
50
+ input_keys.last
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ module PartyResource
2
+ class Request
3
+ attr_reader :verb
4
+ def initialize(verb, path, context, args, params)
5
+ @verb = verb
6
+ @path = path
7
+ @args = args
8
+ @context = context
9
+ @params = params || {}
10
+ end
11
+
12
+ def path
13
+ args = @context.parameter_values(path_params - @args.keys).merge(@args)
14
+ URI.encode(path_params.inject(@path) do |path, param|
15
+ path.gsub(":#{param}", args[param].to_s)
16
+ end)
17
+ end
18
+
19
+ def data
20
+ @params.merge(@args).reject{|name,value| path_params.include?(name)}
21
+ end
22
+
23
+ def http_data(options={})
24
+ options = options.merge(self.params_key => self.data) unless self.data.empty?
25
+ options
26
+ end
27
+
28
+ def params_key
29
+ verb == :get ? :query : :body
30
+ end
31
+
32
+ def path_params
33
+ @path.scan(/:([\w]+)/).flatten.map{|p| p.to_sym}
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,77 @@
1
+ require 'party_resource/buildable'
2
+ module PartyResource
3
+
4
+ class Route
5
+ include Buildable
6
+
7
+ VERBS = [:get, :post, :put, :delete]
8
+
9
+ def initialize(options = {})
10
+ @options = transform_options(options)
11
+ end
12
+
13
+ def call(context, *args)
14
+ options = args.pop if args.last.is_a?(Hash) && args.size == expected_args.size + 1
15
+ raise ArgumentError, "wrong number of arguments (#{args.size} for #{expected_args.size})" unless expected_args.size == args.size
16
+ begin
17
+ builder.call(connector.fetch(request(context, args, options)), context, included(context))
18
+ rescue Error => e
19
+ name = e.class.name.split(/::/).last
20
+ return @options[:rescue][name] if @options[:rescue].has_key?(name)
21
+ raise
22
+ end
23
+ end
24
+
25
+ def connector
26
+ PartyResource::Connector(@options[:connector])
27
+ end
28
+
29
+ private
30
+ def request(context, args, params)
31
+ Request.new(@options[:verb], @options[:path], context, args_hash(args), params)
32
+ end
33
+
34
+ def args_hash(args)
35
+ Hash[*expected_args.zip(args).flatten]
36
+ end
37
+
38
+ def transform_options(options)
39
+ options = {:with => [], :including => {}, :rescue => {}}.merge(options)
40
+ transform_with_option(options)
41
+ transform_location_options(options)
42
+ options
43
+ end
44
+
45
+ def transform_with_option(options)
46
+ options[:with] = [options[:with]] unless options[:with].is_a?(Array)
47
+ end
48
+
49
+ def transform_location_options(options)
50
+ verbs = options.keys & VERBS
51
+ raise ArgumentError, "Must define only one verb (#{verbs.inspect} defined)" unless verbs.size == 1
52
+ options[:verb] = verbs.first
53
+ options[:path] = options.delete(options[:verb])
54
+ end
55
+
56
+ def expected_args
57
+ @options[:with]
58
+ end
59
+
60
+ def including
61
+ @options[:including]
62
+ end
63
+
64
+ def included(context)
65
+ return {} if including.empty?
66
+
67
+ context_hash = context.parameter_values(including.values)
68
+
69
+ including.inject({}) do |hash, pair|
70
+ hash[pair.first] = context_hash[pair.last]
71
+ hash
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+
@@ -0,0 +1,8 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/party_resource/method_define')
2
+ require File.expand_path(File.dirname(__FILE__) + '/party_resource/exceptions')
3
+ require File.expand_path(File.dirname(__FILE__) + '/party_resource/property')
4
+ require File.expand_path(File.dirname(__FILE__) + '/party_resource/party_resource')
5
+ require File.expand_path(File.dirname(__FILE__) + '/party_resource/route')
6
+ require File.expand_path(File.dirname(__FILE__) + '/party_resource/request')
7
+ require File.expand_path(File.dirname(__FILE__) + '/party_resource/connector')
8
+
@@ -0,0 +1,19 @@
1
+ class OtherClass < TestBaseClass
2
+ def self.make_boolean(data)
3
+ data =~ /OK/
4
+ end
5
+ end
6
+
7
+
8
+ class OtherPartyClass
9
+ include PartyResource
10
+ party_connector :other_connector
11
+
12
+ connect :test, :get => '/url', :as => :raw
13
+
14
+ property :thing
15
+
16
+ def initialize(args)
17
+ populate_properties(args)
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ class TestBaseClass
2
+ attr :args
3
+
4
+ def initialize(args={})
5
+ @args = args
6
+ end
7
+
8
+ def method_missing(name, *params)
9
+ return args[name] if params.empty? && args.has_key?(name)
10
+ super
11
+ end
12
+
13
+ def ==(other)
14
+ self.class == other.class && args == other.args
15
+ end
16
+ end
17
+
@@ -0,0 +1,44 @@
1
+ PartyResource::Connector.add(:other_connector, {:base_uri => 'http://otherserver/'})
2
+ PartyResource::Connector.add(:my_connector, {:base_uri => 'http://myserver/path', :username => 'fred', :password => 'pass', :default => true})
3
+
4
+ class TestClass < TestBaseClass
5
+ include PartyResource
6
+
7
+ connect :find, :get => '/find/:id.ext', :with => :id, :on => :class
8
+
9
+ connect :update, :put => '/update/:var.ext', :on => :instance, :as => OtherClass
10
+
11
+ connect :save, :post => '/save/file', :with => :data, :as => :raw
12
+
13
+ connect :destroy, :delete => '/delete', :as => [OtherClass, :make_boolean]
14
+
15
+ connect :foo, :get => '/foo', :with => :value, :as => lambda {|data| "New #{data} Improved" }
16
+
17
+ connect :fetch_json, :get => '/big_data', :as => [:self, :from_json], :rescue => {'ResourceNotFound' => nil}
18
+
19
+ connect :include, :get => '/include', :on => :instance, :as => OtherClass, :including => {:thing => :value2}
20
+
21
+ property :value, :from => :input_name
22
+
23
+ property :value2, :value3
24
+
25
+ property :nested_value, :from => [:block, :var]
26
+
27
+ property :other, :as => OtherClass
28
+
29
+ property :processed, :as => lambda { |data| "Processed: #{data}" }, :to => :output_name
30
+
31
+ property :child, :as => OtherPartyClass
32
+
33
+ def self.from_json(args)
34
+ obj = self.new
35
+ obj.send(:populate_properties, args)
36
+ obj
37
+ end
38
+
39
+ end
40
+
41
+ class InheritedTestClass < TestClass
42
+ property :child_property
43
+ end
44
+