her 0.6.8 → 0.7

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.
@@ -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))