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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.travis.yml +3 -3
- data/Gemfile +8 -0
- data/README.md +49 -11
- data/gemfiles/Gemfile.activemodel-3.2.x +2 -0
- data/gemfiles/Gemfile.activemodel-4.0 +2 -0
- data/gemfiles/Gemfile.activemodel-4.1 +7 -0
- data/her.gemspec +5 -4
- data/lib/her/api.rb +8 -2
- data/lib/her/errors.rb +9 -0
- data/lib/her/middleware/parse_json.rb +1 -1
- data/lib/her/model/associations.rb +1 -0
- data/lib/her/model/associations/association.rb +13 -29
- data/lib/her/model/associations/association_proxy.rb +46 -0
- data/lib/her/model/associations/belongs_to_association.rb +4 -2
- data/lib/her/model/associations/has_many_association.rb +2 -1
- data/lib/her/model/associations/has_one_association.rb +2 -1
- data/lib/her/model/attributes.rb +18 -19
- data/lib/her/model/http.rb +5 -1
- data/lib/her/model/orm.rb +12 -1
- data/lib/her/model/parse.rb +27 -26
- data/lib/her/model/paths.rb +1 -1
- data/lib/her/model/relation.rb +2 -0
- data/lib/her/version.rb +1 -1
- data/spec/middleware/first_level_parse_json_spec.rb +5 -0
- data/spec/model/associations_spec.rb +76 -5
- data/spec/model/attributes_spec.rb +15 -9
- data/spec/model/dirty_spec.rb +19 -7
- data/spec/model/http_spec.rb +8 -0
- data/spec/model/introspection_spec.rb +5 -0
- data/spec/model/orm_spec.rb +77 -11
- data/spec/model/parse_spec.rb +30 -1
- data/spec/model/paths_spec.rb +4 -4
- data/spec/model/relation_spec.rb +4 -4
- data/spec/spec_helper.rb +4 -0
- data/spec/support/macros/request_macros.rb +9 -1
- metadata +63 -53
checksums.yaml
ADDED
@@ -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
data/.travis.yml
CHANGED
@@ -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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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/
|
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
|
```
|
data/her.gemspec
CHANGED
@@ -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.
|
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", "
|
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
|
data/lib/her/api.rb
CHANGED
@@ -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
|
-
# @
|
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
|
-
|
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
|
data/lib/her/errors.rb
CHANGED
@@ -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
|
@@ -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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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))
|