party_resource 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+