her 0.6.3 → 0.6.4
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 +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
|