her 0.6.8 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4f5ffa339d778a9e0c1bdf8284bb6df2452584b9
4
+ data.tar.gz: f37e2a93474b2290a0b676ec70c793232ad49eb7
5
+ SHA512:
6
+ metadata.gz: 9d6670525bc4efb9c658cf9692ffda8fdb2c9ae24267f0b44c48980b39330b9b3fd51f47b73ad024f8cbb53f45ebff43d8adafe5adf6cae91f739a17aa5715bd
7
+ data.tar.gz: 45e82cff66d8f55e5887769a103a8015deff104c27e0d9e1fc0daff79dd53bc9e67681437ff3b692017d7c74596a9a2b3d5cd133906db0cbec9a01a3874057bb
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  /Gemfile.lock
2
2
  /pkg
3
3
  /tmp
4
+ /coverage
@@ -4,17 +4,17 @@ rvm:
4
4
  - 2.0.0
5
5
  - 1.9.3
6
6
  - 1.9.2
7
- - 1.8.7
8
7
 
9
8
  gemfile:
9
+ - gemfiles/Gemfile.activemodel-4.1
10
10
  - gemfiles/Gemfile.activemodel-4.0
11
11
  - gemfiles/Gemfile.activemodel-3.2.x
12
12
 
13
13
  matrix:
14
14
  exclude:
15
- - rvm: 1.8.7
16
- gemfile: gemfiles/Gemfile.activemodel-4.0
17
15
  - rvm: 1.9.2
18
16
  gemfile: gemfiles/Gemfile.activemodel-4.0
17
+ - rvm: 1.9.2
18
+ gemfile: gemfiles/Gemfile.activemodel-4.1
19
19
 
20
20
  script: "echo 'COME ON!' && bundle exec rake spec"
data/Gemfile CHANGED
@@ -1,2 +1,10 @@
1
1
  source "https://rubygems.org"
2
2
  gemspec
3
+
4
+ if RbConfig::CONFIG['RUBY_PROGRAM_VERSION'] && RbConfig::CONFIG['RUBY_PROGRAM_VERSION'] >= '1.9.3'
5
+ gem 'activemodel', '>= 3.2.0'
6
+ gem 'activesupport', '>= 3.2.0'
7
+ else
8
+ gem 'activemodel', '~> 3.2.0'
9
+ gem 'activesupport', '~> 3.2.0'
10
+ end
data/README.md CHANGED
@@ -1,11 +1,17 @@
1
- # Her
2
-
3
- [![Gem Version](https://badge.fury.io/rb/her.png)](https://rubygems.org/gems/her)
4
- [![Build Status](https://secure.travis-ci.org/remiprev/her.png?branch=master)](http://travis-ci.org/remiprev/her)
5
- [![Dependency Status](https://gemnasium.com/remiprev/her.png?travis)](https://gemnasium.com/remiprev/her)
6
- [![Code Climate](https://codeclimate.com/github/remiprev/her.png)](https://codeclimate.com/github/remiprev/her)
7
-
8
- Her is an ORM (Object Relational Mapper) that maps REST resources to Ruby objects. It is designed to build applications that are powered by a RESTful API instead of a database.
1
+ <p align="center">
2
+ <a href="https://github.com/remiprev/her">
3
+ <img src="http://i.imgur.com/43KEchq.png" alt="Her" />
4
+ </a>
5
+ <br />
6
+ Her is an ORM (Object Relational Mapper) that maps REST resources to Ruby objects.<br /> It is designed to build applications that are powered by a RESTful API instead of a database.
7
+ <br /><br />
8
+ <a href="https://rubygems.org/gems/her"><img src="http://img.shields.io/gem/v/her.svg" /></a>
9
+ <a href="https://codeclimate.com/github/remiprev/her"><img src="http://img.shields.io/codeclimate/github/remiprev/her.svg" /></a>
10
+ <a href='https://gemnasium.com/remiprev/her'><img src="http://img.shields.io/gemnasium/remiprev/her.svg" /></a>
11
+ <a href="https://travis-ci.org/remiprev/her"><img src="http://img.shields.io/travis/remiprev/her.svg" /></a>
12
+ </p>
13
+
14
+ ---
9
15
 
10
16
  ## Installation
11
17
 
@@ -26,8 +32,13 @@ First, you have to define which API your models will be bound to. For example, w
26
32
  ```ruby
27
33
  # config/initializers/her.rb
28
34
  Her::API.setup url: "https://api.example.com" do |c|
35
+ # Request
29
36
  c.use Faraday::Request::UrlEncoded
37
+
38
+ # Response
30
39
  c.use Her::Middleware::DefaultParseJSON
40
+
41
+ # Adapter
31
42
  c.use Faraday::Adapter::NetHttp
32
43
  end
33
44
  ```
@@ -75,7 +86,7 @@ end
75
86
  # Update a fetched resource
76
87
  user = User.find(1)
77
88
  user.fullname = "Lindsay Fünke" # OR user.assign_attributes(fullname: "Lindsay Fünke")
78
- user.save
89
+ user.save # returns false if it fails, errors in user.response_errors array
79
90
  # PUT "/users/1" with `fullname=Lindsay+Fünke`
80
91
 
81
92
  # Update a resource without fetching it
@@ -103,7 +114,7 @@ User.create(fullname: "Maeby Fünke")
103
114
 
104
115
  # Save a new resource
105
116
  user = User.new(fullname: "Maeby Fünke")
106
- user.save
117
+ user.save! # raises Her::Errors::ResourceInvalid if it fails
107
118
  # POST "/users" with `fullname=Maeby+Fünke`
108
119
  ```
109
120
 
@@ -142,9 +153,14 @@ end
142
153
  require "lib/my_token_authentication"
143
154
 
144
155
  Her::API.setup url: "https://api.example.com" do |c|
156
+ # Request
145
157
  c.use MyTokenAuthentication
146
158
  c.use Faraday::Request::UrlEncoded
159
+
160
+ # Response
147
161
  c.use Her::Middleware::DefaultParseJSON
162
+
163
+ # Adapter
148
164
  c.use Faraday::Adapter::NetHttp
149
165
  end
150
166
  ```
@@ -175,8 +191,13 @@ TWITTER_CREDENTIALS = {
175
191
  }
176
192
 
177
193
  Her::API.setup url: "https://api.twitter.com/1/" do |c|
194
+ # Request
178
195
  c.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
196
+
197
+ # Response
179
198
  c.use Her::Middleware::DefaultParseJSON
199
+
200
+ # Adapter
180
201
  c.use Faraday::Adapter::NetHttp
181
202
  end
182
203
 
@@ -225,7 +246,10 @@ class MyCustomParser < Faraday::Response::Middleware
225
246
  end
226
247
 
227
248
  Her::API.setup url: "https://api.example.com" do |c|
249
+ # Response
228
250
  c.use MyCustomParser
251
+
252
+ # Adapter
229
253
  c.use Faraday::Adapter::NetHttp
230
254
  end
231
255
  ```
@@ -246,8 +270,13 @@ In your Ruby code:
246
270
 
247
271
  ```ruby
248
272
  Her::API.setup url: "https://api.example.com" do |c|
273
+ # Request
249
274
  c.use FaradayMiddleware::Caching, Memcached::Rails.new('127.0.0.1:11211')
275
+
276
+ # Response
250
277
  c.use Her::Middleware::DefaultParseJSON
278
+
279
+ # Adapter
251
280
  c.use Faraday::Adapter::NetHttp
252
281
  end
253
282
 
@@ -668,7 +697,7 @@ class User
668
697
  end
669
698
 
670
699
  user = User.find("4fd89a42ff204b03a905c535")
671
- # GET "/users/1", response is { "_id": "4fd89a42ff204b03a905c535", "name": "Tobias" }
700
+ # GET "/users/4fd89a42ff204b03a905c535", response is { "_id": "4fd89a42ff204b03a905c535", "name": "Tobias" }
672
701
 
673
702
  user.destroy
674
703
  # DELETE "/users/4fd89a42ff204b03a905c535"
@@ -748,13 +777,19 @@ It is possible to use different APIs for different models. Instead of calling `H
748
777
  # config/initializers/her.rb
749
778
  MY_API = Her::API.new
750
779
  MY_API.setup url: "https://my-api.example.com" do |c|
780
+ # Response
751
781
  c.use Her::Middleware::DefaultParseJSON
782
+
783
+ # Adapter
752
784
  c.use Faraday::Adapter::NetHttp
753
785
  end
754
786
 
755
787
  OTHER_API = Her::API.new
756
788
  OTHER_API.setup url: "https://other-api.example.com" do |c|
789
+ # Response
757
790
  c.use Her::Middleware::DefaultParseJSON
791
+
792
+ # Adapter
758
793
  c.use Faraday::Adapter::NetHttp
759
794
  end
760
795
  ```
@@ -786,7 +821,10 @@ When initializing `Her::API`, you can pass any parameter supported by `Faraday.n
786
821
  ```ruby
787
822
  ssl_options = { ca_path: "/usr/lib/ssl/certs" }
788
823
  Her::API.setup url: "https://api.example.com", ssl: ssl_options do |c|
824
+ # Response
789
825
  c.use Her::Middleware::DefaultParseJSON
826
+
827
+ # Adapter
790
828
  c.use Faraday::Adapter::NetHttp
791
829
  end
792
830
  ```
@@ -3,3 +3,5 @@ source "https://rubygems.org"
3
3
  gemspec :path => "../"
4
4
 
5
5
  gem 'activemodel', '~> 3.2.0'
6
+ gem 'activesupport', '~> 3.2.0'
7
+ gem 'faraday', '~> 0.8.9'
@@ -3,3 +3,5 @@ source "https://rubygems.org"
3
3
  gemspec :path => "../"
4
4
 
5
5
  gem 'activemodel', '~> 4.0.0'
6
+ gem 'activesupport', '~> 4.0.0'
7
+ gem 'faraday', '~> 0.8.9'
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 4.1.0'
6
+ gem 'activesupport', '~> 4.1.0'
7
+ gem 'faraday', '~> 0.8.9'
@@ -19,11 +19,12 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.add_development_dependency "rake", "~> 10.0"
21
21
  s.add_development_dependency "rspec", "~> 2.13"
22
+ s.add_development_dependency "rspec-its", "~> 1.0"
22
23
  s.add_development_dependency "fivemat", "~> 1.2"
23
- s.add_development_dependency "json", "~> 1.7.7"
24
+ s.add_development_dependency "json", "~> 1.8"
24
25
 
25
- s.add_runtime_dependency "activemodel", ">= 3.0.0"
26
- s.add_runtime_dependency "activesupport", ">= 3.0.0"
27
- s.add_runtime_dependency "faraday", "~> 0.8"
26
+ s.add_runtime_dependency "activemodel", ">= 3.0.0", "< 4.2"
27
+ s.add_runtime_dependency "activesupport", ">= 3.0.0", "< 4.2"
28
+ s.add_runtime_dependency "faraday", ">= 0.8", "< 1.0"
28
29
  s.add_runtime_dependency "multi_json", "~> 1.7"
29
30
  end
@@ -5,6 +5,9 @@ module Her
5
5
  # @private
6
6
  attr_reader :base_uri, :connection, :options
7
7
 
8
+ # Constants
9
+ FARADAY_OPTIONS = [:request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class].freeze
10
+
8
11
  # Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
9
12
  def self.setup(opts={}, &block)
10
13
  @default_api = new
@@ -42,12 +45,13 @@ module Her
42
45
  # class MyAuthentication < Faraday::Middleware
43
46
  # def call(env)
44
47
  # env[:request_headers]["X-API-Token"] = "bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"
45
- # @all.call(env)
48
+ # @app.call(env)
46
49
  # end
47
50
  # end
48
51
  # Her::API.setup :url => "https://api.example.com" do |connection|
49
52
  # connection.use Faraday::Request::UrlEncoded
50
53
  # connection.use Her::Middleware::DefaultParseJSON
54
+ # connection.use MyAuthentication
51
55
  # connection.use Faraday::Adapter::NetHttp
52
56
  # end
53
57
  #
@@ -69,7 +73,9 @@ module Her
69
73
  opts[:url] = opts.delete(:base_uri) if opts.include?(:base_uri) # Support legacy :base_uri option
70
74
  @base_uri = opts[:url]
71
75
  @options = opts
72
- @connection = Faraday.new(@options) do |connection|
76
+
77
+ faraday_options = @options.reject { |key, value| !FARADAY_OPTIONS.include?(key.to_sym) }
78
+ @connection = Faraday.new(faraday_options) do |connection|
73
79
  yield connection if block_given?
74
80
  end
75
81
  self
@@ -14,5 +14,14 @@ module Her
14
14
 
15
15
  class ParseError < StandardError
16
16
  end
17
+
18
+ class ResourceInvalid < StandardError
19
+ attr_reader :resource
20
+ def initialize(resource)
21
+ @resource = resource
22
+ errors = @resource.response_errors.join(", ")
23
+ super("Remote validation failed: #{errors}")
24
+ end
25
+ end
17
26
  end
18
27
  end
@@ -3,7 +3,7 @@ module Her
3
3
  class ParseJSON < Faraday::Response::Middleware
4
4
  # @private
5
5
  def parse_json(body = nil)
6
- body ||= '{}'
6
+ body = '{}' if body.blank?
7
7
  message = "Response from the API must behave like a Hash or an Array (last JSON response was #{body.inspect})"
8
8
 
9
9
  json = begin
@@ -1,4 +1,5 @@
1
1
  require "her/model/associations/association"
2
+ require "her/model/associations/association_proxy"
2
3
  require "her/model/associations/belongs_to_association"
3
4
  require "her/model/associations/has_many_association"
4
5
  require "her/model/associations/has_one_association"
@@ -15,13 +15,22 @@ module Her
15
15
  @name = @opts[:name]
16
16
  end
17
17
 
18
+ # @private
19
+ def self.proxy(parent, opts = {})
20
+ AssociationProxy.new new(parent, opts)
21
+ end
22
+
18
23
  # @private
19
24
  def self.parse_single(association, klass, data)
20
25
  data_key = association[:data_key]
21
26
  return {} unless data[data_key]
22
27
 
23
28
  klass = klass.her_nearby_class(association[:class_name])
24
- { association[:name] => klass.new(data[data_key]) }
29
+ if data[data_key].kind_of?(klass)
30
+ { association[:name] => data[data_key] }
31
+ else
32
+ { association[:name] => klass.new(data[data_key]) }
33
+ end
25
34
  end
26
35
 
27
36
  # @private
@@ -35,7 +44,8 @@ module Her
35
44
 
36
45
  # @private
37
46
  def fetch(opts = {})
38
- return @opts[:default].try(:dup) if @parent.attributes.include?(@name) && @parent.attributes[@name].empty? && @params.empty?
47
+ attribute_value = @parent.attributes[@name]
48
+ return @opts[:default].try(:dup) if @parent.attributes.include?(@name) && (attribute_value.nil? || !attribute_value.nil? && attribute_value.empty?) && @params.empty?
39
49
 
40
50
  if @parent.attributes[@name].blank? || @params.any?
41
51
  path = build_association_path lambda { "#{@parent.request_path(@params)}#{@opts[:path]}" }
@@ -66,7 +76,7 @@ module Her
66
76
  # user.comments.where(:approved => 1) # Fetched via GET "/users/1/comments?approved=1
67
77
  def where(params = {})
68
78
  return self if params.blank? && @parent.attributes[@name].blank?
69
- self.clone.tap { |a| a.params = a.params.merge(params) }
79
+ AssociationProxy.new self.clone.tap { |a| a.params = a.params.merge(params) }
70
80
  end
71
81
  alias all where
72
82
 
@@ -86,32 +96,6 @@ module Her
86
96
  @klass.get(path, @params)
87
97
  end
88
98
 
89
- # @private
90
- def nil?
91
- fetch.nil?
92
- end
93
-
94
- # @private
95
- def kind_of?(thing)
96
- fetch.kind_of?(thing)
97
- end
98
-
99
- # @private
100
- def ==(other)
101
- fetch.eql?(other)
102
- end
103
- alias eql? ==
104
-
105
- # ruby 1.8.7 compatibility
106
- # @private
107
- def id
108
- fetch.id
109
- end
110
-
111
- # @private
112
- def method_missing(method, *args, &blk)
113
- fetch.send(method, *args, &blk)
114
- end
115
99
  end
116
100
  end
117
101
  end
@@ -0,0 +1,46 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class AssociationProxy < (ActiveSupport.const_defined?('ProxyObject') ? ActiveSupport::ProxyObject : ActiveSupport::BasicObject)
5
+
6
+ # @private
7
+ def self.install_proxy_methods(target_name, *names)
8
+ names.each do |name|
9
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
10
+ def #{name}(*args, &block)
11
+ #{target_name}.#{name}(*args, &block)
12
+ end
13
+ RUBY
14
+ end
15
+ end
16
+
17
+ install_proxy_methods :association,
18
+ :build, :create, :where, :find, :all, :assign_nested_attributes
19
+
20
+ # @private
21
+ def initialize(association)
22
+ @_her_association = association
23
+ end
24
+
25
+ def association
26
+ @_her_association
27
+ end
28
+
29
+ # @private
30
+ def method_missing(name, *args, &block)
31
+ if :object_id == name # avoid redefining object_id
32
+ return association.fetch.object_id
33
+ end
34
+
35
+ # create a proxy to the fetched object's method
36
+ metaclass = (class << self; self; end)
37
+ metaclass.install_proxy_methods 'association.fetch', name
38
+
39
+ # resend message to fetched object
40
+ __send__(name, *args, &block)
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ end
@@ -2,6 +2,7 @@ module Her
2
2
  module Model
3
3
  module Associations
4
4
  class BelongsToAssociation < Association
5
+
5
6
  # @private
6
7
  def self.attach(klass, name, opts)
7
8
  opts = {
@@ -19,7 +20,7 @@ module Her
19
20
  cached_name = :"@_her_association_#{name}"
20
21
 
21
22
  cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
22
- cached_data || instance_variable_set(cached_name, Her::Model::Associations::BelongsToAssociation.new(self, #{opts.inspect}))
23
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::BelongsToAssociation.proxy(self, #{opts.inspect}))
23
24
  end
24
25
  RUBY
25
26
  end
@@ -72,7 +73,8 @@ module Her
72
73
  # @private
73
74
  def fetch
74
75
  foreign_key_value = @parent.attributes[@opts[:foreign_key].to_sym]
75
- return @opts[:default].try(:dup) if (@parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @params.empty?) || (@parent.persisted? && foreign_key_value.blank?)
76
+ data_key_value = @parent.attributes[@opts[:data_key].to_sym]
77
+ return @opts[:default].try(:dup) if (@parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @params.empty?) || (@parent.persisted? && foreign_key_value.blank? && data_key_value.blank?)
76
78
 
77
79
  if @parent.attributes[@name].blank? || @params.any?
78
80
  path_params = @parent.attributes.merge(@params.merge(@klass.primary_key => foreign_key_value))