her 0.6.3 → 0.6.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/.yardopts +2 -0
- data/README.md +3 -1
- data/her.gemspec +1 -1
- data/lib/her/api.rb +17 -17
- data/lib/her/middleware/accept_json.rb +2 -0
- data/lib/her/middleware/first_level_parse_json.rb +2 -0
- data/lib/her/middleware/parse_json.rb +1 -0
- data/lib/her/middleware/second_level_parse_json.rb +2 -0
- data/lib/her/model.rb +1 -0
- data/lib/her/model/associations.rb +19 -17
- data/lib/her/model/associations/association.rb +54 -5
- data/lib/her/model/associations/belongs_to_association.rb +14 -25
- data/lib/her/model/associations/has_many_association.rb +9 -22
- data/lib/her/model/associations/has_one_association.rb +9 -33
- data/lib/her/model/attributes.rb +55 -58
- data/lib/her/model/http.rb +33 -24
- data/lib/her/model/introspection.rb +1 -0
- data/lib/her/model/orm.rb +2 -2
- data/lib/her/model/parse.rb +2 -1
- data/lib/her/model/paths.rb +7 -11
- data/lib/her/model/relation.rb +27 -24
- data/lib/her/version.rb +1 -1
- data/spec/model/associations_spec.rb +15 -15
- data/spec/model/attributes_spec.rb +81 -0
- data/spec/model/relation_spec.rb +19 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NzFmYjMyNGNlODUyZjk4N2I2MzA5ZGZmYjdkNTQyYTQ5YzZiMzlhMw==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
ODJkYzM0YTU3YTI3YTI2ZmNlNmIxM2UxOTAzZTc2YTA3NTJjMTM0Zg==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ODEzZjJhOTYwNDM0OGI0NGQzYzBmMGVkODdiYjk3NDFkY2E5ZTNkZTRkYWZl
|
10
|
+
MDlhODA5ZDY4NTJhZjE2MDM1MDIzMjViZDIzMjUxMTdhZDA1OTE4NDliODRj
|
11
|
+
MDdkMzdiMTI2MzAzNDE4NjkxNDY5ZDRhNmFmNGJjMjlmODcyNjA=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MTBiODVmMmEzMDI5NTI2NmE0NGMzNDIyNWJkNjRiZTM2YTAxYzc1YjFhMTA4
|
14
|
+
YTM5NWRmNzVhNjE2Yjc0MDVkYTNiMjYyY2NkMTg2ODYxYjJhZDdlZjYzNmVk
|
15
|
+
NmYyNDc2M2IwYmVlNWU1YjU3Y2ZmYTE0YmQ1YWMyYjE4ZGM1YzQ=
|
data/.yardopts
ADDED
data/README.md
CHANGED
@@ -19,6 +19,8 @@ That’s it!
|
|
19
19
|
|
20
20
|
## Usage
|
21
21
|
|
22
|
+
_For a complete reference of all the methods you can use, check out [the documentation](http://rdoc.info/github/remiprev/her)._
|
23
|
+
|
22
24
|
First, you have to define which API your models will be bound to. For example, with Rails, you would create a new `config/initializers/her.rb` file with these lines:
|
23
25
|
|
24
26
|
```ruby
|
@@ -98,7 +100,7 @@ user = User.new(fullname: "Maeby Fünke")
|
|
98
100
|
user.save
|
99
101
|
```
|
100
102
|
|
101
|
-
You can look into the [`her-example`](https://github.com/remiprev/her-example) repository for a sample application using Her.
|
103
|
+
You can look into the [`her-example`](https://github.com/remiprev/her-example) repository for a sample application using Her.
|
102
104
|
|
103
105
|
## Middleware
|
104
106
|
|
data/her.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = Her::VERSION
|
8
8
|
s.authors = ["Rémi Prévost"]
|
9
9
|
s.email = ["remi@exomel.com"]
|
10
|
-
s.homepage = "http://
|
10
|
+
s.homepage = "http://her-rb.org"
|
11
11
|
s.license = "MIT"
|
12
12
|
s.summary = "A simple Representational State Transfer-based Hypertext Transfer Protocol-powered Object Relational Mapper. Her?"
|
13
13
|
s.description = "Her is an ORM that maps REST resources and collections to Ruby objects"
|
data/lib/her/api.rb
CHANGED
@@ -6,9 +6,9 @@ module Her
|
|
6
6
|
attr_reader :base_uri, :connection, :options
|
7
7
|
|
8
8
|
# Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
|
9
|
-
def self.setup(
|
9
|
+
def self.setup(opts={}, &block)
|
10
10
|
@default_api = new
|
11
|
-
@default_api.setup(
|
11
|
+
@default_api.setup(opts, &block)
|
12
12
|
end
|
13
13
|
|
14
14
|
# Create a new API object. This is useful to create multiple APIs and use them with the `uses_api` method.
|
@@ -29,9 +29,9 @@ module Her
|
|
29
29
|
|
30
30
|
# Setup the API connection.
|
31
31
|
#
|
32
|
-
# @param [Hash]
|
33
|
-
# @option
|
34
|
-
# @option
|
32
|
+
# @param [Hash] opts the Faraday options
|
33
|
+
# @option opts [String] :url The main HTTP API root (eg. `https://api.example.com`)
|
34
|
+
# @option opts [String] :ssl A hash containing [SSL options](https://github.com/technoweenie/faraday/wiki/Setting-up-SSL-certificates)
|
35
35
|
#
|
36
36
|
# @return Faraday::Connection
|
37
37
|
#
|
@@ -65,10 +65,10 @@ module Her
|
|
65
65
|
# connection.use MyCustomParser
|
66
66
|
# connection.use Faraday::Adapter::NetHttp
|
67
67
|
# end
|
68
|
-
def setup(
|
69
|
-
|
70
|
-
@base_uri =
|
71
|
-
@options =
|
68
|
+
def setup(opts={}, &blk)
|
69
|
+
opts[:url] = opts.delete(:base_uri) if opts.include?(:base_uri) # Support legacy :base_uri option
|
70
|
+
@base_uri = opts[:url]
|
71
|
+
@options = opts
|
72
72
|
@connection = Faraday.new(@options) do |connection|
|
73
73
|
yield connection if block_given?
|
74
74
|
end
|
@@ -80,20 +80,20 @@ module Her
|
|
80
80
|
# and a metadata Hash.
|
81
81
|
#
|
82
82
|
# @private
|
83
|
-
def request(
|
84
|
-
method =
|
85
|
-
path =
|
86
|
-
headers =
|
87
|
-
|
83
|
+
def request(opts={})
|
84
|
+
method = opts.delete(:_method)
|
85
|
+
path = opts.delete(:_path)
|
86
|
+
headers = opts.delete(:_headers)
|
87
|
+
opts.delete_if { |key, value| key.to_s =~ /^_/ } # Remove all internal parameters
|
88
88
|
response = @connection.send method do |request|
|
89
89
|
request.headers.merge!(headers) if headers
|
90
90
|
if method == :get
|
91
91
|
# For GET requests, treat additional parameters as querystring data
|
92
|
-
request.url path,
|
92
|
+
request.url path, opts
|
93
93
|
else
|
94
94
|
# For POST, PUT and DELETE requests, treat additional parameters as request body
|
95
95
|
request.url path
|
96
|
-
request.body =
|
96
|
+
request.body = opts
|
97
97
|
end
|
98
98
|
end
|
99
99
|
|
@@ -102,7 +102,7 @@ module Her
|
|
102
102
|
|
103
103
|
private
|
104
104
|
# @private
|
105
|
-
def self.default_api(
|
105
|
+
def self.default_api(opts={})
|
106
106
|
defined?(@default_api) ? @default_api : nil
|
107
107
|
end
|
108
108
|
end
|
@@ -2,10 +2,12 @@ module Her
|
|
2
2
|
module Middleware
|
3
3
|
# This middleware adds a "Accept: application/json" HTTP header
|
4
4
|
class AcceptJSON < Faraday::Middleware
|
5
|
+
# @private
|
5
6
|
def add_header(headers)
|
6
7
|
headers.merge! "Accept" => "application/json"
|
7
8
|
end
|
8
9
|
|
10
|
+
# @private
|
9
11
|
def call(env)
|
10
12
|
add_header(env[:request_headers])
|
11
13
|
@app.call(env)
|
@@ -6,6 +6,7 @@ module Her
|
|
6
6
|
#
|
7
7
|
# @param [String] body The response body
|
8
8
|
# @return [Mixed] the parsed response
|
9
|
+
# @private
|
9
10
|
def parse(body)
|
10
11
|
json = parse_json(body)
|
11
12
|
errors = json.delete(:errors) || {}
|
@@ -21,6 +22,7 @@ module Her
|
|
21
22
|
# the value of `env[:body]`.
|
22
23
|
#
|
23
24
|
# @param [Hash] env The response environment
|
25
|
+
# @private
|
24
26
|
def on_complete(env)
|
25
27
|
env[:body] = case env[:status]
|
26
28
|
when 204
|
@@ -7,6 +7,7 @@ module Her
|
|
7
7
|
#
|
8
8
|
# @param [String] body The response body
|
9
9
|
# @return [Mixed] the parsed response
|
10
|
+
# @private
|
10
11
|
def parse(body)
|
11
12
|
json = parse_json(body)
|
12
13
|
|
@@ -21,6 +22,7 @@ module Her
|
|
21
22
|
# the value of `env[:body]`.
|
22
23
|
#
|
23
24
|
# @param [Hash] env The response environment
|
25
|
+
# @private
|
24
26
|
def on_complete(env)
|
25
27
|
env[:body] = case env[:status]
|
26
28
|
when 204
|
data/lib/her/model.rb
CHANGED
@@ -52,10 +52,10 @@ module Her
|
|
52
52
|
# Define an *has_many* association.
|
53
53
|
#
|
54
54
|
# @param [Symbol] name The name of the method added to resources
|
55
|
-
# @param [Hash]
|
56
|
-
# @option
|
57
|
-
# @option
|
58
|
-
# @option
|
55
|
+
# @param [Hash] opts Options
|
56
|
+
# @option opts [String] :class_name The name of the class to map objects to
|
57
|
+
# @option opts [Symbol] :data_key The attribute where the data is stored
|
58
|
+
# @option opts [Path] :path The relative path where to fetch the data (defaults to `/{name}`)
|
59
59
|
#
|
60
60
|
# @example
|
61
61
|
# class User
|
@@ -70,16 +70,17 @@ module Her
|
|
70
70
|
# @user = User.find(1)
|
71
71
|
# @user.articles # => [#<Article(articles/2) id=2 title="Hello world.">]
|
72
72
|
# # Fetched via GET "/users/1/articles"
|
73
|
-
def has_many(name,
|
74
|
-
Her::Model::Associations::HasManyAssociation.attach(self, name,
|
73
|
+
def has_many(name, opts={})
|
74
|
+
Her::Model::Associations::HasManyAssociation.attach(self, name, opts)
|
75
75
|
end
|
76
76
|
|
77
77
|
# Define an *has_one* association.
|
78
78
|
#
|
79
79
|
# @param [Symbol] name The name of the method added to resources
|
80
|
-
# @
|
81
|
-
# @option
|
82
|
-
# @option
|
80
|
+
# @param [Hash] opts Options
|
81
|
+
# @option opts [String] :class_name The name of the class to map objects to
|
82
|
+
# @option opts [Symbol] :data_key The attribute where the data is stored
|
83
|
+
# @option opts [Path] :path The relative path where to fetch the data (defaults to `/{name}`)
|
83
84
|
#
|
84
85
|
# @example
|
85
86
|
# class User
|
@@ -94,17 +95,18 @@ module Her
|
|
94
95
|
# @user = User.find(1)
|
95
96
|
# @user.organization # => #<Organization(organizations/2) id=2 name="Foobar Inc.">
|
96
97
|
# # Fetched via GET "/users/1/organization"
|
97
|
-
def has_one(name,
|
98
|
-
Her::Model::Associations::HasOneAssociation.attach(self, name,
|
98
|
+
def has_one(name, opts={})
|
99
|
+
Her::Model::Associations::HasOneAssociation.attach(self, name, opts)
|
99
100
|
end
|
100
101
|
|
101
102
|
# Define a *belongs_to* association.
|
102
103
|
#
|
103
104
|
# @param [Symbol] name The name of the method added to resources
|
104
|
-
# @
|
105
|
-
# @option
|
106
|
-
# @option
|
107
|
-
# @option
|
105
|
+
# @param [Hash] opts Options
|
106
|
+
# @option opts [String] :class_name The name of the class to map objects to
|
107
|
+
# @option opts [Symbol] :data_key The attribute where the data is stored
|
108
|
+
# @option opts [Path] :path The relative path where to fetch the data (defaults to `/{class_name}.pluralize/{id}`)
|
109
|
+
# @option opts [Symbol] :foreign_key The foreign key used to build the `:id` part of the path (defaults to `{name}_id`)
|
108
110
|
#
|
109
111
|
# @example
|
110
112
|
# class User
|
@@ -119,8 +121,8 @@ module Her
|
|
119
121
|
# @user = User.find(1) # => #<User(users/1) id=1 team_id=2 name="Tobias">
|
120
122
|
# @user.team # => #<Team(teams/2) id=2 name="Developers">
|
121
123
|
# # Fetched via GET "/teams/2"
|
122
|
-
def belongs_to(name,
|
123
|
-
Her::Model::Associations::BelongsToAssociation.attach(self, name,
|
124
|
+
def belongs_to(name, opts={})
|
125
|
+
Her::Model::Associations::BelongsToAssociation.attach(self, name, opts)
|
124
126
|
end
|
125
127
|
end
|
126
128
|
end
|
@@ -2,22 +2,71 @@ module Her
|
|
2
2
|
module Model
|
3
3
|
module Associations
|
4
4
|
class Association
|
5
|
-
|
5
|
+
# @private
|
6
|
+
attr_accessor :params
|
6
7
|
|
7
8
|
# @private
|
8
9
|
def initialize(parent, opts = {})
|
9
10
|
@parent = parent
|
10
11
|
@opts = opts
|
11
|
-
@
|
12
|
+
@params = {}
|
12
13
|
|
13
14
|
@klass = @parent.class.her_nearby_class(@opts[:class_name])
|
14
15
|
@name = @opts[:name]
|
15
16
|
end
|
16
17
|
|
18
|
+
# @private
|
19
|
+
def self.parse_single(association, klass, data)
|
20
|
+
data_key = association[:data_key]
|
21
|
+
return {} unless data[data_key]
|
22
|
+
|
23
|
+
klass = klass.her_nearby_class(association[:class_name])
|
24
|
+
{ association[:name] => klass.new(data[data_key]) }
|
25
|
+
end
|
26
|
+
|
27
|
+
# @private
|
28
|
+
def assign_single_nested_attributes(attributes)
|
29
|
+
if @parent.attributes[@name].blank?
|
30
|
+
@parent.attributes[@name] = @klass.new(@klass.parse(attributes))
|
31
|
+
else
|
32
|
+
@parent.attributes[@name].assign_attributes(attributes)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @private
|
37
|
+
def fetch(opts = {})
|
38
|
+
return @opts[:default].try(:dup) if @parent.attributes.include?(@name) && @parent.attributes[@name].empty? && @params.empty?
|
39
|
+
|
40
|
+
if @parent.attributes[@name].blank? || @params.any?
|
41
|
+
path = build_association_path lambda { "#{@parent.request_path(@params)}#{@opts[:path]}" }
|
42
|
+
@klass.get(path, @params)
|
43
|
+
else
|
44
|
+
@parent.attributes[@name]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @private
|
49
|
+
def build_association_path(code)
|
50
|
+
begin
|
51
|
+
instance_exec(&code)
|
52
|
+
rescue Her::Errors::PathError
|
53
|
+
return nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
17
57
|
# Add query parameters to the HTTP request performed to fetch the data
|
18
|
-
|
19
|
-
|
20
|
-
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# class User
|
61
|
+
# include Her::Model
|
62
|
+
# has_many :comments
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# user = User.find(1)
|
66
|
+
# user.comments.where(:approved => 1) # Fetched via GET "/users/1/comments?approved=1
|
67
|
+
def where(params = {})
|
68
|
+
return self if params.blank? && @parent.attributes[@name].blank?
|
69
|
+
self.clone.tap { |a| a.params = a.params.merge(params) }
|
21
70
|
end
|
22
71
|
alias all where
|
23
72
|
|
@@ -3,33 +3,30 @@ module Her
|
|
3
3
|
module Associations
|
4
4
|
class BelongsToAssociation < Association
|
5
5
|
# @private
|
6
|
-
def self.attach(klass, name,
|
7
|
-
|
6
|
+
def self.attach(klass, name, opts)
|
7
|
+
opts = {
|
8
8
|
:class_name => name.to_s.classify,
|
9
9
|
:name => name,
|
10
10
|
:data_key => name,
|
11
|
+
:default => nil,
|
11
12
|
:foreign_key => "#{name}_id",
|
12
13
|
:path => "/#{name.to_s.pluralize}/:id"
|
13
|
-
}.merge(
|
14
|
-
klass.associations[:belongs_to] <<
|
14
|
+
}.merge(opts)
|
15
|
+
klass.associations[:belongs_to] << opts
|
15
16
|
|
16
17
|
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
17
18
|
def #{name}
|
18
19
|
cached_name = :"@_her_association_#{name}"
|
19
20
|
|
20
21
|
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
|
21
|
-
cached_data || instance_variable_set(cached_name, Her::Model::Associations::BelongsToAssociation.new(self, #{
|
22
|
+
cached_data || instance_variable_set(cached_name, Her::Model::Associations::BelongsToAssociation.new(self, #{opts.inspect}))
|
22
23
|
end
|
23
24
|
RUBY
|
24
25
|
end
|
25
26
|
|
26
27
|
# @private
|
27
|
-
def self.parse(
|
28
|
-
|
29
|
-
return {} unless data[data_key]
|
30
|
-
|
31
|
-
klass = klass.her_nearby_class(association[:class_name])
|
32
|
-
{ association[:name] => klass.new(data[data_key]) }
|
28
|
+
def self.parse(*args)
|
29
|
+
parse_single(*args)
|
33
30
|
end
|
34
31
|
|
35
32
|
# Initialize a new object
|
@@ -75,16 +72,12 @@ module Her
|
|
75
72
|
# @private
|
76
73
|
def fetch
|
77
74
|
foreign_key_value = @parent.attributes[@opts[:foreign_key].to_sym]
|
78
|
-
return
|
79
|
-
|
80
|
-
if @parent.attributes[@name].blank? || @query_attrs.any?
|
81
|
-
path = begin
|
82
|
-
@klass.build_request_path(@parent.attributes.merge(@query_attrs.merge(@klass.primary_key => foreign_key_value)))
|
83
|
-
rescue Her::Errors::PathError
|
84
|
-
return nil
|
85
|
-
end
|
75
|
+
return @opts[:default].try(:dup) if (@parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @params.empty?) || (@parent.persisted? && foreign_key_value.blank?)
|
86
76
|
|
87
|
-
|
77
|
+
if @parent.attributes[@name].blank? || @params.any?
|
78
|
+
path_params = @parent.attributes.merge(@params.merge(@klass.primary_key => foreign_key_value))
|
79
|
+
path = build_association_path lambda { @klass.build_request_path(path_params) }
|
80
|
+
@klass.get(path, @params)
|
88
81
|
else
|
89
82
|
@parent.attributes[@name]
|
90
83
|
end
|
@@ -92,11 +85,7 @@ module Her
|
|
92
85
|
|
93
86
|
# @private
|
94
87
|
def assign_nested_attributes(attributes)
|
95
|
-
|
96
|
-
@parent.attributes[@name] = @klass.new(@klass.parse(attributes))
|
97
|
-
else
|
98
|
-
@parent.attributes[@name].assign_attributes(attributes)
|
99
|
-
end
|
88
|
+
assign_single_nested_attributes(attributes)
|
100
89
|
end
|
101
90
|
end
|
102
91
|
end
|
@@ -3,22 +3,23 @@ module Her
|
|
3
3
|
module Associations
|
4
4
|
class HasManyAssociation < Association
|
5
5
|
# @private
|
6
|
-
def self.attach(klass, name,
|
7
|
-
|
6
|
+
def self.attach(klass, name, opts)
|
7
|
+
opts = {
|
8
8
|
:class_name => name.to_s.classify,
|
9
9
|
:name => name,
|
10
10
|
:data_key => name,
|
11
|
+
:default => Her::Collection.new,
|
11
12
|
:path => "/#{name}",
|
12
13
|
:inverse_of => nil
|
13
|
-
}.merge(
|
14
|
-
klass.associations[:has_many] <<
|
14
|
+
}.merge(opts)
|
15
|
+
klass.associations[:has_many] << opts
|
15
16
|
|
16
17
|
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
17
18
|
def #{name}
|
18
19
|
cached_name = :"@_her_association_#{name}"
|
19
20
|
|
20
21
|
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
|
21
|
-
cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.new(self, #{
|
22
|
+
cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.new(self, #{opts.inspect}))
|
22
23
|
end
|
23
24
|
RUBY
|
24
25
|
end
|
@@ -79,24 +80,10 @@ module Her
|
|
79
80
|
|
80
81
|
# @private
|
81
82
|
def fetch
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
path = begin
|
86
|
-
@parent.request_path(@query_attrs)
|
87
|
-
rescue Her::Errors::PathError
|
88
|
-
return nil
|
89
|
-
end
|
90
|
-
|
91
|
-
@klass.get_collection("#{path}#{@opts[:path]}", @query_attrs)
|
92
|
-
else
|
93
|
-
@parent.attributes[@name]
|
83
|
+
super.tap do |o|
|
84
|
+
inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
|
85
|
+
o.each { |entry| entry.send("#{inverse_of}=", @parent) }
|
94
86
|
end
|
95
|
-
|
96
|
-
inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
|
97
|
-
output.each { |entry| entry.send("#{inverse_of}=", @parent) }
|
98
|
-
|
99
|
-
output
|
100
87
|
end
|
101
88
|
|
102
89
|
# @private
|